Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,107 @@ describe('Traceability: document-paragraph-id-stability-and-fingerprint — Para
});
},
);

test.openspec('insertParagraphBookmarks resolves seed collisions with a deterministic salt')(
'insertParagraphBookmarks resolves seed collisions with a deterministic salt',
async ({ given, when, then, attachPrettyJson }: AllureBddContext) => {
const xmlBody =
'<w:p><w:r><w:t>Anchor context.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Duplicate clause.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Tail context.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Anchor context.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Duplicate clause.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Tail context.</w:t></w:r></w:p>';
const doc = makeDoc(xmlBody);
let duplicateIds: (string | null)[] = [];

await given('two paragraphs have identical text and identical neighbor context', async () => {
await attachPrettyJson('Collision fixture', {
duplicateParagraphIndexes: [1, 4],
duplicateText: 'Duplicate clause.',
previousText: 'Anchor context.',
nextText: 'Tail context.',
});
});

await when('insertParagraphBookmarks is called', async () => {
insertParagraphBookmarks(doc, 'test-attachment');
const paragraphs = Array.from(doc.getElementsByTagNameNS(OOXML.W_NS, 'p'));
duplicateIds = [paragraphs[1], paragraphs[4]].map((p) => getParagraphBookmarkId(p as Element));
await attachPrettyJson('Duplicate paragraph identifiers', duplicateIds);
});

await then('the colliding paragraphs receive the unsalted hash then the |salt:1 hash', () => {
expect(duplicateIds.length).toBe(2);
expect(duplicateIds[0]).toMatch(/^_bk_[0-9a-f]{12}$/);
expect(duplicateIds[1]).toMatch(/^_bk_[0-9a-f]{12}$/);
expect(duplicateIds[1]).not.toEqual(duplicateIds[0]);
// Pin the exact derivation so the test characterizes the salt-loop, not
// just "two distinct IDs". If buildParagraphSeed later includes sibling
// position or wider context, both IDs would still be distinct without
// the salt loop ever running — this assertion fails loudly in that case.
expect(duplicateIds[0]).toBe('_bk_04c5b72c79f7'); // sha12(seed)
expect(duplicateIds[1]).toBe('_bk_a2abd088979b'); // sha12(seed|salt:1)
});
},
);

test.openspec('Collision resolution is stable across independent reopens')(
'Collision resolution is stable across independent reopens',
async ({ given, when, then, attachPrettyJson }: AllureBddContext) => {
const xmlBody =
'<w:p><w:r><w:t>Anchor context.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Duplicate clause.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Tail context.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Anchor context.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Duplicate clause.</w:t></w:r></w:p>' +
'<w:p><w:r><w:t>Tail context.</w:t></w:r></w:p>';

let firstOpenIds: (string | null)[] = [];
let secondOpenIds: (string | null)[] = [];

await given('the same colliding paragraph content is opened twice independently', async () => {
await attachPrettyJson('Collision fixture', {
duplicateParagraphIndexes: [1, 4],
duplicateText: 'Duplicate clause.',
previousText: 'Anchor context.',
nextText: 'Tail context.',
});
});

await when('insertParagraphBookmarks is applied to each open', async () => {
const doc1 = makeDoc(xmlBody);
insertParagraphBookmarks(doc1, 'test-attachment');
const firstParagraphs = Array.from(doc1.getElementsByTagNameNS(OOXML.W_NS, 'p'));
firstOpenIds = [firstParagraphs[1], firstParagraphs[4]].map((p) =>
getParagraphBookmarkId(p as Element),
);

const doc2 = makeDoc(xmlBody);
insertParagraphBookmarks(doc2, 'test-attachment');
const secondParagraphs = Array.from(doc2.getElementsByTagNameNS(OOXML.W_NS, 'p'));
secondOpenIds = [secondParagraphs[1], secondParagraphs[4]].map((p) =>
getParagraphBookmarkId(p as Element),
);

await attachPrettyJson('Duplicate paragraph identifiers by open', {
firstOpenIds,
secondOpenIds,
});
});

await then('collision salts are assigned byte-identically by document order', () => {
expect(firstOpenIds.length).toBe(2);
expect(secondOpenIds).toEqual(firstOpenIds);
for (const id of firstOpenIds) {
expect(id).toMatch(/^_bk_[0-9a-f]{12}$/);
}
expect(firstOpenIds[1]).not.toEqual(firstOpenIds[0]);
// Pin the cross-open salt assignment so any drift in salt-loop iteration
// order (e.g., if derivation later considered prior-document state)
// would fail the test loudly. See companion scenario for the seed math.
expect(firstOpenIds).toEqual(['_bk_04c5b72c79f7', '_bk_a2abd088979b']);
});
},
);
});
Loading