evOo-local/packages/adapters/src/pi-runtime-bargein-stress.test.ts
cynarAI ca869561ac
Voice-first streaming, non-blocking follow-ups and chat UX (#205)
* media: check ffmpeg filter support before subtitle operations

* sandbox: handle synchronous spawn errors safely in desktop sandbox

* Voice-first streaming, non-blocking follow-ups, quiet routines and chat UX

Why: questions blocked the chat, follow-up messages had no acknowledgement, message actions sat beside the bubble, and voice turn-taking added latency.

What: waiting runs accept steering messages and the bot marks each claimed follow-up as seen; unclaimed follow-ups show as queued. Reaction, copy and reply controls sit under each message. Streaming voice (VAD endpointing, barge-in that stops the run, clause-based TTS), an optimistic input queue, procedural earcons and delta-based quiet routines were added. Onboarding can be skipped straight to the main conversation. Dual-surface layout and project pickers are included. Status colors use semantic tokens.

Tested: unit suites for core, adapters, api and web pass, typecheck and Biome are clean. Browser end-to-end tests were not run locally.

* Update translation catalogs

Re-extracted catalogs for the new and moved strings; the catalog sync test passes.

* Add project plan and test readiness notes

Notes from the milestone work: feature inventory, test infrastructure and test readiness. No code changes.

* fix(ci): align scale guards, space deletion, computer toggle, and mobile expo deps

* fix(ci): restore space deletion guard, browser toggle, and run-steps contracts

* fix(ci): restore model resolution errors, group stage, and onboarding flow

* fix(ci): restore in-app-browser close button, disable stop button while sending, and sync lingui catalogs

* fix(ci): address web e2e failures for bot-crud, choice cards, message quote, and voice onboarding

* test(web): wait for avatar and rig visibility in oo-avatar motion spec

* test(web): wait for persisted message IDs in message-hover-actions pagination spec

* Onboarding: always drop the local voice when the voice step is skipped

Skipping waited only for the latest voice load and disconnected only
when the client saw the connect succeed. An earlier or client-failed
connect could still persist the local credential, so a user who
skipped voice was left with one. Wait for every load and always
disconnect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy1md9QE1ayJbDCwJ4vAQs

* Shell: hide the optimistic copy once the transcript shows the send

A message sent while the bot works streamed into the transcript before
its send request resolved, so the same text showed twice until the
optimistic bubble was removed. Drop an optimistic message as soon as a
persisted user message with the same text appears.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy1md9QE1ayJbDCwJ4vAQs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 14:08:31 +02:00

198 lines
7.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { raceSignal } from "./pi-runtime.js";
describe("Adversarial Stress Test: raceSignal and Tool Cancellation Races", () => {
it("Race 1: in-flight tool aborting right as executeTool resolves in the same microtask", async () => {
// Subcase 1A: synchronous abort immediately following resolve()
// Event loop detail: Abort event fires synchronously, preempting the queued promise.then() microtask.
{
const controller = new AbortController();
let resolveTool!: (val: string) => void;
const toolPromise = new Promise<string>((res) => {
resolveTool = res;
});
const raced = raceSignal(toolPromise, controller.signal);
resolveTool("success");
controller.abort(new Error("interrupted"));
await expect(raced).rejects.toThrow("interrupted");
}
// Subcase 1B: microtask drained after resolve() before abort()
// Once promise.then() microtask executes, resolve() settles the raced promise and removes the abort listener.
{
const controller = new AbortController();
let resolveTool!: (val: string) => void;
const toolPromise = new Promise<string>((res) => {
resolveTool = res;
});
const raced = raceSignal(toolPromise, controller.signal);
resolveTool("success");
await Promise.resolve(); // drain microtask
controller.abort(new Error("too late"));
await expect(raced).resolves.toBe("success");
}
// Subcase 1C: tool promise already resolved before raceSignal called
{
const controller = new AbortController();
const alreadyResolved = Promise.resolve("immediate");
const raced = raceSignal(alreadyResolved, controller.signal);
await Promise.resolve();
controller.abort(new Error("aborted"));
await expect(raced).resolves.toBe("immediate");
}
});
it("Race 2: listener cleanup guarantees no memory leak after resolve or abort", async () => {
const controller = new AbortController();
// Run 100 resolved calls
for (let i = 0; i < 100; i++) {
await raceSignal(Promise.resolve(`res-${i}`), controller.signal);
}
// Run 100 rejected calls (non-abort)
for (let i = 0; i < 100; i++) {
await expect(
raceSignal(Promise.reject(new Error(`err-${i}`)), controller.signal),
).rejects.toThrow(`err-${i}`);
}
// Now abort
controller.abort(new Error("final abort"));
// Run 50 calls on already-aborted signal
for (let i = 0; i < 50; i++) {
await expect(raceSignal(Promise.resolve("late"), controller.signal)).rejects.toThrow(
"final abort",
);
}
});
it("Race 3: rapid back-to-back aborts while multiple tools are queued concurrently", async () => {
const NUM_TOOLS = 25;
const controllers = Array.from({ length: 5 }, () => new AbortController());
const resolvers: Array<(val: string) => void> = [];
const promises = Array.from({ length: NUM_TOOLS }, (_, index) => {
const controller = controllers[index % controllers.length]!;
const pending = new Promise<string>((resolve) => {
resolvers.push(resolve);
});
return raceSignal(pending, controller.signal);
});
// Rapidly abort controllers while tools are pending
controllers.forEach((c, idx) => {
c.abort(new Error(`Aborted batch ${idx}`));
});
// Settle all pending promises afterwards (simulating slow tools finishing late)
resolvers.forEach((res, idx) => {
res(`tool-${idx}-resolved`);
});
const results = await Promise.allSettled(promises);
expect(results).toHaveLength(NUM_TOOLS);
results.forEach((r, idx) => {
expect(r.status).toBe("rejected");
if (r.status === "rejected") {
const batch = idx % controllers.length;
expect((r as PromiseRejectedResult).reason.message).toBe(`Aborted batch ${batch}`);
}
});
});
it("Race 4: long-running tool cancellation under signal timeout", async () => {
const timeoutSignal = AbortSignal.timeout(25);
const longRunningTool = new Promise<string>((resolve) => {
setTimeout(() => resolve("tool finished after 200ms"), 200);
});
const start = Date.now();
await expect(raceSignal(longRunningTool, timeoutSignal)).rejects.toThrow();
const elapsed = Date.now() - start;
// Must have rejected around 25ms, well before the 200ms tool completion
expect(elapsed).toBeLessThan(100);
});
it("Race 5: abort with non-Error string reason propagates exact reason", async () => {
const controller = new AbortController();
const pending = new Promise<string>(() => {});
const raced = raceSignal(pending, controller.signal);
controller.abort("user_barge_in_detected");
await expect(raced).rejects.toBe("user_barge_in_detected");
});
it("Race 6: high-concurrency fuzz stress test (100 parallel randomized tool races)", async () => {
const randomDelays = Array.from({ length: 100 }, () => Math.floor(Math.random() * 20));
const results = await Promise.all(
randomDelays.map(async (toolDelay, idx) => {
const abortDelay = Math.floor(Math.random() * 20);
const controller = new AbortController();
const toolPromise = new Promise<string>((resolve) => {
setTimeout(() => resolve(`tool-${idx}-ok`), toolDelay);
});
setTimeout(() => controller.abort(new Error(`abort-${idx}`)), abortDelay);
try {
const res = await raceSignal(toolPromise, controller.signal);
return { status: "fulfilled" as const, res, toolDelay, abortDelay };
} catch (err) {
return {
status: "rejected" as const,
err: (err as Error).message,
toolDelay,
abortDelay,
};
}
}),
);
expect(results).toHaveLength(100);
for (const r of results) {
if (r.status === "fulfilled") {
expect(r.res).toContain("-ok");
// Tool resolved before abort fired (within timing margin)
expect(r.toolDelay).toBeLessThanOrEqual(r.abortDelay + 10);
} else {
expect(r.err).toContain("abort-");
}
}
});
it("Race 7: underlying tool promise runs uncancelled in background because executeTool has no signal parameter", async () => {
let underlyingToolFinished = false;
const controller = new AbortController();
const longRunningTool = new Promise<string>((resolve) => {
setTimeout(() => {
underlyingToolFinished = true;
resolve("done");
}, 50);
});
const raced = raceSignal(longRunningTool, controller.signal);
controller.abort(new Error("user barge-in"));
// Raced promise rejects immediately
await expect(raced).rejects.toThrow("user barge-in");
expect(underlyingToolFinished).toBe(false);
// Wait for the underlying tool timer to elapse
await new Promise((r) => setTimeout(r, 60));
// Notice: underlying task still completed in background because raceSignal does not / cannot abort the executor itself
expect(underlyingToolFinished).toBe(true);
});
});