Skip to content
Merged
Show file tree
Hide file tree
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
53 changes: 53 additions & 0 deletions tests/chronicle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,57 @@ describe('createChronicle', () => {
// Chronicle overhead per change must stay well under 1 ms
expect(perChange).toBeLessThan(1);
});

// ── Additional coverage tests ──────────────────────────────────────────────

it('forwards flush writes to an optional persistent writer', async () => {
// Build a minimal mock writer to verify that chronicle calls writeBatch.
const written = [];
const mockWriter = { writeBatch(nodes, edges) { written.push({ nodes, edges }); } };

const chronicleWithWriter = createChronicle(db, { batchMs: 0, writer: mockWriter });
db.emit('value1', 'path1');
await new Promise((r) => setTimeout(r, 10));
chronicleWithWriter.flush();
chronicleWithWriter.stop();

expect(written.length).toBeGreaterThan(0);
expect(written[0].nodes.length).toBe(1);
});

it('start() is idempotent — calling it twice does not double-subscribe', async () => {
// createChronicle already calls start() internally.
// Calling start() again must not add a second subscription.
chronicle.start(); // second call should be a no-op

db.emit('x', 'key');
await new Promise((r) => setTimeout(r, 10));
chronicle.flush();

// If start() subscribed twice we would get 2 nodes instead of 1.
expect(chronicle.stats().nodes).toBe(1);
});

it('trace respects maxDepth and stops traversal early', async () => {
// Build a 3-node chain: root → mid → leaf
db.emit('r', 'a');
await new Promise((r) => setTimeout(r, 10));
chronicle.flush();
const rootId = chronicle._nodes[0].id;

withCause(rootId, () => db.emit('m', 'b'));
await new Promise((r) => setTimeout(r, 10));
chronicle.flush();
const midId = chronicle._nodes[1].id;

withCause(midId, () => db.emit('l', 'c'));
await new Promise((r) => setTimeout(r, 10));
chronicle.flush();
const leafId = chronicle._nodes[2].id;

// With maxDepth=1 the traversal stops after visiting leaf and mid
// (root would be at depth 2, which is > maxDepth=1 → continue path triggered)
const chain = chronicle.trace(leafId, { direction: 'backward', maxDepth: 1 });
expect(chain.length).toBe(2); // leaf + mid, root cut off by maxDepth
});
});
44 changes: 44 additions & 0 deletions tests/persistent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,50 @@ describe('createPersistentWriter', () => {
expect(all.length).toBe(2);
});

it('queryEdges filters out non-edge records (nodes) from the store', () => {
// Writing both a node and an edge ensures queryEdges encounters node records
// and returns false for them (the _type !== chronicle_edge guard path).
writer.writeBatch(
[{ id: 'a', timestamp: 100, path: 'x', diff: {} }],
[{ from: 'a', to: 'b', type: 'causes', timestamp: 100 }]
);
const edges = writer.queryEdges('a');
// Only the edge should be returned, not the node record
expect(edges.length).toBe(1);
expect(edges[0].from).toBe('a');
});

it('stats counts both nodes and edges from mixed store', () => {
writer.writeBatch(
[{ id: 'n1', timestamp: 100, path: 'x', diff: {} }],
[{ from: 'n1', to: 'n2', type: 'causes', timestamp: 100 }]
);
const s = writer.stats();
expect(s.nodes).toBe(1);
expect(s.edges).toBe(1);
});

it('trace indexes causes edges for forward and backward traversal', () => {
// Two root→leaf edges — ensures both forward and backward index entries are built
writer.writeBatch(
[
{ id: 'root', timestamp: 100, path: 'r', diff: {} },
{ id: 'leaf', timestamp: 200, path: 'l', diff: {} },
],
[
{ from: 'root', to: 'leaf', type: 'causes', timestamp: 200 },
// A second root sharing the same 'to' (leaf) ensures the backward index
// append path (edgeIndex.backward.get(to).push(from)) is also exercised
// for a key that already exists.
{ from: 'root', to: 'leaf', type: 'causes', timestamp: 201 },
]
);
const backward = writer.trace('leaf', { direction: 'backward' });
expect(backward.some((n) => n.id === 'root')).toBe(true);
const forward = writer.trace('root', { direction: 'forward' });
expect(forward.some((n) => n.id === 'leaf')).toBe(true);
});

