@vantran-se/openchamber-web 1.24.4 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
- package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
- package/dist/assets/FilesView-DjksmZRo.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
- package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
- package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
- package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
- package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
- package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
- package/dist/assets/PlanView-_eZvb-G5.js +5 -0
- package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
- package/dist/assets/SettingsView-BRfr46hb.js +29 -0
- package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
- package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
- package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
- package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
- package/dist/assets/de-CUO1-KqF.js +10 -0
- package/dist/assets/es-BlbC6sGJ.js +16 -0
- package/dist/assets/fr-D2nwe_0W.js +14 -0
- package/dist/assets/index-BQOoYIOu.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
- package/dist/assets/ja-CA7vnDsZ.js +16 -0
- package/dist/assets/ko-Chy6a9l-.js +16 -0
- package/dist/assets/main-B9JFK3vF.js +896 -0
- package/dist/assets/main-Bhiqnrhd.js +2 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
- package/dist/assets/miniChat-B0UOL5zA.js +2 -0
- package/dist/assets/mobile-BJppFIEo.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
- package/dist/assets/pl-CTR5A_1P.js +16 -0
- package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
- package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
- package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
- package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
- package/dist/assets/tr-Bh5vK6cW.js +7 -0
- package/dist/assets/uk-CLDkZFy_.js +16 -0
- package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
- package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
- package/dist/assets/zh-CN-DODgcmYj.js +16 -0
- package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -1,607 +1,359 @@
|
|
|
1
|
-
import { afterEach,
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { createSessionGoalRuntime } from './runtime.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The goal record — status, turns, token accounting, audit notes — lives in
|
|
7
|
+
* OpenChamber's own session metadata store, because OpenCode 2.x accepts
|
|
8
|
+
* session metadata only when a session is created. `readSessionMetadata` and
|
|
9
|
+
* `persistSessionGoal` are the seams.
|
|
10
|
+
*
|
|
11
|
+
* What is pinned here is the wiring: the runtime stays inert and costs nothing
|
|
12
|
+
* when no store is given, it reads the goal from the store rather than from the
|
|
13
|
+
* OpenCode record, and a goal that starts or resumes arms the loop through
|
|
14
|
+
* `notifyGoalChanged` instead of through an OpenCode `session.updated` event
|
|
15
|
+
* (which no longer carries our metadata).
|
|
16
|
+
*
|
|
17
|
+
* The previous suite drove the whole audit loop against a fake v1 OpenCode; it
|
|
18
|
+
* is gone rather than rewritten because every route and shape it asserted on
|
|
19
|
+
* belongs to v1. Subagents come from `GET /api/session?parentID=` now.
|
|
20
|
+
*/
|
|
21
|
+
|
|
5
22
|
const SESSION_ID = 'ses_parent';
|
|
6
|
-
const
|
|
7
|
-
const DIRECTORY = '/workspace';
|
|
23
|
+
const runtimes = [];
|
|
8
24
|
|
|
9
|
-
const
|
|
25
|
+
const activeGoal = (extra = {}) => ({
|
|
10
26
|
id: 'goal_1',
|
|
11
27
|
objective: 'Finish the task',
|
|
12
28
|
status: 'active',
|
|
13
|
-
turnsUsed:
|
|
29
|
+
turnsUsed: 0,
|
|
14
30
|
createdAt: 1,
|
|
15
31
|
updatedAt: 1,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const session = {
|
|
19
|
-
id: SESSION_ID,
|
|
20
|
-
directory: DIRECTORY,
|
|
21
|
-
metadata: { openchamber: { goal } },
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
25
|
-
status,
|
|
26
|
-
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
...extra,
|
|
27
33
|
});
|
|
28
34
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const makeRuntime = (overrides = {}) => {
|
|
36
|
+
const buildOpenCodeUrl = vi.fn((fetchPath) => `http://opencode.test${fetchPath}`);
|
|
37
|
+
const getSmallModelService = vi.fn(async () => {
|
|
38
|
+
throw new Error('the small model must not be consulted in this test');
|
|
39
|
+
});
|
|
40
|
+
const emitGoalNotification = vi.fn();
|
|
34
41
|
const runtime = createSessionGoalRuntime({
|
|
35
|
-
buildOpenCodeUrl
|
|
42
|
+
buildOpenCodeUrl,
|
|
36
43
|
getOpenCodeAuthHeaders: () => ({}),
|
|
37
44
|
getSmallModelService,
|
|
45
|
+
emitGoalNotification,
|
|
38
46
|
isEnabled: () => true,
|
|
39
|
-
idleQuietMs:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
type: 'session.status',
|
|
43
|
-
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
47
|
+
idleQuietMs: 1,
|
|
48
|
+
kickoffQuietMs: 1,
|
|
49
|
+
...overrides,
|
|
44
50
|
});
|
|
45
|
-
|
|
46
|
-
return { runtime, getSmallModelService };
|
|
51
|
+
runtimes.push(runtime);
|
|
52
|
+
return { runtime, buildOpenCodeUrl, getSmallModelService, emitGoalNotification };
|
|
47
53
|
};
|
|
48
54
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
sessionID: SESSION_ID,
|
|
53
|
-
role: 'assistant',
|
|
54
|
-
providerID: 'provider',
|
|
55
|
-
modelID: 'model',
|
|
56
|
-
time: { created: 2, completed: 2 },
|
|
57
|
-
tokens: { input: 1, output: 1, cache: { read: 0 } },
|
|
58
|
-
...infoOverrides,
|
|
59
|
-
},
|
|
60
|
-
parts: [{ type: 'text', text: 'The agent made progress.' }],
|
|
55
|
+
const wired = (metadata = {}) => ({
|
|
56
|
+
readSessionMetadata: vi.fn(async () => metadata),
|
|
57
|
+
persistSessionGoal: vi.fn(async () => undefined),
|
|
61
58
|
});
|
|
62
59
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...session,
|
|
68
|
-
metadata: { openchamber: { goal: { ...goal, ...goalOverrides } } },
|
|
69
|
-
};
|
|
70
|
-
const service = {
|
|
71
|
-
generateSmallModelText: vi.fn(async () => ({
|
|
72
|
-
text: '{"verdict":"continue","note":"More work remains"}',
|
|
73
|
-
providerID: 'provider',
|
|
74
|
-
modelID: 'model',
|
|
75
|
-
})),
|
|
76
|
-
};
|
|
77
|
-
const fetchImpl = vi.fn(async (input, init = {}) => {
|
|
78
|
-
const pathname = requestPath(input);
|
|
79
|
-
requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
|
|
80
|
-
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') {
|
|
81
|
-
activeSession.metadata = JSON.parse(init.body).metadata;
|
|
82
|
-
return jsonResponse(activeSession);
|
|
83
|
-
}
|
|
84
|
-
if (pathname === `/session/${SESSION_ID}`) return jsonResponse(activeSession);
|
|
85
|
-
if (pathname === '/session/status') return jsonResponse({});
|
|
86
|
-
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
|
|
87
|
-
if (pathname === `/session/${SESSION_ID}/message`) {
|
|
88
|
-
const nextMessages = messageFactory ? messageFactory(messageFetchCount) : messages;
|
|
89
|
-
messageFetchCount += 1;
|
|
90
|
-
return jsonResponse(nextMessages);
|
|
91
|
-
}
|
|
92
|
-
if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
|
|
93
|
-
throw new Error(`Unexpected request: ${pathname}`);
|
|
94
|
-
});
|
|
95
|
-
vi.stubGlobal('fetch', fetchImpl);
|
|
96
|
-
const runtime = createSessionGoalRuntime({
|
|
97
|
-
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
|
98
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
99
|
-
getSmallModelService: async () => service,
|
|
100
|
-
isEnabled: () => true,
|
|
101
|
-
idleQuietMs: 10,
|
|
102
|
-
maxAutoTurns,
|
|
103
|
-
});
|
|
104
|
-
return { runtime, requests, service, activeSession };
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const runIdleTick = async (runtime) => {
|
|
108
|
-
runtime.processPayload({
|
|
109
|
-
type: 'session.status',
|
|
110
|
-
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
111
|
-
});
|
|
112
|
-
await vi.runOnlyPendingTimersAsync();
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const lastPatchedGoal = (requests) => {
|
|
116
|
-
const patches = requests.filter((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
|
117
|
-
expect(patches.length).toBeGreaterThan(0);
|
|
118
|
-
return JSON.parse(patches.at(-1).body).metadata.openchamber.goal;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
describe('session goal live activity gate', () => {
|
|
122
|
-
beforeEach(() => {
|
|
123
|
-
vi.useFakeTimers();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
afterEach(() => {
|
|
127
|
-
vi.unstubAllGlobals();
|
|
128
|
-
vi.useRealTimers();
|
|
129
|
-
});
|
|
60
|
+
const idle = (sessionID = SESSION_ID) => ({
|
|
61
|
+
type: 'session.status',
|
|
62
|
+
properties: { sessionID, status: { type: 'idle' } },
|
|
63
|
+
});
|
|
130
64
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
paths.push(pathname);
|
|
136
|
-
if (pathname === `/session/${SESSION_ID}`) return jsonResponse(session);
|
|
137
|
-
if (pathname === '/session/status') return jsonResponse({ [SESSION_ID]: { type: 'busy' } });
|
|
138
|
-
throw new Error(`Unexpected request: ${pathname}`);
|
|
139
|
-
}));
|
|
140
|
-
|
|
141
|
-
expect(paths).toEqual([`/session/${SESSION_ID}`, '/session/status']);
|
|
142
|
-
expect(getSmallModelService).not.toHaveBeenCalled();
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
while (runtimes.length > 0) runtimes.pop().stop?.();
|
|
67
|
+
vi.restoreAllMocks();
|
|
68
|
+
});
|
|
143
69
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A fake v2 OpenCode over the global fetch: flat message records (`type`,
|
|
72
|
+
* `content[]`, `model`, `finish`, `tokens`), `{ location, data }` envelopes,
|
|
73
|
+
* `/api/session/active` for busy state, and the three continuation calls.
|
|
74
|
+
*/
|
|
75
|
+
const v2OpenCode = ({ messages, active = {}, childPages = [[]], childrenStatus = 200 }) => {
|
|
76
|
+
const calls = [];
|
|
77
|
+
const json = (data, status = 200) => new Response(JSON.stringify({ location: { directory: '/repo' }, data }), {
|
|
78
|
+
status,
|
|
79
|
+
headers: { 'content-type': 'application/json' },
|
|
147
80
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
expect(getSmallModelService).not.toHaveBeenCalled();
|
|
166
|
-
|
|
167
|
-
await vi.advanceTimersByTimeAsync(60_000);
|
|
168
|
-
expect(paths).toHaveLength(3);
|
|
169
|
-
runtime.stop();
|
|
81
|
+
const fetchMock = vi.fn(async (input, init = {}) => {
|
|
82
|
+
const url = new URL(String(input));
|
|
83
|
+
calls.push({ path: url.pathname, query: Object.fromEntries(url.searchParams), method: init.method ?? 'GET', body: init.body ? JSON.parse(init.body) : null });
|
|
84
|
+
// Real shape: `{ data }` without a `location`, unlike directory-scoped routes.
|
|
85
|
+
if (url.pathname === '/api/session/active') return Response.json({ data: active });
|
|
86
|
+
if (url.pathname === '/api/session') {
|
|
87
|
+
// Children of the parent, cursor paged: the first page is selected by
|
|
88
|
+
// `parentID`, later ones by the cursor alone.
|
|
89
|
+
const index = url.searchParams.has('cursor') ? Number(url.searchParams.get('cursor').replace('page-', '')) : 0;
|
|
90
|
+
if (index === 0 && url.searchParams.get('parentID') !== SESSION_ID) return json({ data: [], cursor: {} });
|
|
91
|
+
const data = childPages[index] ?? [];
|
|
92
|
+
const next = index + 1 < childPages.length ? { next: `page-${index + 1}` } : {};
|
|
93
|
+
return json({ data, cursor: next }, childrenStatus);
|
|
94
|
+
}
|
|
95
|
+
if (url.pathname.endsWith('/message')) return json({ data: [...messages].reverse(), cursor: null });
|
|
96
|
+
if (url.pathname === `/api/session/${SESSION_ID}`) return json({ id: SESSION_ID, location: { directory: '/repo' } });
|
|
97
|
+
return json({});
|
|
170
98
|
});
|
|
99
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
100
|
+
return { calls };
|
|
101
|
+
};
|
|
171
102
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
103
|
+
const assistantRecord = (overrides = {}) => ({
|
|
104
|
+
id: 'msg_a1',
|
|
105
|
+
sessionID: SESSION_ID,
|
|
106
|
+
type: 'assistant',
|
|
107
|
+
agent: 'build',
|
|
108
|
+
model: { providerID: 'anthropic', id: 'claude-sonnet-5' },
|
|
109
|
+
content: [{ type: 'text', text: 'Done with step one.' }],
|
|
110
|
+
finish: 'stop',
|
|
111
|
+
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 20, write: 0 } },
|
|
112
|
+
time: { created: 10, completed: 20 },
|
|
113
|
+
...overrides,
|
|
114
|
+
});
|
|
184
115
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
'/session/status',
|
|
191
|
-
]);
|
|
192
|
-
runtime.stop();
|
|
193
|
-
});
|
|
116
|
+
const runTick = async (runtime) => {
|
|
117
|
+
await runtime.notifyGoalChanged(SESSION_ID, '/repo', { openchamber: { goal: activeGoal() } });
|
|
118
|
+
// idleQuietMs / kickoffQuietMs are 1 ms; the tick itself is async.
|
|
119
|
+
for (let i = 0; i < 20; i += 1) await new Promise((resolve) => setTimeout(resolve, 5));
|
|
120
|
+
};
|
|
194
121
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
|
|
204
|
-
if (pathname === `/session/${SESSION_ID}/message`) {
|
|
205
|
-
return jsonResponse([{
|
|
206
|
-
info: {
|
|
207
|
-
id: 'msg_assistant',
|
|
208
|
-
sessionID: SESSION_ID,
|
|
209
|
-
role: 'assistant',
|
|
210
|
-
providerID: 'provider',
|
|
211
|
-
modelID: 'model',
|
|
212
|
-
time: { completed: 2 },
|
|
213
|
-
tokens: { input: 1, output: 1, cache: { read: 0 } },
|
|
214
|
-
},
|
|
215
|
-
parts: [{ type: 'text', text: 'The task is verified complete.' }],
|
|
216
|
-
}]);
|
|
217
|
-
}
|
|
218
|
-
throw new Error(`Unexpected request: ${pathname}`);
|
|
122
|
+
describe('session goal tick on v2 messages', () => {
|
|
123
|
+
it('reads the flat v2 assistant record, audits it, and continues on the same model and agent', async () => {
|
|
124
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
125
|
+
const { calls } = v2OpenCode({
|
|
126
|
+
messages: [
|
|
127
|
+
{ id: 'msg_u1', sessionID: SESSION_ID, type: 'user', text: 'Finish the task', time: { created: 1 } },
|
|
128
|
+
assistantRecord(),
|
|
129
|
+
],
|
|
219
130
|
});
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
})),
|
|
226
|
-
};
|
|
227
|
-
vi.stubGlobal('fetch', fetchImpl);
|
|
228
|
-
const runtime = createSessionGoalRuntime({
|
|
229
|
-
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
|
230
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
231
|
-
getSmallModelService: async () => service,
|
|
232
|
-
isEnabled: () => true,
|
|
233
|
-
idleQuietMs: 10,
|
|
131
|
+
const seam = wired({ openchamber: { goal: activeGoal() } });
|
|
132
|
+
const generate = vi.fn(async () => ({ text: JSON.stringify({ verdict: 'continue', note: 'more to do' }), providerID: 'anthropic', modelID: 'claude-haiku-5' }));
|
|
133
|
+
const { runtime } = makeRuntime({
|
|
134
|
+
...seam,
|
|
135
|
+
getSmallModelService: async () => ({ generateSmallModelText: generate }),
|
|
234
136
|
});
|
|
235
137
|
|
|
236
|
-
runtime
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
expect(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
138
|
+
await runTick(runtime);
|
|
139
|
+
|
|
140
|
+
// The audit saw the assistant's text and ran within the session's provider.
|
|
141
|
+
expect(generate).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(generate.mock.calls[0][0]).toMatchObject({ preferredProviderID: 'anthropic', preferredModelID: 'claude-sonnet-5', restrictToPreferredProvider: true });
|
|
143
|
+
expect(generate.mock.calls[0][0].prompt).toContain('Done with step one.');
|
|
144
|
+
// Tokens were accounted from the v2 record: input + cache.read + output.
|
|
145
|
+
const written = seam.persistSessionGoal.mock.calls.at(-1)[2];
|
|
146
|
+
expect(written).toMatchObject({ turnsUsed: 1, tokensUsed: 170 });
|
|
147
|
+
// The continuation is one plain prompt: the session keeps its own model
|
|
148
|
+
// and agent, nothing is re-selected.
|
|
149
|
+
const continuation = calls.filter((call) => call.method === 'POST').map((call) => [call.path, call.body]);
|
|
150
|
+
expect(continuation).toEqual([
|
|
151
|
+
[`/api/session/${SESSION_ID}/prompt`, { text: expect.stringContaining('Finish the task') }],
|
|
152
|
+
]);
|
|
251
153
|
runtime.stop();
|
|
252
154
|
});
|
|
253
155
|
|
|
254
|
-
it('
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
|
|
264
|
-
if (pathname === `/session/${SESSION_ID}/message`) {
|
|
265
|
-
return jsonResponse([{
|
|
266
|
-
info: {
|
|
267
|
-
id: 'msg_assistant_len',
|
|
268
|
-
sessionID: SESSION_ID,
|
|
269
|
-
role: 'assistant',
|
|
270
|
-
providerID: 'provider',
|
|
271
|
-
modelID: 'model',
|
|
272
|
-
finish: 'length',
|
|
273
|
-
time: { completed: 2 },
|
|
274
|
-
tokens: { input: 100, output: 4096, reasoning: 4096, cache: { read: 0 } },
|
|
275
|
-
},
|
|
276
|
-
parts: [{ type: 'reasoning', text: 'Drafting extensive implementation...' }],
|
|
277
|
-
}]);
|
|
278
|
-
}
|
|
279
|
-
throw new Error(`Unexpected request: ${pathname}`);
|
|
280
|
-
});
|
|
281
|
-
const service = {
|
|
282
|
-
generateSmallModelText: vi.fn(),
|
|
283
|
-
};
|
|
284
|
-
vi.stubGlobal('fetch', fetchImpl);
|
|
285
|
-
const runtime = createSessionGoalRuntime({
|
|
286
|
-
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
|
287
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
288
|
-
getSmallModelService: async () => service,
|
|
289
|
-
isEnabled: () => true,
|
|
290
|
-
idleQuietMs: 10,
|
|
156
|
+
it('settles the goal as complete when the audit says so, without a continuation', async () => {
|
|
157
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
158
|
+
const { calls } = v2OpenCode({ messages: [assistantRecord()] });
|
|
159
|
+
const seam = wired({ openchamber: { goal: activeGoal() } });
|
|
160
|
+
const { runtime, emitGoalNotification } = makeRuntime({
|
|
161
|
+
...seam,
|
|
162
|
+
getSmallModelService: async () => ({
|
|
163
|
+
generateSmallModelText: async () => ({ text: JSON.stringify({ verdict: 'complete', note: 'all done' }) }),
|
|
164
|
+
}),
|
|
291
165
|
});
|
|
292
166
|
|
|
293
|
-
runtime
|
|
294
|
-
type: 'session.status',
|
|
295
|
-
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
296
|
-
});
|
|
297
|
-
await vi.runOnlyPendingTimersAsync();
|
|
298
|
-
|
|
299
|
-
// Audit must be skipped on length-truncated turn
|
|
300
|
-
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
301
|
-
|
|
302
|
-
// Goal accounting is persisted and turnsUsed incremented
|
|
303
|
-
const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
|
304
|
-
expect(patch).toBeDefined();
|
|
305
|
-
const writtenGoal = JSON.parse(patch.body).metadata.openchamber.goal;
|
|
306
|
-
expect(writtenGoal).toMatchObject({
|
|
307
|
-
status: 'active',
|
|
308
|
-
turnsUsed: 2,
|
|
309
|
-
});
|
|
167
|
+
await runTick(runtime);
|
|
310
168
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
expect(
|
|
314
|
-
const promptBody = JSON.parse(promptAsync.body);
|
|
315
|
-
expect(promptBody.parts[0].text).toContain('Continue working toward the active session goal.');
|
|
169
|
+
expect(seam.persistSessionGoal.mock.calls.at(-1)[2]).toMatchObject({ status: 'complete', note: 'all done' });
|
|
170
|
+
expect(calls.some((call) => call.method === 'POST')).toBe(false);
|
|
171
|
+
expect(emitGoalNotification).toHaveBeenCalledTimes(1);
|
|
316
172
|
runtime.stop();
|
|
317
173
|
});
|
|
318
174
|
|
|
319
|
-
it('
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (pathname === '/session/status') return jsonResponse({});
|
|
327
|
-
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
|
|
328
|
-
if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
|
|
329
|
-
if (pathname === `/session/${SESSION_ID}/message`) {
|
|
330
|
-
return jsonResponse([{
|
|
331
|
-
info: {
|
|
332
|
-
id: 'msg_assistant_len_err',
|
|
333
|
-
sessionID: SESSION_ID,
|
|
334
|
-
role: 'assistant',
|
|
335
|
-
providerID: 'provider',
|
|
336
|
-
modelID: 'model',
|
|
337
|
-
error: { name: 'MessageOutputLengthError', message: 'Maximum token limit reached' },
|
|
338
|
-
time: { completed: 2 },
|
|
339
|
-
tokens: { input: 100, output: 4096, reasoning: 4096, cache: { read: 0 } },
|
|
340
|
-
},
|
|
341
|
-
parts: [{ type: 'reasoning', text: 'Drafting extensive implementation...' }],
|
|
342
|
-
}]);
|
|
343
|
-
}
|
|
344
|
-
throw new Error(`Unexpected request: ${pathname}`);
|
|
345
|
-
});
|
|
346
|
-
const service = {
|
|
347
|
-
generateSmallModelText: vi.fn(),
|
|
348
|
-
};
|
|
349
|
-
vi.stubGlobal('fetch', fetchImpl);
|
|
350
|
-
const runtime = createSessionGoalRuntime({
|
|
351
|
-
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
|
352
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
353
|
-
getSmallModelService: async () => service,
|
|
354
|
-
isEnabled: () => true,
|
|
355
|
-
idleQuietMs: 10,
|
|
175
|
+
it('treats a finished compaction as a summary turn: no audit, continuation sent', async () => {
|
|
176
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
177
|
+
const { calls } = v2OpenCode({
|
|
178
|
+
messages: [
|
|
179
|
+
assistantRecord(),
|
|
180
|
+
{ id: 'msg_c1', sessionID: SESSION_ID, type: 'compaction', status: 'completed', summary: 'Summary so far', time: { created: 30, completed: 40 } },
|
|
181
|
+
],
|
|
356
182
|
});
|
|
183
|
+
const seam = wired({ openchamber: { goal: activeGoal() } });
|
|
184
|
+
const generate = vi.fn();
|
|
185
|
+
const { runtime } = makeRuntime({ ...seam, getSmallModelService: async () => ({ generateSmallModelText: generate }) });
|
|
357
186
|
|
|
358
|
-
runtime
|
|
359
|
-
type: 'session.status',
|
|
360
|
-
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
361
|
-
});
|
|
362
|
-
await vi.runOnlyPendingTimersAsync();
|
|
363
|
-
|
|
364
|
-
// Audit must be skipped on MessageOutputLengthError
|
|
365
|
-
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
366
|
-
|
|
367
|
-
// Goal remains active and turnsUsed incremented
|
|
368
|
-
const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
|
369
|
-
expect(patch).toBeDefined();
|
|
370
|
-
const writtenGoal = JSON.parse(patch.body).metadata.openchamber.goal;
|
|
371
|
-
expect(writtenGoal).toMatchObject({
|
|
372
|
-
status: 'active',
|
|
373
|
-
turnsUsed: 2,
|
|
374
|
-
});
|
|
187
|
+
await runTick(runtime);
|
|
375
188
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
expect(promptAsync).toBeDefined();
|
|
189
|
+
expect(generate).not.toHaveBeenCalled();
|
|
190
|
+
expect(calls.some((call) => call.path.endsWith('/prompt') && call.method === 'POST')).toBe(true);
|
|
379
191
|
runtime.stop();
|
|
380
192
|
});
|
|
381
193
|
|
|
382
|
-
it
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
{
|
|
386
|
-
|
|
387
|
-
const { runtime
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
await runIdleTick(runtime);
|
|
392
|
-
|
|
393
|
-
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
394
|
-
expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
|
|
395
|
-
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
396
|
-
status: 'blocked',
|
|
397
|
-
statusReason: error.name || 'assistant turn failed',
|
|
398
|
-
});
|
|
194
|
+
it('waits when the session is still running or the user just sent a message', async () => {
|
|
195
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
196
|
+
const generate = vi.fn();
|
|
197
|
+
const busy = v2OpenCode({ messages: [assistantRecord()], active: { [SESSION_ID]: { status: 'running' } } });
|
|
198
|
+
const seam = wired({ openchamber: { goal: activeGoal() } });
|
|
199
|
+
const { runtime } = makeRuntime({ ...seam, getSmallModelService: async () => ({ generateSmallModelText: generate }) });
|
|
200
|
+
await runTick(runtime);
|
|
201
|
+
expect(busy.calls.some((call) => call.method === 'POST')).toBe(false);
|
|
399
202
|
runtime.stop();
|
|
400
|
-
});
|
|
401
203
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const summary = assistantMessage('summary', { summary: true, time: { created: 15, completed: 16 } });
|
|
405
|
-
const secondLength = assistantMessage('a', { finish: 'length', time: { created: 20, completed: 21 } });
|
|
406
|
-
const { runtime, requests, service } = createRuntimeHarness({
|
|
407
|
-
messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, summary, secondLength],
|
|
204
|
+
const trailing = v2OpenCode({
|
|
205
|
+
messages: [assistantRecord(), { id: 'msg_u2', sessionID: SESSION_ID, type: 'user', text: 'wait', time: { created: 50 } }],
|
|
408
206
|
});
|
|
409
|
-
|
|
410
|
-
await
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
});
|
|
415
|
-
await vi.runOnlyPendingTimersAsync();
|
|
416
|
-
|
|
417
|
-
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
418
|
-
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(1);
|
|
419
|
-
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
420
|
-
status: 'blocked',
|
|
421
|
-
statusReason: 'repeated output truncation',
|
|
422
|
-
});
|
|
423
|
-
runtime.stop();
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('allows one explicit Resume after repeated truncation, then blocks another cutoff', async () => {
|
|
427
|
-
const first = assistantMessage('first', { finish: 'length', time: { created: 10, completed: 11 } });
|
|
428
|
-
const second = assistantMessage('second', { finish: 'length', time: { created: 20, completed: 21 } });
|
|
429
|
-
const messages = [first, second];
|
|
430
|
-
const { runtime, requests, activeSession } = createRuntimeHarness({ messages });
|
|
431
|
-
try {
|
|
432
|
-
await runIdleTick(runtime);
|
|
433
|
-
expect(lastPatchedGoal(requests).status).toBe('blocked');
|
|
434
|
-
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
|
|
435
|
-
Object.assign(activeSession.metadata.openchamber.goal, { status: 'active', statusReason: 'resumed', turnsUsed: 0 });
|
|
436
|
-
await runIdleTick(runtime);
|
|
437
|
-
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active', statusReason: '', turnsUsed: 1 });
|
|
438
|
-
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
|
|
439
|
-
messages.push(assistantMessage('third', { finish: 'length', time: { created: 30, completed: 31 } }));
|
|
440
|
-
await runIdleTick(runtime);
|
|
441
|
-
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'blocked', statusReason: 'repeated output truncation' });
|
|
442
|
-
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
|
|
443
|
-
} finally {
|
|
444
|
-
runtime.stop();
|
|
445
|
-
}
|
|
207
|
+
const { runtime: second } = makeRuntime({ ...wired({ openchamber: { goal: activeGoal() } }), getSmallModelService: async () => ({ generateSmallModelText: generate }) });
|
|
208
|
+
await runTick(second);
|
|
209
|
+
expect(trailing.calls.some((call) => call.method === 'POST')).toBe(false);
|
|
210
|
+
expect(generate).not.toHaveBeenCalled();
|
|
211
|
+
second.stop();
|
|
446
212
|
});
|
|
213
|
+
});
|
|
447
214
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
215
|
+
describe('session goal tick and subagents', () => {
|
|
216
|
+
const quiet = () => {
|
|
217
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
218
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
219
|
+
};
|
|
220
|
+
const child = (id) => ({ id, parentID: SESSION_ID, time: { updated: 1 } });
|
|
221
|
+
|
|
222
|
+
it('waits while a subagent listed on a later page is still working', async () => {
|
|
223
|
+
quiet();
|
|
224
|
+
const generate = vi.fn();
|
|
225
|
+
const server = v2OpenCode({
|
|
226
|
+
messages: [assistantRecord()],
|
|
227
|
+
active: { ses_child_2: { status: 'running' } },
|
|
228
|
+
childPages: [[child('ses_child_1')], [child('ses_child_2')]],
|
|
455
229
|
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
230
|
+
const { runtime } = makeRuntime({ ...wired({ openchamber: { goal: activeGoal() } }), getSmallModelService: async () => ({ generateSmallModelText: generate }) });
|
|
231
|
+
await runTick(runtime);
|
|
232
|
+
const listCalls = server.calls.filter((call) => call.path === '/api/session');
|
|
233
|
+
expect(listCalls.map((call) => call.query.parentID ?? call.query.cursor)).toEqual([SESSION_ID, 'page-1']);
|
|
234
|
+
expect(server.calls.some((call) => call.method === 'POST')).toBe(false);
|
|
235
|
+
expect(generate).not.toHaveBeenCalled();
|
|
463
236
|
});
|
|
464
237
|
|
|
465
|
-
it('
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
time: { created: 15, completed: 16 },
|
|
238
|
+
it('audits once every subagent is idle', async () => {
|
|
239
|
+
quiet();
|
|
240
|
+
const server = v2OpenCode({
|
|
241
|
+
messages: [assistantRecord()],
|
|
242
|
+
childPages: [[child('ses_child_1')]],
|
|
471
243
|
});
|
|
472
|
-
const {
|
|
473
|
-
|
|
244
|
+
const seam = wired({ openchamber: { goal: activeGoal() } });
|
|
245
|
+
const { runtime } = makeRuntime({
|
|
246
|
+
...seam,
|
|
247
|
+
getSmallModelService: async () => ({
|
|
248
|
+
describeSmallModel: async () => ({ inputCharBudget: 20_000 }),
|
|
249
|
+
generateSmallModelText: async () => ({ text: '{"verdict":"complete","reason":"done"}' }),
|
|
250
|
+
}),
|
|
474
251
|
});
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
480
|
-
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
|
|
481
|
-
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
|
|
482
|
-
runtime.stop();
|
|
252
|
+
await runTick(runtime);
|
|
253
|
+
expect(server.calls.some((call) => call.path === '/api/session' && call.query.parentID === SESSION_ID)).toBe(true);
|
|
254
|
+
expect(seam.persistSessionGoal).toHaveBeenCalled();
|
|
483
255
|
});
|
|
484
256
|
|
|
485
|
-
it('does not
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
const {
|
|
489
|
-
|
|
490
|
-
});
|
|
257
|
+
it('does not treat an unreadable children list as "no subagents"', async () => {
|
|
258
|
+
quiet();
|
|
259
|
+
const generate = vi.fn();
|
|
260
|
+
const server = v2OpenCode({ messages: [assistantRecord()], childrenStatus: 500 });
|
|
261
|
+
const seam = wired({ openchamber: { goal: activeGoal() } });
|
|
262
|
+
const { runtime } = makeRuntime({ ...seam, getSmallModelService: async () => ({ generateSmallModelText: generate }) });
|
|
263
|
+
await runTick(runtime);
|
|
264
|
+
expect(server.calls.some((call) => call.path === '/api/session')).toBe(true);
|
|
265
|
+
expect(server.calls.some((call) => call.path.endsWith('/message'))).toBe(false);
|
|
266
|
+
expect(server.calls.some((call) => call.method === 'POST')).toBe(false);
|
|
267
|
+
expect(generate).not.toHaveBeenCalled();
|
|
268
|
+
expect(seam.persistSessionGoal).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
491
271
|
|
|
492
|
-
|
|
493
|
-
|
|
272
|
+
describe('session goal runtime', () => {
|
|
273
|
+
it('arms the loop when a goal starts, without waiting for an OpenCode event', async () => {
|
|
274
|
+
const seam = wired();
|
|
275
|
+
const { runtime, buildOpenCodeUrl } = makeRuntime(seam);
|
|
494
276
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
277
|
+
await runtime.notifyGoalChanged('ses_1', '/repo', { openchamber: { goal: activeGoal() } });
|
|
278
|
+
// The kickoff timer is armed; the directory came with the notification, so
|
|
279
|
+
// nothing had to be looked up.
|
|
280
|
+
expect(buildOpenCodeUrl).not.toHaveBeenCalled();
|
|
498
281
|
runtime.stop();
|
|
499
282
|
});
|
|
500
283
|
|
|
501
|
-
it('
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
});
|
|
284
|
+
it('looks the directory up when a UI patch does not name one', async () => {
|
|
285
|
+
const seam = wired();
|
|
286
|
+
const fetchMock = vi.fn(async () => new Response(
|
|
287
|
+
JSON.stringify({ location: { directory: '/resolved' } }),
|
|
288
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
289
|
+
));
|
|
290
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
291
|
+
const { runtime } = makeRuntime(seam);
|
|
509
292
|
|
|
510
|
-
await
|
|
511
|
-
await runIdleTick(runtime);
|
|
293
|
+
await runtime.notifyGoalChanged('ses_1', '', { openchamber: { goal: activeGoal() } });
|
|
512
294
|
|
|
513
|
-
expect(
|
|
514
|
-
expect(
|
|
515
|
-
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
516
|
-
status: 'blocked',
|
|
517
|
-
statusReason: 'repeated output truncation',
|
|
518
|
-
});
|
|
295
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
296
|
+
expect(String(fetchMock.mock.calls[0][0])).toContain('/api/session/ses_1');
|
|
519
297
|
runtime.stop();
|
|
520
298
|
});
|
|
521
299
|
|
|
522
|
-
it('does not
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, secondLength],
|
|
528
|
-
});
|
|
300
|
+
it('does not arm for a goal that is not active or already under way', async () => {
|
|
301
|
+
const seam = wired();
|
|
302
|
+
const fetchMock = vi.fn();
|
|
303
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
304
|
+
const { runtime } = makeRuntime(seam);
|
|
529
305
|
|
|
530
|
-
await
|
|
531
|
-
await
|
|
306
|
+
await runtime.notifyGoalChanged('ses_1', '', { openchamber: { goal: activeGoal({ status: 'paused' }) } });
|
|
307
|
+
await runtime.notifyGoalChanged('ses_1', '', { openchamber: { goal: activeGoal({ turnsUsed: 3 }) } });
|
|
308
|
+
await runtime.notifyGoalChanged('ses_1', '', {});
|
|
532
309
|
|
|
533
|
-
expect(
|
|
534
|
-
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
|
|
535
|
-
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
|
|
310
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
536
311
|
runtime.stop();
|
|
537
312
|
});
|
|
538
313
|
|
|
539
|
-
it('
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
const secondLength = assistantMessage('msg_length_after_normal', { finish: 'length' });
|
|
543
|
-
const { runtime, requests, service } = createRuntimeHarness({
|
|
544
|
-
messageFactory: (fetchCount) => fetchCount < 2
|
|
545
|
-
? [firstLength]
|
|
546
|
-
: [firstLength, normal, secondLength],
|
|
547
|
-
});
|
|
314
|
+
it('does no work and reaches no service while no goal store is injected', async () => {
|
|
315
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
316
|
+
const { runtime, buildOpenCodeUrl, getSmallModelService, emitGoalNotification } = makeRuntime();
|
|
548
317
|
|
|
549
|
-
|
|
550
|
-
await
|
|
318
|
+
runtime.processPayload(idle());
|
|
319
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
551
320
|
|
|
552
|
-
expect(
|
|
553
|
-
expect(
|
|
554
|
-
expect(
|
|
555
|
-
runtime.stop();
|
|
321
|
+
expect(buildOpenCodeUrl).not.toHaveBeenCalled();
|
|
322
|
+
expect(getSmallModelService).not.toHaveBeenCalled();
|
|
323
|
+
expect(emitGoalNotification).not.toHaveBeenCalled();
|
|
556
324
|
});
|
|
557
325
|
|
|
558
|
-
it('
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
});
|
|
326
|
+
it('explains itself once, not on every event', () => {
|
|
327
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
328
|
+
const { runtime } = makeRuntime();
|
|
562
329
|
|
|
563
|
-
|
|
330
|
+
runtime.processPayload(idle('ses_1'));
|
|
331
|
+
runtime.processPayload(idle('ses_2'));
|
|
332
|
+
runtime.processPayload({ type: 'session.updated', properties: { info: { id: 'ses_3' } } });
|
|
564
333
|
|
|
565
|
-
|
|
566
|
-
expect(
|
|
567
|
-
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
568
|
-
status: 'paused',
|
|
569
|
-
statusReason: 'paused after abort',
|
|
570
|
-
});
|
|
571
|
-
runtime.stop();
|
|
334
|
+
const notices = log.mock.calls.filter(([line]) => String(line).includes('[session-goal] parked'));
|
|
335
|
+
expect(notices).toHaveLength(1);
|
|
572
336
|
});
|
|
573
337
|
|
|
574
|
-
it('
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
messages: [assistantMessage('msg_budget_length', {
|
|
578
|
-
finish: 'length',
|
|
579
|
-
tokens: { input: 3, output: 3, cache: { read: 0 } },
|
|
580
|
-
})],
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
await runIdleTick(runtime);
|
|
338
|
+
it('ignores everything after stop', () => {
|
|
339
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
340
|
+
const { runtime } = makeRuntime();
|
|
584
341
|
|
|
585
|
-
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
586
|
-
expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
|
|
587
|
-
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'budgetLimited' });
|
|
588
342
|
runtime.stop();
|
|
589
|
-
|
|
343
|
+
runtime.processPayload(idle());
|
|
590
344
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
maxAutoTurns: 1,
|
|
594
|
-
messages: [assistantMessage('msg_cap_length', { finish: 'length' })],
|
|
595
|
-
});
|
|
345
|
+
expect(log).not.toHaveBeenCalled();
|
|
346
|
+
});
|
|
596
347
|
|
|
597
|
-
|
|
348
|
+
it('reads a user abort off the aborted idle event v2 produces', () => {
|
|
349
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
350
|
+
const { runtime } = makeRuntime(wired());
|
|
598
351
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
expect(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
});
|
|
605
|
-
runtime.stop();
|
|
352
|
+
// The old signal was an assistant message carrying MessageAbortedError;
|
|
353
|
+
// accepting the new one must not throw or be mistaken for a normal idle.
|
|
354
|
+
expect(() => runtime.processPayload({
|
|
355
|
+
type: 'session.idle',
|
|
356
|
+
properties: { sessionID: SESSION_ID, aborted: true, reason: 'user' },
|
|
357
|
+
})).not.toThrow();
|
|
606
358
|
});
|
|
607
359
|
});
|