Five independent items from the post-review backlog. F1: Stop on an external agent task now aborts the running child via a per-task AbortController registry reachable from the cancel route, and finalizes the assistant message as cancelled (fixing two latent bugs — catch blocks left the message streaming, and warm success-paths wrote complete on an aborted turn); warm pools/worktrees are preserved and the native path is unchanged. F2/F3: prune the tool-call parser to its two load-bearing exports (unexport eight zero-caller symbols, add a gate test for the <invoke>-as-text fallback) and route placeholder-rejection logging through pino. F6: a 90s per-chunk stall-timeout wraps native inference's fullStream via AbortSignal.any so a hung stream finalizes the message instead of hanging — no retry (a pure classifyStreamError helper is added). F7: a read-only view_session_history MCP tool (newest-N, chronological). F9: retire the unused apps/coder/web :9502 fallback SPA, keeping every API/WS/health/MCP route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
2.0 KiB
TypeScript
52 lines
2.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createCancelRegistry } from '../cancel-registry.js';
|
|
|
|
/**
|
|
* F1 — per-task abort wiring. The registry is the missing link between the Stop
|
|
* route and the in-flight external run: register an AbortController per task id,
|
|
* cancel(taskId) aborts its signal, the run's .finally deletes it. Pure (no DB /
|
|
* child / IO) so the abort + idempotency contract is unit-testable in isolation.
|
|
*/
|
|
describe('CancelRegistry (F1 abort wiring)', () => {
|
|
it('register hands back a fresh controller; cancel aborts its signal', () => {
|
|
const reg = createCancelRegistry();
|
|
const ac = reg.register('t1');
|
|
expect(ac.signal.aborted).toBe(false);
|
|
expect(reg.has('t1')).toBe(true);
|
|
|
|
expect(reg.cancel('t1')).toBe(true);
|
|
expect(ac.signal.aborted).toBe(true);
|
|
});
|
|
|
|
it('cancel on an unknown task returns false (native task / cancel-before-register)', () => {
|
|
const reg = createCancelRegistry();
|
|
expect(reg.has('nope')).toBe(false);
|
|
expect(reg.cancel('nope')).toBe(false);
|
|
});
|
|
|
|
it('double-Stop is idempotent: a second cancel never throws and the signal stays aborted', () => {
|
|
const reg = createCancelRegistry();
|
|
const ac = reg.register('t1');
|
|
|
|
expect(reg.cancel('t1')).toBe(true);
|
|
// The run-function has not hit its .finally yet, so the entry is still
|
|
// present — a rapid second Stop re-aborts (abort() no-ops) without throwing.
|
|
expect(() => reg.cancel('t1')).not.toThrow();
|
|
expect(reg.cancel('t1')).toBe(true);
|
|
expect(ac.signal.aborted).toBe(true);
|
|
});
|
|
|
|
it('cancel after delete returns false (cancel-after-natural-exit is safe)', () => {
|
|
const reg = createCancelRegistry();
|
|
reg.register('t1');
|
|
reg.delete('t1');
|
|
expect(reg.has('t1')).toBe(false);
|
|
expect(reg.cancel('t1')).toBe(false);
|
|
});
|
|
|
|
it('delete of an unknown id is a no-op (never throws)', () => {
|
|
const reg = createCancelRegistry();
|
|
expect(() => reg.delete('ghost')).not.toThrow();
|
|
});
|
|
});
|