it('trace walks causal chain backward', () => {
writer.writeBatch(
[
Expand Down
80 changes: 80 additions & 0 deletions tests/praxis.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,16 @@ describe('diff-classification module', () => {
expect(result).toMatch(/mutation/);
});
});

// Direct impl guard-clause test — covers the "no DIFF_RECORDED event" skip path inside
// a multi-line block (line 182) that is unreachable via the engine when eventTypes
// pre-filters the batch.
describe('rule impl guard clauses (direct call)', () => {
it('scoreImpactRule.impl skips when no matching event is passed', () => {
const result = scoreImpactRule.impl({}, []);
expect(result.kind).toBe('skip');
});
});
});

// ── retention-policy ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -415,6 +425,25 @@ describe('retention-policy module', () => {
expect(typeof result).toBe('string');
});
});

// Direct impl guard-clause test — covers the "no RETENTION_AUDIT_REQUESTED event" skip path
// that is unreachable via the engine when eventTypes pre-filters the batch.
describe('rule impl guard clauses (direct call)', () => {
it('agePruningRule.impl skips when no matching event is passed', () => {
const result = agePruningRule.impl({}, []);
expect(result.kind).toBe('skip');
});

it('quotaEnforcementRule.impl skips when no matching event is passed', () => {
const result = quotaEnforcementRule.impl({}, []);
expect(result.kind).toBe('skip');
});

it('archivalGateRule.impl skips when no matching event is passed', () => {
const result = archivalGateRule.impl({}, []);
expect(result.kind).toBe('skip');
});
});
});

// ── alerting ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -645,6 +674,25 @@ describe('alerting module', () => {
expect(typeof result).toBe('string');
});
});

// Direct impl guard-clause tests — cover the "no ALERT_EVALUATION_REQUESTED event" skip paths
// that are unreachable via the engine when eventTypes pre-filters the batch.
describe('rule impl guard clauses (direct call)', () => {
it('burstDetectionRule.impl skips when no matching event is passed', () => {
const result = burstDetectionRule.impl({}, []);
expect(result.kind).toBe('skip');
});

it('criticalSpikeRule.impl skips when no matching event is passed', () => {
const result = criticalSpikeRule.impl({}, []);
expect(result.kind).toBe('skip');
});

it('impactAnomalyRule.impl skips when no matching event is passed', () => {
const result = impactAnomalyRule.impl({}, []);
expect(result.kind).toBe('skip');
});
});
});

// ── integrity ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -820,6 +868,38 @@ describe('integrity module', () => {
expect(noDuplicateNodesConstraint.impl({ context: {} })).toBe(true);
});
});

// Direct impl guard-clause tests — cover the "no matching event" skip paths
// and branch paths unreachable via the engine when eventTypes pre-filters the batch.
describe('rule impl guard clauses (direct call)', () => {
it('gapDetectionRule.impl skips when no matching event is passed', () => {
const result = gapDetectionRule.impl({}, []);
expect(result.kind).toBe('skip');
});

it('replayValidationRule.impl skips when no matching event is passed', () => {
const result = replayValidationRule.impl({}, []);
expect(result.kind).toBe('skip');
});

it('replayValidationRule.impl ignores nodes without a diff property', () => {
// A node that lacks a diff (or diff.after) should be skipped by the
// reconstruction loop (line 278: if (node.diff && node.diff.after !== undefined)).
const nodes = [
{ id: 'n1', path: 'x', timestamp: 1, diff: { after: 1 } },
{ id: 'n2', path: 'y', timestamp: 2 }, // no diff at all
{ id: 'n3', path: 'z', timestamp: 3, diff: {} }, // diff without .after
];
// Expected reconstructed state only contains x:1 (from n1).
const expectedChecksum = simpleHash({ x: 1 });
const result = replayValidationRule.impl({}, [{
tag: REPLAY_VALIDATION_REQUESTED,
payload: { nodes, expectedChecksum, initialState: {} },
}]);
expect(result.kind).toBe('emit');
expect(result.facts[0].tag).toBe('chronos.integrity.replayValid');
});
});
});

// ── createChronosEngine ────────────────────────────────────────────────────
Expand Down
Loading