Files
Dragan Spiridonov f2742c8651 docs(adr): add ADR-116 for proof-gate + cross-ref from code
The proof-gate work cited only upstream RuVector 'ADR-194' — which is not an
AQE in-repo ADR. Adds ADR-116 (the AQE-side integration decision: port the
hash-chain to TS, opt-in/fail-soft/single-store wiring, and why NOT to depend
on the @ruvector npm packages — backed by the verified ruvllm mock-mode/demo
findings) and cross-references it from the four source/test files that cite
ADR-194, so the in-repo decision is discoverable from the code.

Comment/docs only — no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 06:22:43 +00:00

97 lines
3.6 KiB
TypeScript

/**
* Tests for the HashChainGate TS port (ported from ruvector-proof-gate,
* upstream ADR-194; AQE ADR-116).
* Mirrors the Rust crate's re-derivation tamper cases + adds persistence round-trip.
*/
import { describe, it, expect } from 'vitest';
import { HashChainGate, type HashChainState } from '../../../src/integrations/ruvector/proof-gate.js';
function gateWith(n: number): HashChainGate {
const g = new HashChainGate();
for (let i = 0; i < n; i++) g.admit({ op: 'insert', table: 'qe_patterns', seq: i, rows: i * 10 });
return g;
}
describe('HashChainGate — admission & receipts', () => {
it('produces sequential, unique commitments and verifies its own receipts', () => {
const g = new HashChainGate();
const r0 = g.admit('a');
const r1 = g.admit('b');
expect(r0.sequence).toBe(0);
expect(r1.sequence).toBe(1);
expect(r0.chainCommitment).not.toBe(r1.chainCommitment);
expect(g.verifyReceipt(r0)).toBe(true);
expect(g.verifyReceipt(r1)).toBe(true);
expect(g.length).toBe(2);
});
it('rejects a receipt whose commitment was altered', () => {
const g = new HashChainGate();
const r = g.admit('x');
expect(g.verifyReceipt({ ...r, chainCommitment: 'deadbeef' })).toBe(false);
});
it('chains identical payloads to DIFFERENT commitments (order binds)', () => {
const g = new HashChainGate();
const a = g.admit('same');
const b = g.admit('same');
expect(a.payloadHash).toBe(b.payloadHash); // same content
expect(a.chainCommitment).not.toBe(b.chainCommitment); // different chain position
});
it('canonicalizes object key order (equal payloads → equal hash)', () => {
const g1 = new HashChainGate();
const g2 = new HashChainGate();
expect(g1.admit({ a: 1, b: 2 }).payloadHash).toBe(g2.admit({ b: 2, a: 1 }).payloadHash);
});
});
describe('HashChainGate — verifyIntegrity (re-derivation)', () => {
it('a clean chain (and an empty chain) re-verifies', () => {
expect(gateWith(8).verifyIntegrity()).toBe(true);
expect(new HashChainGate().verifyIntegrity()).toBe(true);
});
it('detects a mutated commitment, payload hash, reorder, and length mismatch', () => {
// Mutated commitment
const a = gateWith(8).toJSON();
a.chain[3] = a.chain[3].replace(/^./, (c) => (c === 'f' ? '0' : 'f'));
expect(HashChainGate.fromJSON(a).verifyIntegrity()).toBe(false);
// Mutated payload hash
const b = gateWith(8).toJSON();
b.payloadHashes[2] = b.payloadHashes[2].replace(/^./, (c) => (c === 'f' ? '0' : 'f'));
expect(HashChainGate.fromJSON(b).verifyIntegrity()).toBe(false);
// Reordered entries
const c = gateWith(8).toJSON();
[c.chain[2], c.chain[5]] = [c.chain[5], c.chain[2]];
[c.payloadHashes[2], c.payloadHashes[5]] = [c.payloadHashes[5], c.payloadHashes[2]];
expect(HashChainGate.fromJSON(c).verifyIntegrity()).toBe(false);
// Length mismatch
const d = gateWith(8).toJSON();
d.payloadHashes.pop();
expect(HashChainGate.fromJSON(d).verifyIntegrity()).toBe(false);
});
});
describe('HashChainGate — persistence round-trip', () => {
it('survives toJSON/fromJSON with intact verification and continuable chain', () => {
const g = gateWith(5);
const state: HashChainState = g.toJSON();
const restored = HashChainGate.fromJSON(state);
expect(restored.verifyIntegrity()).toBe(true);
expect(restored.chainRoot()).toBe(g.chainRoot());
expect(restored.length).toBe(5);
// Chain continues correctly from the restored head.
const r = restored.admit('after-restore');
expect(r.sequence).toBe(5);
expect(restored.verifyIntegrity()).toBe(true);
});
});