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
151
ui-electrobun/tests/unit/hardening/retention.test.ts
Normal file
151
ui-electrobun/tests/unit/hardening/retention.test.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// Tests for session retention — enforceMaxSessions logic.
|
||||
// Uses bun:test. Tests the retention count + age pruning from agent-store.svelte.ts.
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
|
||||
// ── Replicated types and retention logic ────────────────────────────────────
|
||||
|
||||
interface SessionEntry {
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
status: 'idle' | 'running' | 'done' | 'error';
|
||||
lastMessageTs: number;
|
||||
}
|
||||
|
||||
interface RetentionConfig {
|
||||
count: number;
|
||||
days: number;
|
||||
}
|
||||
|
||||
function setRetentionConfig(count: number, days: number): RetentionConfig {
|
||||
return {
|
||||
count: Math.max(1, Math.min(50, count)),
|
||||
days: Math.max(1, Math.min(365, days)),
|
||||
};
|
||||
}
|
||||
|
||||
function enforceMaxSessions(
|
||||
sessions: SessionEntry[],
|
||||
projectId: string,
|
||||
config: RetentionConfig,
|
||||
): string[] {
|
||||
const now = Date.now();
|
||||
const maxAgeMs = config.days * 24 * 60 * 60 * 1000;
|
||||
|
||||
// Filter to this project's non-running sessions, sorted newest first
|
||||
const projectSessions = sessions
|
||||
.filter(s => s.projectId === projectId && s.status !== 'running')
|
||||
.sort((a, b) => b.lastMessageTs - a.lastMessageTs);
|
||||
|
||||
const toPurge: string[] = [];
|
||||
|
||||
// Prune by count
|
||||
if (projectSessions.length > config.count) {
|
||||
const excess = projectSessions.slice(config.count);
|
||||
for (const s of excess) toPurge.push(s.sessionId);
|
||||
}
|
||||
|
||||
// Prune by age
|
||||
for (const s of projectSessions) {
|
||||
if (s.lastMessageTs > 0 && (now - s.lastMessageTs) > maxAgeMs) {
|
||||
if (!toPurge.includes(s.sessionId)) toPurge.push(s.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
return toPurge;
|
||||
}
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('setRetentionConfig', () => {
|
||||
it('clamps count to [1, 50]', () => {
|
||||
expect(setRetentionConfig(0, 30).count).toBe(1);
|
||||
expect(setRetentionConfig(100, 30).count).toBe(50);
|
||||
expect(setRetentionConfig(5, 30).count).toBe(5);
|
||||
});
|
||||
|
||||
it('clamps days to [1, 365]', () => {
|
||||
expect(setRetentionConfig(5, 0).days).toBe(1);
|
||||
expect(setRetentionConfig(5, 500).days).toBe(365);
|
||||
expect(setRetentionConfig(5, 30).days).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enforceMaxSessions — count-based pruning', () => {
|
||||
it('keeps only N most recent sessions', () => {
|
||||
const now = Date.now();
|
||||
const sessions: SessionEntry[] = [
|
||||
{ sessionId: 's1', projectId: 'p1', status: 'done', lastMessageTs: now - 50000 },
|
||||
{ sessionId: 's2', projectId: 'p1', status: 'done', lastMessageTs: now - 40000 },
|
||||
{ sessionId: 's3', projectId: 'p1', status: 'done', lastMessageTs: now - 30000 },
|
||||
{ sessionId: 's4', projectId: 'p1', status: 'done', lastMessageTs: now - 20000 },
|
||||
{ sessionId: 's5', projectId: 'p1', status: 'done', lastMessageTs: now - 10000 },
|
||||
];
|
||||
const config: RetentionConfig = { count: 3, days: 365 };
|
||||
const toPurge = enforceMaxSessions(sessions, 'p1', config);
|
||||
expect(toPurge).toHaveLength(2);
|
||||
// s1 and s2 are oldest
|
||||
expect(toPurge).toContain('s1');
|
||||
expect(toPurge).toContain('s2');
|
||||
});
|
||||
|
||||
it('does not purge when under limit', () => {
|
||||
const now = Date.now();
|
||||
const sessions: SessionEntry[] = [
|
||||
{ sessionId: 's1', projectId: 'p1', status: 'done', lastMessageTs: now },
|
||||
];
|
||||
const config: RetentionConfig = { count: 5, days: 365 };
|
||||
expect(enforceMaxSessions(sessions, 'p1', config)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enforceMaxSessions — age-based pruning', () => {
|
||||
it('prunes sessions older than retention days', () => {
|
||||
const now = Date.now();
|
||||
const oldTs = now - (31 * 24 * 60 * 60 * 1000); // 31 days ago
|
||||
const sessions: SessionEntry[] = [
|
||||
{ sessionId: 's-old', projectId: 'p1', status: 'done', lastMessageTs: oldTs },
|
||||
{ sessionId: 's-new', projectId: 'p1', status: 'done', lastMessageTs: now },
|
||||
];
|
||||
const config: RetentionConfig = { count: 10, days: 30 };
|
||||
const toPurge = enforceMaxSessions(sessions, 'p1', config);
|
||||
expect(toPurge).toEqual(['s-old']);
|
||||
});
|
||||
|
||||
it('keeps sessions within retention window', () => {
|
||||
const now = Date.now();
|
||||
const recentTs = now - (5 * 24 * 60 * 60 * 1000); // 5 days ago
|
||||
const sessions: SessionEntry[] = [
|
||||
{ sessionId: 's1', projectId: 'p1', status: 'done', lastMessageTs: recentTs },
|
||||
];
|
||||
const config: RetentionConfig = { count: 10, days: 30 };
|
||||
expect(enforceMaxSessions(sessions, 'p1', config)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enforceMaxSessions — running sessions protected', () => {
|
||||
it('never purges running sessions', () => {
|
||||
const now = Date.now();
|
||||
const sessions: SessionEntry[] = [
|
||||
{ sessionId: 's-running', projectId: 'p1', status: 'running', lastMessageTs: now - 999999999 },
|
||||
{ sessionId: 's-done', projectId: 'p1', status: 'done', lastMessageTs: now },
|
||||
];
|
||||
const config: RetentionConfig = { count: 1, days: 1 };
|
||||
const toPurge = enforceMaxSessions(sessions, 'p1', config);
|
||||
expect(toPurge).not.toContain('s-running');
|
||||
});
|
||||
});
|
||||
|
||||
describe('enforceMaxSessions — project isolation', () => {
|
||||
it('only prunes sessions for the specified project', () => {
|
||||
const now = Date.now();
|
||||
const sessions: SessionEntry[] = [
|
||||
{ sessionId: 'p1-s1', projectId: 'p1', status: 'done', lastMessageTs: now - 1000 },
|
||||
{ sessionId: 'p2-s1', projectId: 'p2', status: 'done', lastMessageTs: now - 1000 },
|
||||
];
|
||||
const config: RetentionConfig = { count: 0, days: 365 }; // count 0 → clamped to 1
|
||||
const actualConfig = setRetentionConfig(0, 365);
|
||||
const toPurge = enforceMaxSessions(sessions, 'p1', actualConfig);
|
||||
expect(toPurge).not.toContain('p2-s1');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue