Skip to content

Commit be37179

Browse files
committed
fix(server): hash code changed after introduction of OCR
See b4e5d9d#r182901277
1 parent 3b57d2b commit be37179

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import blobService from "./blob.js";
4+
import type { Blob } from "./blob-interface.js";
5+
6+
// These tests lock the exact output of calculateContentHash against known inputs.
7+
// The hash is written to entity_changes.hash and shipped to sync peers — any
8+
// change to the formula (fields, separators, ordering) silently invalidates
9+
// hashes on already-synced blobs, so changes must be deliberate. If a code
10+
// change breaks these expectations, update the expected values AND ensure a
11+
// migration or sync-compatibility plan is in place.
12+
describe("calculateContentHash", () => {
13+
const baseBlob: Blob = {
14+
blobId: "blob001",
15+
content: "hello world",
16+
utcDateModified: "2026-01-01 00:00:00.000Z"
17+
};
18+
19+
it("hashes a blob without textRepresentation to the locked value (no trailing separator)", () => {
20+
// Must match hash("blobId|content") — same formula used before
21+
// textRepresentation existed, so pre-OCR blobs don't re-hash on upgrade.
22+
expect(blobService.calculateContentHash(baseBlob)).toBe("dyhIZrQHB3Bb1bhcZVPld8Q6ONU=");
23+
});
24+
25+
it("hashes a blob with textRepresentation to the locked value", () => {
26+
expect(blobService.calculateContentHash({ ...baseBlob, textRepresentation: "OCR result" }))
27+
.toBe("CsMYMZbvYJtrGVGJwxnr5w6KbMg=");
28+
});
29+
30+
it("treats undefined, null, and empty-string textRepresentation identically", () => {
31+
const undefinedHash = blobService.calculateContentHash(baseBlob);
32+
const nullHash = blobService.calculateContentHash({ ...baseBlob, textRepresentation: null });
33+
const emptyHash = blobService.calculateContentHash({ ...baseBlob, textRepresentation: "" });
34+
expect(nullHash).toBe(undefinedHash);
35+
expect(emptyHash).toBe(undefinedHash);
36+
});
37+
38+
it("produces different hashes for different blobIds, content, and textRepresentation", () => {
39+
const base = blobService.calculateContentHash(baseBlob);
40+
const diffId = blobService.calculateContentHash({ ...baseBlob, blobId: "blob002" });
41+
const diffContent = blobService.calculateContentHash({ ...baseBlob, content: "other" });
42+
const diffTextRep = blobService.calculateContentHash({ ...baseBlob, textRepresentation: "x" });
43+
expect(diffId).not.toBe(base);
44+
expect(diffContent).not.toBe(base);
45+
expect(diffTextRep).not.toBe(base);
46+
});
47+
48+
it("hashes string and equivalent Buffer content identically", () => {
49+
const stringHash = blobService.calculateContentHash(baseBlob);
50+
const bufferHash = blobService.calculateContentHash({ ...baseBlob, content: Buffer.from("hello world") });
51+
expect(bufferHash).toBe(stringHash);
52+
});
53+
});

apps/server/src/services/blob.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ function processContent(content: Buffer | string | null, isProtected: boolean, i
5151
}
5252

5353
function calculateContentHash({ blobId, content, textRepresentation }: Blob) {
54-
return hash(`${blobId}|${content.toString()}|${textRepresentation ?? ""}`);
54+
const textRepresentationSegment = textRepresentation ? `|${textRepresentation}` : "";
55+
return hash(`${blobId}|${content.toString()}${textRepresentationSegment}`);
5556
}
5657

5758
export default {

0 commit comments

Comments
 (0)