test: complete test suite — 166 new tests (stores + hardening + agent)
@agor/stores (37 tests): - theme: 6 (17 themes, 3 groups, no duplicates) - notifications: 11 (types, rate limiter, window expiry) - health: 20 (scoring, burn rate, context pressure, tool tracking) Electrobun stores (90 tests): - agent-store: 27 (seqId, dedup, double-start guard, persistence) - workspace-store: 17 (CRUD, derived state, aggregates) - plugin-store: 14 (commands, events, permissions, meta) - keybinding-store: 18 (defaults, chords, conflicts, capture) Hardening (39 tests): - durable-sequencing: 10 (monotonic, dedup, restore) - file-conflict: 10 (mtime, atomic write, workflows) - backpressure: 7 (paste 64KB, buffer 50MB, line 10MB) - retention: 7 (count, age, running protected) - channel-acl: 9 (join/leave, rejection, isolation) Total across all suites: 1,020+ tests
This commit is contained in:
parent
c0eca4964a
commit
1995f03682
7 changed files with 911 additions and 10 deletions
142
ui-electrobun/tests/unit/hardening/durable-sequencing.test.ts
Normal file
142
ui-electrobun/tests/unit/hardening/durable-sequencing.test.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// Tests for durable sequencing — monotonic seqId assignment and deduplication.
|
||||
// Uses bun:test.
|
||||
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
|
||||
// ── Replicated seqId counter from agent-store.svelte.ts ─────────────────────
|
||||
|
||||
function createSeqCounter() {
|
||||
const counters = new Map<string, number>();
|
||||
return {
|
||||
next(sessionId: string): number {
|
||||
const current = counters.get(sessionId) ?? 0;
|
||||
const next = current + 1;
|
||||
counters.set(sessionId, next);
|
||||
return next;
|
||||
},
|
||||
get(sessionId: string): number {
|
||||
return counters.get(sessionId) ?? 0;
|
||||
},
|
||||
set(sessionId: string, value: number): void {
|
||||
counters.set(sessionId, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ── Deduplication logic ─────────────────────────────────────────────────────
|
||||
|
||||
interface RawMsg {
|
||||
msgId: string;
|
||||
seqId: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function deduplicateMessages(messages: RawMsg[]): { deduplicated: RawMsg[]; maxSeqId: number } {
|
||||
const seqIdSet = new Set<number>();
|
||||
const deduplicated: RawMsg[] = [];
|
||||
let maxSeqId = 0;
|
||||
|
||||
for (const m of messages) {
|
||||
const sid = m.seqId ?? 0;
|
||||
if (sid > 0 && seqIdSet.has(sid)) continue;
|
||||
if (sid > 0) seqIdSet.add(sid);
|
||||
if (sid > maxSeqId) maxSeqId = sid;
|
||||
deduplicated.push(m);
|
||||
}
|
||||
|
||||
return { deduplicated, maxSeqId };
|
||||
}
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('seqId monotonic assignment', () => {
|
||||
it('starts at 1', () => {
|
||||
const counter = createSeqCounter();
|
||||
expect(counter.next('s1')).toBe(1);
|
||||
});
|
||||
|
||||
it('never decreases', () => {
|
||||
const counter = createSeqCounter();
|
||||
let prev = 0;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const next = counter.next('s1');
|
||||
expect(next).toBeGreaterThan(prev);
|
||||
prev = next;
|
||||
}
|
||||
});
|
||||
|
||||
it('each call returns unique value', () => {
|
||||
const counter = createSeqCounter();
|
||||
const ids = new Set<number>();
|
||||
for (let i = 0; i < 50; i++) {
|
||||
ids.add(counter.next('s1'));
|
||||
}
|
||||
expect(ids.size).toBe(50);
|
||||
});
|
||||
|
||||
it('independent per session', () => {
|
||||
const counter = createSeqCounter();
|
||||
expect(counter.next('a')).toBe(1);
|
||||
expect(counter.next('b')).toBe(1);
|
||||
expect(counter.next('a')).toBe(2);
|
||||
expect(counter.next('b')).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deduplication', () => {
|
||||
it('removes messages with duplicate seqIds', () => {
|
||||
const messages: RawMsg[] = [
|
||||
{ msgId: '1', seqId: 1, content: 'hello' },
|
||||
{ msgId: '2', seqId: 2, content: 'world' },
|
||||
{ msgId: '3', seqId: 1, content: 'hello-dup' }, // duplicate
|
||||
];
|
||||
const { deduplicated } = deduplicateMessages(messages);
|
||||
expect(deduplicated).toHaveLength(2);
|
||||
expect(deduplicated.map(m => m.msgId)).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('keeps first occurrence of duplicate seqId', () => {
|
||||
const messages: RawMsg[] = [
|
||||
{ msgId: 'a', seqId: 5, content: 'first' },
|
||||
{ msgId: 'b', seqId: 5, content: 'second' },
|
||||
];
|
||||
const { deduplicated } = deduplicateMessages(messages);
|
||||
expect(deduplicated).toHaveLength(1);
|
||||
expect(deduplicated[0].msgId).toBe('a');
|
||||
});
|
||||
|
||||
it('preserves messages with seqId 0 (unsequenced)', () => {
|
||||
const messages: RawMsg[] = [
|
||||
{ msgId: 'x', seqId: 0, content: 'legacy' },
|
||||
{ msgId: 'y', seqId: 0, content: 'legacy2' },
|
||||
];
|
||||
const { deduplicated } = deduplicateMessages(messages);
|
||||
expect(deduplicated).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns correct maxSeqId', () => {
|
||||
const messages: RawMsg[] = [
|
||||
{ msgId: '1', seqId: 3, content: 'a' },
|
||||
{ msgId: '2', seqId: 7, content: 'b' },
|
||||
{ msgId: '3', seqId: 5, content: 'c' },
|
||||
];
|
||||
const { maxSeqId } = deduplicateMessages(messages);
|
||||
expect(maxSeqId).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore resumes from max seqId', () => {
|
||||
it('counter resumes after restoring maxSeqId', () => {
|
||||
const counter = createSeqCounter();
|
||||
// Simulate: restored messages had max seqId 42
|
||||
counter.set('session-1', 42);
|
||||
expect(counter.next('session-1')).toBe(43);
|
||||
expect(counter.next('session-1')).toBe(44);
|
||||
});
|
||||
|
||||
it('handles empty restore (maxSeqId 0)', () => {
|
||||
const counter = createSeqCounter();
|
||||
counter.set('session-1', 0);
|
||||
expect(counter.next('session-1')).toBe(1);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue