From 467d9b0dc4ab28302eadc77120298184a9226083 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:33:07 +0200 Subject: [PATCH] Keep main green: racing E2E tests, the git-clone push, and runs that never fail (#224) * Stop two web E2E tests from racing on main Replies read themselves aloud by default since the voice update, so in the group chat test the last bot's automatic read-aloud could beat the manual Speak click. The test turns automatic reading off for its two bots. Opening the browser panel on a running computer now only fetches the screen, as it did before the panel change, instead of also reloading the whole thread on every computer state change. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1 * Wait for the speaking switch to settle in the voice test The app turns the local voice on by itself on first start and keeps the switch disabled meanwhile. The helper read "off" during that window and clicked a disabled switch; it now retries until the switch is on, clicking only when it is enabled and off. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1 * Fix main after the git-clone push: lint, i18n and abort tests - Lint: formatting in two files and no assignment inside an expression in the steering poller. - i18n: German for the new "Resume" and "Stopped" labels on web, and de/ru/zh on mobile; catalogs re-extracted. - Tests follow the intended change that a caller abort ends a turn as "stopped" instead of failing it as a provider error. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1 * Record runtime failures again instead of leaving the run running A runtime error closes the stream through withRuntimeCleanup, which aborts the run's own controller. The catch treated that as an outside stop and returned without finalizing, so a failed run stayed running forever and its delegated task never reported back. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1 --------- Co-authored-by: Claude --- apps/mobile/lib/locales/de.ts | 2 + apps/mobile/lib/locales/ru.ts | 2 + apps/mobile/lib/locales/zh.ts | 2 + apps/web/e2e/group-chats.spec.ts | 4 ++ apps/web/e2e/voice.spec.ts | 18 +++---- .../src/components/browser/InAppBrowser.tsx | 3 ++ .../components/browser/OoComputerBrowser.tsx | 9 ++-- apps/web/src/locales/de/messages.po | 10 ++++ apps/web/src/locales/en/messages.po | 10 ++++ apps/web/src/locales/es/messages.po | 10 ++++ apps/web/src/locales/hi/messages.po | 10 ++++ apps/web/src/locales/ko/messages.po | 10 ++++ apps/web/src/locales/pt-BR/messages.po | 10 ++++ apps/web/src/locales/ru/messages.po | 10 ++++ apps/web/src/locales/tr/messages.po | 10 ++++ apps/web/src/locales/zh-CN/messages.po | 10 ++++ apps/web/src/pages/Shell.tsx | 4 ++ .../supervisor/src/github-broker.test.ts | 6 ++- packages/adapters/src/executor.ts | 13 +++-- .../src/pi-runtime-tool-dispatch.test.ts | 49 ++++++++++--------- packages/adapters/src/pi-runtime.ts | 3 +- .../testkit/src/computer-recording.test.ts | 5 +- packages/testkit/src/pi-offline.test.ts | 12 ++--- 23 files changed, 166 insertions(+), 56 deletions(-) diff --git a/apps/mobile/lib/locales/de.ts b/apps/mobile/lib/locales/de.ts index ac8dff8..aea56e1 100644 --- a/apps/mobile/lib/locales/de.ts +++ b/apps/mobile/lib/locales/de.ts @@ -261,6 +261,8 @@ export const DE_MESSAGES: Record = { Eyeshadow: "Lidschatten", Failed: "Fehlgeschlagen", "Failed to refresh": "Aktualisieren fehlgeschlagen", + Resume: "Fortsetzen", + Stopped: "Gestoppt", "Failed to remove message": "Nachricht konnte nicht entfernt werden", "Failed to send message": "Nachricht konnte nicht gesendet werden", "Failed to stop work": "Arbeit konnte nicht gestoppt werden", diff --git a/apps/mobile/lib/locales/ru.ts b/apps/mobile/lib/locales/ru.ts index 70f4e44..93b41f6 100644 --- a/apps/mobile/lib/locales/ru.ts +++ b/apps/mobile/lib/locales/ru.ts @@ -274,6 +274,8 @@ export const RU_MESSAGES: Record = { "Executor token": "Токен Executor", Failed: "Ошибка", "Failed to refresh": "Не удалось обновить", + Resume: "Продолжить", + Stopped: "Остановлено", "Failed to remove message": "Не удалось удалить сообщение", "Failed to send message": "Не удалось отправить сообщение", "Failed to stop work": "Не удалось остановить работу", diff --git a/apps/mobile/lib/locales/zh.ts b/apps/mobile/lib/locales/zh.ts index 96e6b95..01f96f7 100644 --- a/apps/mobile/lib/locales/zh.ts +++ b/apps/mobile/lib/locales/zh.ts @@ -259,6 +259,8 @@ export const ZH_MESSAGES: Record = { "Executor token": "Executor 令牌", Failed: "失败", "Failed to refresh": "刷新失败", + Resume: "继续", + Stopped: "已停止", "Failed to remove message": "删除消息失败", "Failed to send message": "发送消息失败", "Failed to stop work": "停止工作失败", diff --git a/apps/web/e2e/group-chats.spec.ts b/apps/web/e2e/group-chats.spec.ts index a3eceb7..30f23bf 100644 --- a/apps/web/e2e/group-chats.spec.ts +++ b/apps/web/e2e/group-chats.spec.ts @@ -63,6 +63,10 @@ test("create group from + and see two bots in one transcript", async ({ page }, const researcherId = await createBot(page, "Researcher"); const writerId = await createBot(page, "Research Writer"); + // Replies read themselves aloud by default; keep that off so only the Speak click below talks. + for (const botId of [researcherId, writerId]) { + await rpc(page, "bots/update", { botId, autoSpeak: false }); + } await openNewGroup(page); await page.locator("label:has-text('Name') input").fill("Draft team"); diff --git a/apps/web/e2e/voice.spec.ts b/apps/web/e2e/voice.spec.ts index b54be0f..0458bf6 100644 --- a/apps/web/e2e/voice.spec.ts +++ b/apps/web/e2e/voice.spec.ts @@ -7,17 +7,15 @@ async function turnOnSpeaking(page: Page) { await expect(voice).toBeVisible(); await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0); await expect(voice.getByLabel("API key", { exact: true })).toHaveCount(0); - // The app turns the local voice on by itself the first time it starts. + // The app turns the local voice on by itself the first time it starts, and the switch stays + // disabled while that runs; only click once it settled off. const speaking = voice.getByRole("switch", { name: "Speaking on or off" }); - // Clicking before the automatic start has settled would turn the voice off again. - const startedByItself = await expect(speaking) - .toHaveAttribute("aria-checked", "true", { timeout: 5_000 }) - .then( - () => true, - () => false, - ); - if (!startedByItself) await speaking.click(); - await expect(speaking).toHaveAttribute("aria-checked", "true"); + await expect(async () => { + if ((await speaking.getAttribute("aria-checked")) !== "true" && (await speaking.isEnabled())) { + await speaking.click(); + } + await expect(speaking).toHaveAttribute("aria-checked", "true", { timeout: 3_000 }); + }).toPass({ timeout: 30_000 }); await expect(voice.getByTestId("voice-carousel")).toBeVisible(); return voice; } diff --git a/apps/web/src/components/browser/InAppBrowser.tsx b/apps/web/src/components/browser/InAppBrowser.tsx index 07fcd86..4d744c9 100644 --- a/apps/web/src/components/browser/InAppBrowser.tsx +++ b/apps/web/src/components/browser/InAppBrowser.tsx @@ -18,6 +18,7 @@ export function InAppBrowser({ sandboxProvider, booting, onRefreshScreen, + onConnectScreen, onOpenComputer, hideCloseButton = false, }: { @@ -29,6 +30,7 @@ export function InAppBrowser({ sandboxProvider: string | undefined; booting: boolean; onRefreshScreen: () => Promise; + onConnectScreen: () => Promise; onOpenComputer?: () => void; hideCloseButton?: boolean; }) { @@ -92,6 +94,7 @@ export function InAppBrowser({ sandboxProvider={sandboxProvider} booting={booting} onRefreshScreen={onRefreshScreen} + onConnectScreen={onConnectScreen} onOpenComputer={onOpenComputer} /> ) : ( diff --git a/apps/web/src/components/browser/OoComputerBrowser.tsx b/apps/web/src/components/browser/OoComputerBrowser.tsx index 4b4155b..7b21409 100644 --- a/apps/web/src/components/browser/OoComputerBrowser.tsx +++ b/apps/web/src/components/browser/OoComputerBrowser.tsx @@ -35,6 +35,7 @@ export function OoComputerBrowser({ sandboxProvider, booting, onRefreshScreen, + onConnectScreen, onOpenComputer, }: { active: Bot; @@ -44,6 +45,8 @@ export function OoComputerBrowser({ sandboxProvider: string | undefined; booting: boolean; onRefreshScreen: () => Promise; + /** Fetches the screen of an already running computer, without touching the thread. */ + onConnectScreen: () => Promise; /** Opens the full computer window, where the user can take control or teach. */ onOpenComputer?: () => void; }) { @@ -88,12 +91,12 @@ export function OoComputerBrowser({ }; }, [active.id, computer?.state, refreshChrome]); - const refreshScreen = useRef(onRefreshScreen); - refreshScreen.current = onRefreshScreen; + const connectScreen = useRef(onConnectScreen); + connectScreen.current = onConnectScreen; // Showing the browser never starts a stopped computer; it only reconnects a running screen. useEffect(() => { if (computer?.state !== "running" || embeddedScreenUrl || computer?.kind === "desktop") return; - void refreshScreen.current().catch(() => undefined); + void connectScreen.current().catch(() => undefined); }, [active.id, computer?.kind, computer?.state, embeddedScreenUrl]); const runNav = useCallback( diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index e3adf50..2a93096 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -2354,6 +2354,7 @@ msgstr "Verbindung wird getrennt…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Ausblenden" @@ -4720,6 +4721,11 @@ msgstr "Stellt den zuletzt gespeicherten Arbeitsbereich wieder her. Nicht gespei msgid "Restoring your workspace" msgstr "Dein Arbeitsbereich wird wiederhergestellt" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "Fortsetzen" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "Wiederholen" @@ -5362,6 +5368,10 @@ msgstr "Anlernen beenden" msgid "Stop timer" msgstr "Timer stoppen" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "Gestoppt" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index aacf8d4..6c28cad 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -2354,6 +2354,7 @@ msgstr "Disconnecting…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Dismiss" @@ -4720,6 +4721,11 @@ msgstr "Restore the last saved workspace. Unsaved work on the computer is lost." msgid "Restoring your workspace" msgstr "Restoring your workspace" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "Resume" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "Retry" @@ -5362,6 +5368,10 @@ msgstr "Stop teaching" msgid "Stop timer" msgstr "Stop timer" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "Stopped" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 9393905..502caa2 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -2354,6 +2354,7 @@ msgstr "Desconectando…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Descartar" @@ -4720,6 +4721,11 @@ msgstr "Restaura el último espacio de trabajo guardado. Se pierde el trabajo si msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Detener enseñanza" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index eaddbc2..effdf3c 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -2354,6 +2354,7 @@ msgstr "डिस्कनेक्ट हो रहा है…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "अंतिम सहेजे गए वर्कस्पेस क msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "सिखाना रोकें" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index 99be890..1b29905 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -2354,6 +2354,7 @@ msgstr "연결 해제 중…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "닫기" @@ -4720,6 +4721,11 @@ msgstr "마지막으로 저장된 작업 공간을 복원합니다. 컴퓨터에 msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "가르치기 종료" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index e8b0621..1227b2c 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -2354,6 +2354,7 @@ msgstr "Desconectando…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "Restaure o último espaço de trabalho salvo. O trabalho não salvo no c msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Parar de ensinar" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 9a356a8..ec38a25 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -2354,6 +2354,7 @@ msgstr "Отключение…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Закрыть" @@ -4720,6 +4721,11 @@ msgstr "Восстановите последнее сохраненное ра msgid "Restoring your workspace" msgstr "Восстановление вашего рабочего пространства" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Остановить обучение" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index cb18ba9..0f7a212 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -2354,6 +2354,7 @@ msgstr "Bağlantı kesiliyor…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "" msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Öğretmeyi durdur" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index 2657a08..8294f7f 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -2354,6 +2354,7 @@ msgstr "正在断开…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "还原上次保存的工作区。电脑上未保存的工作会丢失。 msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "停止示范" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index db4cb16..44fcd23 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -4687,6 +4687,10 @@ export function ShellPage() { await refreshComputerScreen(target.id); await refreshThread(target.id).catch(() => undefined); }} + onConnectScreen={async () => { + const target = active ?? coordinatorBot; + if (target) await refreshComputerScreen(target.id); + }} onOpenComputer={() => void openComputer()} /> diff --git a/infra/sandboxes/supervisor/src/github-broker.test.ts b/infra/sandboxes/supervisor/src/github-broker.test.ts index 3d38be3..57e622b 100644 --- a/infra/sandboxes/supervisor/src/github-broker.test.ts +++ b/infra/sandboxes/supervisor/src/github-broker.test.ts @@ -166,7 +166,11 @@ describe("github broker remotes", () => { assertRemoteAllowed(linked, ["git", "fetch", "origin"], repo), ).resolves.toBeUndefined(); await expect( - assertRemoteAllowed(linked, ["git", "ls-remote", "https://github.com/cynar/umbra.git"], repo), + assertRemoteAllowed( + linked, + ["git", "ls-remote", "https://github.com/cynar/umbra.git"], + repo, + ), ).resolves.toBeUndefined(); } finally { rmSync(repo, { recursive: true, force: true }); diff --git a/packages/adapters/src/executor.ts b/packages/adapters/src/executor.ts index 9a92637..c9902a4 100644 --- a/packages/adapters/src/executor.ts +++ b/packages/adapters/src/executor.ts @@ -1233,7 +1233,9 @@ export function createRunExecutor(deps: ExecutorDeps) { hasModelOverride && bot.modelProvider ? findModelCredential(deps.prisma, run, bot.modelProvider, bot.modelId) : Promise.resolve(null), - bot.modelTier ? findSpaceModelTier(deps.prisma, run, bot.modelTier) : Promise.resolve(null), + bot.modelTier + ? findSpaceModelTier(deps.prisma, run, bot.modelTier) + : Promise.resolve(null), ]); const brokerWorkspacePath = projectWorkspacePath ?? @@ -5296,12 +5298,9 @@ export function createRunExecutor(deps: ExecutorDeps) { getLogger().error("history.compact enqueue failed", error); } } catch (error) { - if ( - approvalPausePending || - !leaseValid || - deps.shutdownSignal?.aborted || - runAbortController?.signal.aborted - ) { + // A runtime error also aborts runAbortController through withRuntimeCleanup, + // so only an abort from outside the run may skip recording the failure. + if (approvalPausePending || !leaseValid || deps.shutdownSignal?.aborted) { return; } if (run.trigger === "routine") { diff --git a/packages/adapters/src/pi-runtime-tool-dispatch.test.ts b/packages/adapters/src/pi-runtime-tool-dispatch.test.ts index 3ea19a5..644cbac 100644 --- a/packages/adapters/src/pi-runtime-tool-dispatch.test.ts +++ b/packages/adapters/src/pi-runtime-tool-dispatch.test.ts @@ -1167,30 +1167,31 @@ describe("Pi connector tool dispatch", () => { return { ok: true }; }); - await expect(async () => { - for await (const _event of runtime.run( - { - botId: "b", - threadId: "t", - runId: "fuse-resume", - prompt: "keep going", - instructions: "Use shell.", - history: [], - tools: [shellTool], - model: { provider: "test", id: "dispatch-test-model" }, - executeTool, - }, - { - operationId: "2d", - traceId: "2d", - spaceId: "w", - userId: "u", - signal: abort.signal, - }, - )) { - // Exhaust until abort fails the turn. - } - }).rejects.toThrow(); + // A caller abort ends the turn as stopped rather than as a provider error. + const stopped: unknown[] = []; + for await (const event of runtime.run( + { + botId: "b", + threadId: "t", + runId: "fuse-resume", + prompt: "keep going", + instructions: "Use shell.", + history: [], + tools: [shellTool], + model: { provider: "test", id: "dispatch-test-model" }, + executeTool, + }, + { + operationId: "2d", + traceId: "2d", + spaceId: "w", + userId: "u", + signal: abort.signal, + }, + )) { + stopped.push(event); + } + expect(stopped).toContainEqual({ type: "done", text: "stopped" }); executeTool.mockImplementation(async () => ({ ok: true })); const events: unknown[] = []; diff --git a/packages/adapters/src/pi-runtime.ts b/packages/adapters/src/pi-runtime.ts index f7834fe..be967d0 100644 --- a/packages/adapters/src/pi-runtime.ts +++ b/packages/adapters/src/pi-runtime.ts @@ -558,7 +558,8 @@ function ensureHostSteeringPoller(host: ToolHost): void { // Nested subagents run inside the parent's active `run_subagent` tool call, whose // root host poller already claims steering and cascades abort to nested tools. if (host.depth > 0 || !host.request.claimSteering || host.signal.aborted) return; - const poller = (host.steeringPoller ??= { inFlight: false }); + host.steeringPoller ??= { inFlight: false }; + const poller = host.steeringPoller; if (poller.timer) return; poller.timer = setInterval(async () => { if (host.signal.aborted || host.activeToolControllers.size === 0) { diff --git a/packages/testkit/src/computer-recording.test.ts b/packages/testkit/src/computer-recording.test.ts index 7ce6768..ad08c43 100644 --- a/packages/testkit/src/computer-recording.test.ts +++ b/packages/testkit/src/computer-recording.test.ts @@ -209,9 +209,8 @@ describe("sanitized real-model computer recordings", () => { fixture.computer, interruptedContext, ); - await expect( - executeContactsJourney(emulator.model, recorder, interruptedContext), - ).rejects.toThrow(/aborted/i); + // A caller abort ends the turn as stopped, not as a provider error. + await executeContactsJourney(emulator.model, recorder, interruptedContext); emulator.assertComplete(); expect(held).toBe(true); expect(controller.signal.aborted).toBe(true); diff --git a/packages/testkit/src/pi-offline.test.ts b/packages/testkit/src/pi-offline.test.ts index 1bf1f82..49d1aa1 100644 --- a/packages/testkit/src/pi-offline.test.ts +++ b/packages/testkit/src/pi-offline.test.ts @@ -236,17 +236,15 @@ describe("real Pi against an offline model HTTP endpoint", () => { new PiAgentRuntime().run(runRequest(server.model), { signal: controller.signal }), events, ); - // Attach the rejection handler before aborting to avoid an unhandled rejection. - const outcome = work.then( - () => undefined, - (error: unknown) => error, - ); await opened.promise; controller.abort(); - expect(await outcome).toBeInstanceOf(Error); + // A caller abort ends the turn as stopped, not as a provider error. + await work; await closed.promise; server.assertComplete(); - expect(events.some((event) => event.type === "done")).toBe(false); + expect(events.filter((event) => event.type === "done")).toEqual([ + { type: "done", text: "stopped" }, + ]); }); it("keeps two model connections and their responses isolated", async () => {