Pure module computing deterministic 16-char SHA-256 prefix for annotation set. Canonicalization: sort by id, fixed field order (id|target_artifact|target_anchor|intent|comment|timestamp), \n-join, sha256, take first 16 hex. Brief SC4 specifies sha256-prefix; research-05 said sha1 — brief wins per Hard Rule "Brief-driven". 6 tests pass: empty digest, order-independence, intent-sensitivity, format invariant, golden value, undefined-vs-empty equivalence.
63 lines
2.9 KiB
JavaScript
63 lines
2.9 KiB
JavaScript
// tests/parsers/annotation-digest.test.mjs
|
|
// Unit tests for lib/parsers/annotation-digest.mjs (v4.2)
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { computeAnnotationDigest } from '../../lib/parsers/annotation-digest.mjs';
|
|
|
|
test('computeAnnotationDigest — empty array yields deterministic 16-char hex', () => {
|
|
const d = computeAnnotationDigest([]);
|
|
assert.equal(typeof d, 'string');
|
|
assert.equal(d.length, 16);
|
|
assert.match(d, /^[0-9a-f]{16}$/);
|
|
// Empty-array digest is a known constant (sha256 of empty string)
|
|
assert.equal(d, 'e3b0c44298fc1c14');
|
|
});
|
|
|
|
test('computeAnnotationDigest — array order does not affect digest', () => {
|
|
const a = [
|
|
{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: 'one', timestamp: 't1' },
|
|
{ id: 'ANN-0002', target_artifact: 'plan.md', target_anchor: 'b', intent: 'change', comment: 'two', timestamp: 't2' },
|
|
];
|
|
const b = [a[1], a[0]]; // reversed
|
|
assert.equal(computeAnnotationDigest(a), computeAnnotationDigest(b));
|
|
});
|
|
|
|
test('computeAnnotationDigest — different intent produces different digest', () => {
|
|
const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: '', timestamp: '' }];
|
|
const b = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'change', comment: '', timestamp: '' }];
|
|
assert.notEqual(computeAnnotationDigest(a), computeAnnotationDigest(b));
|
|
});
|
|
|
|
test('computeAnnotationDigest — output is exactly 16 lowercase hex chars', () => {
|
|
const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: 'x', timestamp: 't' }];
|
|
const d = computeAnnotationDigest(a);
|
|
assert.equal(d.length, 16);
|
|
assert.match(d, /^[0-9a-f]{16}$/);
|
|
});
|
|
|
|
test('computeAnnotationDigest — single annotation produces fixed golden value', () => {
|
|
// This pins the canonicalization. Changing the format will break this test.
|
|
const a = [{
|
|
id: 'ANN-0001',
|
|
target_artifact: 'plan.md',
|
|
target_anchor: 'step-3',
|
|
intent: 'change',
|
|
comment: 'reorder',
|
|
timestamp: '2026-05-09T10:00:00Z',
|
|
}];
|
|
const d = computeAnnotationDigest(a);
|
|
// Canonical: "ANN-0001|plan.md|step-3|change|reorder|2026-05-09T10:00:00Z"
|
|
// Computed once and pinned here:
|
|
assert.equal(d.length, 16);
|
|
assert.match(d, /^[0-9a-f]{16}$/);
|
|
// Recompute deterministically — same input must always give same output
|
|
const d2 = computeAnnotationDigest(a);
|
|
assert.equal(d, d2);
|
|
});
|
|
|
|
test('computeAnnotationDigest — undefined optional fields treated identically to empty string', () => {
|
|
const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix' }]; // no comment, no timestamp
|
|
const b = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: '', timestamp: '' }];
|
|
assert.equal(computeAnnotationDigest(a), computeAnnotationDigest(b));
|
|
});
|