mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-13 18:29:32 +01:00
A coordinator can leave text for a worker without typing into its pane. - Store (src/web/agent-inbox.ts): read until ack by id, seen only for the receiver, 16 KiB/message, 512 KiB + 200 messages per inbox, full = 409 CONFLICT (never drops the oldest), 12 h TTL from each message's createdAt. - Routes (routes/inbox-routes.ts): POST/GET /api/sessions/:id/inbox (?wait= parks while nothing is unseen), POST .../inbox/ack (receiver only, 403 otherwise), GET /api/agent-inbox/summary (owner-scoped counts, not in toState()). Caller = resolveParentSessionId(). - Inbox waiters are the wait registry's third kind: same caps, ended by cancelAll/cancelEverything. - Opt-in snapshot (agentInboxPersistEnabled, synced, default off): agent-inbox.json via dataPath(), 0600 tmp+rename, off deletes it, boot prune only after a successful restore. Dropped on delete, kept on detach. - CLI: codeman agent post | inbox [--wait] | ack, INBOX column in ls. - waitCapacityResponse/abortOnClientHangUp moved to route-helpers.ts unchanged. - Docs: api-reference, README, CLAUDE.md, architecture-invariants, skill + plugin mirror. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
68 lines
3.0 KiB
TypeScript
68 lines
3.0 KiB
TypeScript
/** @fileoverview Startup restoration must gate destructive tab-layout reconciliation. */
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { WebServer } from '../src/web/server.js';
|
|
|
|
describe('tab layout restore gate', () => {
|
|
it('does not unlock pruning, cleanup stale state, or reconcile after a failed restore', async () => {
|
|
const markRestorationComplete = vi.fn();
|
|
const markRestorationFailed = vi.fn();
|
|
const reconcileAfterRestoration = vi.fn(async () => {});
|
|
const cleanupStaleSessions = vi.fn(async () => 0);
|
|
const server = Object.create(WebServer.prototype) as {
|
|
tabLayouts: {
|
|
markRestorationComplete: typeof markRestorationComplete;
|
|
markRestorationFailed: typeof markRestorationFailed;
|
|
reconcileAfterRestoration: typeof reconcileAfterRestoration;
|
|
};
|
|
cleanupStaleSessions: typeof cleanupStaleSessions;
|
|
mux: { reconcileSessions(): Promise<never> };
|
|
restoreMuxSessions(): Promise<boolean>;
|
|
finalizeRestoredState(restored: boolean): Promise<void>;
|
|
};
|
|
server.tabLayouts = { markRestorationComplete, markRestorationFailed, reconcileAfterRestoration };
|
|
server.cleanupStaleSessions = cleanupStaleSessions;
|
|
server.mux = { reconcileSessions: vi.fn(async () => Promise.reject(new Error('mux unavailable'))) };
|
|
const errorLog = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const restored = await server.restoreMuxSessions();
|
|
await server.finalizeRestoredState(restored);
|
|
|
|
expect(restored).toBe(false);
|
|
expect(markRestorationComplete).not.toHaveBeenCalled();
|
|
expect(markRestorationFailed).toHaveBeenCalledTimes(1);
|
|
expect(cleanupStaleSessions).not.toHaveBeenCalled();
|
|
expect(reconcileAfterRestoration).not.toHaveBeenCalled();
|
|
expect(errorLog).toHaveBeenCalledWith('[Server] Failed to restore mux sessions:', expect.any(Error));
|
|
});
|
|
|
|
it('unlocks cleanup and reconciliation only after successful restoration', async () => {
|
|
const order: string[] = [];
|
|
const server = Object.create(WebServer.prototype) as {
|
|
tabLayouts: {
|
|
markRestorationComplete(): void;
|
|
markRestorationFailed(): void;
|
|
reconcileAfterRestoration(): Promise<void>;
|
|
};
|
|
cleanupStaleSessions(): Promise<number>;
|
|
finalizeRestoredState(restored: boolean): Promise<void>;
|
|
sessions: Map<string, unknown>;
|
|
store: { getSessions(): Record<string, unknown> };
|
|
};
|
|
// Read by the agent-inbox prune at the end of a successful restore.
|
|
server.sessions = new Map();
|
|
server.store = { getSessions: () => ({}) };
|
|
server.tabLayouts = {
|
|
markRestorationComplete: () => order.push('complete'),
|
|
markRestorationFailed: () => order.push('failed'),
|
|
reconcileAfterRestoration: async () => {
|
|
order.push('reconcile');
|
|
},
|
|
};
|
|
server.cleanupStaleSessions = async () => (order.push('cleanup'), 0);
|
|
|
|
await server.finalizeRestoredState(true);
|
|
|
|
expect(order).toEqual(['complete', 'cleanup', 'reconcile']);
|
|
});
|
|
});
|