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
103
ui-electrobun/tests/unit/hardening/backpressure.test.ts
Normal file
103
ui-electrobun/tests/unit/hardening/backpressure.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Tests for backpressure guards — paste truncation and stdout buffer limits.
|
||||
// Uses bun:test. Tests the logic from Terminal.svelte and sidecar-manager.ts.
|
||||
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
|
||||
// ── Constants (replicated from source) ──────────────────────────────────────
|
||||
|
||||
const MAX_PASTE_CHUNK = 64 * 1024; // 64 KB (Terminal.svelte)
|
||||
const MAX_LINE_SIZE = 10 * 1024 * 1024; // 10 MB (sidecar-manager.ts)
|
||||
const MAX_PENDING_BUFFER = 50 * 1024 * 1024; // 50 MB (sidecar-manager.ts)
|
||||
|
||||
// ── Replicated truncation logic ──────────────────────────────────────────────
|
||||
|
||||
function truncatePaste(payload: string): { text: string; wasTruncated: boolean } {
|
||||
if (payload.length > MAX_PASTE_CHUNK) {
|
||||
return { text: payload.slice(0, MAX_PASTE_CHUNK), wasTruncated: true };
|
||||
}
|
||||
return { text: payload, wasTruncated: false };
|
||||
}
|
||||
|
||||
function applyBufferBackpressure(buffer: string): string {
|
||||
// If buffer exceeds MAX_PENDING_BUFFER, keep only last MAX_LINE_SIZE bytes
|
||||
if (buffer.length > MAX_PENDING_BUFFER) {
|
||||
return buffer.slice(-MAX_LINE_SIZE);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function shouldTruncateLine(buffer: string): boolean {
|
||||
// If buffer exceeds MAX_LINE_SIZE without a newline, truncate
|
||||
return buffer.length > MAX_LINE_SIZE && !buffer.includes('\n');
|
||||
}
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('paste truncation', () => {
|
||||
it('passes through text under 64KB', () => {
|
||||
const text = 'hello world';
|
||||
const result = truncatePaste(text);
|
||||
expect(result.wasTruncated).toBe(false);
|
||||
expect(result.text).toBe(text);
|
||||
});
|
||||
|
||||
it('passes through text exactly at 64KB', () => {
|
||||
const text = 'x'.repeat(MAX_PASTE_CHUNK);
|
||||
const result = truncatePaste(text);
|
||||
expect(result.wasTruncated).toBe(false);
|
||||
expect(result.text.length).toBe(MAX_PASTE_CHUNK);
|
||||
});
|
||||
|
||||
it('truncates text over 64KB', () => {
|
||||
const text = 'x'.repeat(MAX_PASTE_CHUNK + 1000);
|
||||
const result = truncatePaste(text);
|
||||
expect(result.wasTruncated).toBe(true);
|
||||
expect(result.text.length).toBe(MAX_PASTE_CHUNK);
|
||||
});
|
||||
|
||||
it('preserves first 64KB of content on truncation', () => {
|
||||
const prefix = 'START-';
|
||||
const text = prefix + 'x'.repeat(MAX_PASTE_CHUNK + 1000);
|
||||
const result = truncatePaste(text);
|
||||
expect(result.text.startsWith('START-')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stdout buffer backpressure', () => {
|
||||
it('leaves buffer unchanged under 50MB', () => {
|
||||
const buffer = 'x'.repeat(1000);
|
||||
expect(applyBufferBackpressure(buffer)).toBe(buffer);
|
||||
});
|
||||
|
||||
it('truncates buffer over 50MB to last 10MB', () => {
|
||||
const buffer = 'x'.repeat(MAX_PENDING_BUFFER + 1000);
|
||||
const result = applyBufferBackpressure(buffer);
|
||||
expect(result.length).toBe(MAX_LINE_SIZE);
|
||||
});
|
||||
|
||||
it('keeps tail of buffer (most recent data)', () => {
|
||||
const head = 'H'.repeat(MAX_PENDING_BUFFER);
|
||||
const tail = 'T'.repeat(MAX_LINE_SIZE);
|
||||
const buffer = head + tail;
|
||||
const result = applyBufferBackpressure(buffer);
|
||||
// Result should be the last MAX_LINE_SIZE chars, which is all T's
|
||||
expect(result).toBe(tail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('line size guard', () => {
|
||||
it('no truncation for buffer with newlines', () => {
|
||||
const buffer = 'x'.repeat(MAX_LINE_SIZE + 100) + '\nmore data';
|
||||
expect(shouldTruncateLine(buffer)).toBe(false);
|
||||
});
|
||||
|
||||
it('truncates when buffer exceeds MAX_LINE_SIZE without newline', () => {
|
||||
const buffer = 'x'.repeat(MAX_LINE_SIZE + 1);
|
||||
expect(shouldTruncateLine(buffer)).toBe(true);
|
||||
});
|
||||
|
||||
it('no truncation at exactly MAX_LINE_SIZE', () => {
|
||||
const buffer = 'x'.repeat(MAX_LINE_SIZE);
|
||||
expect(shouldTruncateLine(buffer)).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue