@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3
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 +13 -7
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +179 -0
- package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
- package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
- package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
- package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
- package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
- package/dist/assets/SettingsView-BRxELjow.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
- package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
- package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
- package/dist/assets/collapsible-DXbDynN4.js +3 -0
- package/dist/assets/context-menu-CqolsLCm.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-CM42qUed.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
- package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/main-BILgBI3B.js +169 -0
- package/dist/assets/main-BeveiW6F.js +2 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
- package/dist/assets/miniChat-BgrzM3u7.js +2 -0
- package/dist/assets/mobile-DtYoQE8C.js +2 -0
- package/dist/assets/multirun-jy8dEI96.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-BQghHRfp.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
- package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
- package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
- package/dist/assets/src-CLN7Z-zx.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
- package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
- package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +2 -2
- package/server/index.js +392 -20
- package/server/lib/assistants/DOCUMENTATION.md +26 -21
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +454 -0
- package/server/lib/assistants/routes.js +11 -5
- package/server/lib/assistants/service.js +850 -294
- package/server/lib/assistants/service.test.js +588 -274
- package/server/lib/conversations/DOCUMENTATION.md +5 -5
- package/server/lib/conversations/service.js +87 -57
- package/server/lib/conversations/service.test.js +132 -100
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +51 -5
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +95 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +41 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +516 -453
- package/server/lib/llm/generate.test.js +705 -484
- package/server/lib/llm/routes.js +8 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/notifications/runtime.js +5 -0
- package/server/lib/opencode/DOCUMENTATION.md +103 -16
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +111 -102
- package/server/lib/opencode/config-entity-routes.test.js +123 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +238 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +208 -111
- package/server/lib/opencode/env-runtime.test.js +211 -14
- package/server/lib/opencode/feature-routes-runtime.js +80 -5
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +539 -95
- package/server/lib/opencode/lifecycle.test.js +698 -6
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +88 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +368 -28
- package/server/lib/opencode/proxy.test.js +338 -0
- package/server/lib/opencode/routes.js +303 -76
- package/server/lib/opencode/routes.test.js +315 -50
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +35 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +26 -40
- package/server/lib/opencode/skill-routes.test.js +71 -7
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +60 -0
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +381 -310
- package/server/lib/scheduled-tasks/runtime.test.js +600 -309
- package/server/lib/session-goal/DOCUMENTATION.md +173 -22
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +240 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +1617 -113
- package/server/lib/session-goal/runtime.test.js +1500 -70
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +171 -41
- package/server/lib/session-index/sync-runtime.test.js +164 -9
- package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
- package/server/lib/session-metadata/routes.js +193 -0
- package/server/lib/session-metadata/routes.test.js +156 -0
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +375 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +123 -75
- package/server/lib/session-turn-pages/routes.test.js +566 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -146
- package/server/lib/small-model/catalog.js +84 -54
- package/server/lib/small-model/catalog.test.js +111 -9
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +125 -241
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +63 -12
- package/dist/assets/AssistantView-BUveAgc4.js +0 -3
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
- package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
- package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
- package/dist/assets/SettingsView-PkXIavr7.js +0 -25
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/collapsible-D5Phaykh.js +0 -1
- package/dist/assets/context-menu-CubSDW2g.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fr-Dbz0CdIA.js +0 -13
- package/dist/assets/gitApi-DXyC4MiV.js +0 -7
- package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
- package/dist/assets/ja-DumanVQA.js +0 -15
- package/dist/assets/ko-C0IkiUjQ.js +0 -15
- package/dist/assets/main-BPS5ylib.js +0 -2
- package/dist/assets/main-Cpd9VWYs.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-B8_KLWiY.js +0 -2
- package/dist/assets/mobile-CtktKrXn.js +0 -2
- package/dist/assets/multirun-CYfP9Cfm.js +0 -4
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
- package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
- package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -2,12 +2,20 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
5
8
|
|
|
6
|
-
vi.mock('
|
|
7
|
-
|
|
9
|
+
vi.mock('../opencode/v2-client.js', () => ({
|
|
10
|
+
makeOpenCodeV2Client: vi.fn(),
|
|
8
11
|
}));
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
vi.mock('../session-goal/objectives.js', () => ({
|
|
14
|
+
writeObjective: vi.fn(async () => ({ content: 'ok' })),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const { makeOpenCodeV2Client } = await import('../opencode/v2-client.js');
|
|
18
|
+
const { writeObjective } = await import('../session-goal/objectives.js');
|
|
11
19
|
const { createScheduledTaskRunHistoryStore } = await import('./run-history-store.js');
|
|
12
20
|
const {
|
|
13
21
|
computeNextRunAt,
|
|
@@ -37,6 +45,7 @@ const createRuntime = (updateScheduledTaskState, overrides = {}) => createSchedu
|
|
|
37
45
|
waitForOpenCodeReady: vi.fn(async () => {
|
|
38
46
|
throw new Error('OpenCode unavailable');
|
|
39
47
|
}),
|
|
48
|
+
persistSessionMetadata: vi.fn(async () => ({})),
|
|
40
49
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
41
50
|
...overrides,
|
|
42
51
|
});
|
|
@@ -310,10 +319,26 @@ describe('scheduled-tasks project sync isolation', () => {
|
|
|
310
319
|
});
|
|
311
320
|
});
|
|
312
321
|
|
|
322
|
+
describe('scheduled-tasks v2 cutover residuals', () => {
|
|
323
|
+
it('runtime source has no 1.x SDK, HeyAPI wrappers, or retired session APIs', () => {
|
|
324
|
+
const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'runtime.js'), 'utf8');
|
|
325
|
+
expect(source).not.toContain('@opencode-ai/sdk');
|
|
326
|
+
expect(source).not.toContain('createOpencodeClient');
|
|
327
|
+
expect(source).not.toContain('prompt_async');
|
|
328
|
+
expect(source).not.toContain('session.abort');
|
|
329
|
+
expect(source).not.toContain('session.status({');
|
|
330
|
+
expect(source).not.toContain('session.messages');
|
|
331
|
+
expect(source).not.toContain('session.update');
|
|
332
|
+
expect(source).not.toContain('time: { archived');
|
|
333
|
+
expect(source).not.toContain('result?.error');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
313
337
|
describe('scheduled-tasks run history and session lifecycle', () => {
|
|
314
338
|
beforeEach(() => {
|
|
315
339
|
vi.restoreAllMocks();
|
|
316
|
-
|
|
340
|
+
makeOpenCodeV2Client.mockReset();
|
|
341
|
+
writeObjective.mockClear();
|
|
317
342
|
});
|
|
318
343
|
|
|
319
344
|
const createHistoryStore = () => {
|
|
@@ -341,111 +366,94 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
341
366
|
};
|
|
342
367
|
|
|
343
368
|
const createSuccessfulClient = ({
|
|
344
|
-
updateResult,
|
|
345
369
|
sessionID = 'ses_1',
|
|
346
370
|
/** Override session settlement polls after prompt/command admission. */
|
|
347
371
|
settlement = 'success',
|
|
372
|
+
activeImpl,
|
|
373
|
+
messageListImpl,
|
|
348
374
|
} = {}) => {
|
|
349
|
-
const create = vi.fn(async () => ({
|
|
350
|
-
const
|
|
351
|
-
const command = vi.fn(async () => ({
|
|
352
|
-
const
|
|
375
|
+
const create = vi.fn(async () => ({ id: sessionID }));
|
|
376
|
+
const prompt = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }));
|
|
377
|
+
const command = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }));
|
|
378
|
+
const interrupt = vi.fn(async () => undefined);
|
|
353
379
|
const list = vi.fn(async () => ({ data: [] }));
|
|
354
|
-
// Default settlement: idle
|
|
355
|
-
// turn outcome (not
|
|
380
|
+
// Default settlement: idle (absent from active map) + completed assistant
|
|
381
|
+
// so history records the real turn outcome (not prompt admission alone).
|
|
356
382
|
let poll = 0;
|
|
357
|
-
const
|
|
383
|
+
const active = vi.fn(async () => {
|
|
384
|
+
if (typeof activeImpl === 'function') {
|
|
385
|
+
return activeImpl();
|
|
386
|
+
}
|
|
358
387
|
poll += 1;
|
|
359
388
|
if (settlement === 'busy-then-success' && poll < 2) {
|
|
360
|
-
return {
|
|
389
|
+
return { [sessionID]: { type: 'running' } };
|
|
361
390
|
}
|
|
362
|
-
return {
|
|
391
|
+
return {};
|
|
363
392
|
});
|
|
364
|
-
const
|
|
393
|
+
const messageList = vi.fn(async () => {
|
|
394
|
+
if (typeof messageListImpl === 'function') {
|
|
395
|
+
return messageListImpl();
|
|
396
|
+
}
|
|
365
397
|
if (settlement === 'assistant-error') {
|
|
366
398
|
return {
|
|
367
399
|
data: [{
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
error: { name: 'ProviderError', message: 'upstream failed' },
|
|
372
|
-
},
|
|
400
|
+
id: 'msg_err',
|
|
401
|
+
type: 'assistant',
|
|
402
|
+
error: { name: 'ProviderError', message: 'upstream failed' },
|
|
373
403
|
}],
|
|
374
404
|
};
|
|
375
405
|
}
|
|
376
|
-
if (settlement === 'busy-then-success') {
|
|
377
|
-
|
|
378
|
-
return { data: [{ info: { id: 'msg_user', role: 'user' } }] };
|
|
379
|
-
}
|
|
406
|
+
if (settlement === 'busy-then-success' && poll < 2) {
|
|
407
|
+
return { data: [{ id: 'msg_user', type: 'user' }] };
|
|
380
408
|
}
|
|
381
409
|
return {
|
|
382
410
|
data: [{
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
},
|
|
411
|
+
id: 'msg_ok',
|
|
412
|
+
type: 'assistant',
|
|
413
|
+
time: { created: Date.now(), completed: Date.now() },
|
|
414
|
+
finish: 'stop',
|
|
388
415
|
}],
|
|
389
416
|
};
|
|
390
417
|
});
|
|
391
|
-
const get = vi.fn(async () => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
goal: { id: 'g1', status: 'complete', objective: 'done' },
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if (settlement === 'goal-blocked') {
|
|
405
|
-
return {
|
|
406
|
-
data: {
|
|
407
|
-
id: sessionID,
|
|
408
|
-
metadata: {
|
|
409
|
-
openchamber: {
|
|
410
|
-
goal: { id: 'g1', status: 'blocked', objective: 'x', note: 'stuck' },
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
return { data: { id: sessionID, metadata: {} } };
|
|
417
|
-
});
|
|
418
|
-
createOpencodeClient.mockReturnValue({
|
|
419
|
-
session: { create, update, command, abort, status, messages, get },
|
|
418
|
+
const get = vi.fn(async () => ({
|
|
419
|
+
id: sessionID,
|
|
420
|
+
location: { directory: '/tmp/project-1' },
|
|
421
|
+
}));
|
|
422
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
423
|
+
session: { create, prompt, command, interrupt, active, get },
|
|
424
|
+
message: { list: messageList },
|
|
420
425
|
command: { list },
|
|
421
426
|
});
|
|
422
|
-
return { create,
|
|
427
|
+
return { create, prompt, command, interrupt, list, active, messageList, get };
|
|
423
428
|
};
|
|
424
429
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
get: vi.fn(async () => ({ data: { id: sessionID, metadata: {} } })),
|
|
430
|
+
const completedAssistant = (id = 'msg_ok') => ({
|
|
431
|
+
id,
|
|
432
|
+
type: 'assistant',
|
|
433
|
+
time: { created: 1_000, completed: 1_100 },
|
|
434
|
+
finish: 'stop',
|
|
431
435
|
});
|
|
432
436
|
|
|
433
|
-
|
|
437
|
+
const erroredAssistant = (id = 'msg_err') => ({
|
|
438
|
+
id,
|
|
439
|
+
type: 'assistant',
|
|
440
|
+
time: { created: 2_000, completed: 2_100 },
|
|
441
|
+
error: { name: 'ProviderError', message: 'upstream failed' },
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('creates session with Scheduled title and location, attaches history, then prompts via session.prompt', async () => {
|
|
434
445
|
const history = createHistoryStore();
|
|
435
446
|
const client = createSuccessfulClient();
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
return { ok: true, text: async () => '' };
|
|
439
|
-
}
|
|
440
|
-
throw new Error(`unexpected fetch ${url} ${init?.method}`);
|
|
441
|
-
});
|
|
442
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
447
|
+
const persistSessionMetadata = vi.fn(async () => ({}));
|
|
448
|
+
const onSystemSessionPersisted = vi.fn();
|
|
443
449
|
|
|
444
450
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
445
451
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
446
452
|
}));
|
|
447
453
|
const runtime = createRuntime(updateScheduledTaskState, {
|
|
448
454
|
runHistoryStore: history,
|
|
455
|
+
persistSessionMetadata,
|
|
456
|
+
onSystemSessionPersisted,
|
|
449
457
|
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
450
458
|
});
|
|
451
459
|
await runtime.syncProject('project-1');
|
|
@@ -463,49 +471,105 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
463
471
|
});
|
|
464
472
|
expect(typeof started.id).toBe('string');
|
|
465
473
|
|
|
474
|
+
expect(makeOpenCodeV2Client).toHaveBeenCalledWith({
|
|
475
|
+
baseUrl: 'http://127.0.0.1:4096',
|
|
476
|
+
authHeaders: {},
|
|
477
|
+
});
|
|
466
478
|
expect(client.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
467
|
-
directory: '/tmp/project-1',
|
|
479
|
+
location: { directory: '/tmp/project-1' },
|
|
468
480
|
title: expect.stringMatching(/^\[Scheduled\] Task /),
|
|
481
|
+
model: { id: 'gpt-4.1', providerID: 'openai' },
|
|
469
482
|
metadata: {
|
|
470
483
|
openchamber: {
|
|
471
|
-
scheduledTask: {
|
|
484
|
+
scheduledTask: expect.objectContaining({
|
|
472
485
|
projectID: 'project-1',
|
|
473
486
|
taskID: 'task-1',
|
|
474
487
|
runID: started.id,
|
|
475
488
|
name: 'Task',
|
|
476
|
-
},
|
|
489
|
+
}),
|
|
477
490
|
},
|
|
478
491
|
},
|
|
479
492
|
}), expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
480
|
-
expect(
|
|
481
|
-
expect(
|
|
493
|
+
expect(client.create.mock.calls[0][0].directory).toBeUndefined();
|
|
494
|
+
expect(persistSessionMetadata).toHaveBeenCalledWith('ses_1', {
|
|
495
|
+
openchamber: {
|
|
496
|
+
scheduledTask: expect.objectContaining({
|
|
497
|
+
projectID: 'project-1',
|
|
498
|
+
taskID: 'task-1',
|
|
499
|
+
runID: started.id,
|
|
500
|
+
name: 'Task',
|
|
501
|
+
}),
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
expect(onSystemSessionPersisted).toHaveBeenCalledWith({
|
|
482
505
|
sessionID: 'ses_1',
|
|
483
506
|
directory: '/tmp/project-1',
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
507
|
+
metadata: expect.objectContaining({
|
|
508
|
+
openchamber: expect.objectContaining({
|
|
509
|
+
scheduledTask: expect.objectContaining({ taskID: 'task-1' }),
|
|
510
|
+
}),
|
|
511
|
+
}),
|
|
512
|
+
});
|
|
513
|
+
expect(history.attachSession).toHaveBeenCalledWith(started.id, 'ses_1');
|
|
514
|
+
expect(client.prompt).toHaveBeenCalledWith({
|
|
515
|
+
sessionID: 'ses_1',
|
|
516
|
+
text: 'run',
|
|
517
|
+
}, expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
487
518
|
expect(history.finishRun).toHaveBeenCalledWith(started.id, expect.objectContaining({
|
|
488
519
|
status: 'success',
|
|
489
520
|
sessionId: 'ses_1',
|
|
490
521
|
}));
|
|
491
|
-
// Must wait for session settlement, not finish on
|
|
492
|
-
expect(client.
|
|
493
|
-
expect(client.
|
|
494
|
-
expect(client.
|
|
522
|
+
// Must wait for session settlement, not finish on prompt admission alone.
|
|
523
|
+
expect(client.active).toHaveBeenCalled();
|
|
524
|
+
expect(client.messageList).toHaveBeenCalled();
|
|
525
|
+
expect(client.active.mock.invocationCallOrder[0]).toBeGreaterThan(client.prompt.mock.invocationCallOrder[0]);
|
|
526
|
+
});
|
|
495
527
|
|
|
496
|
-
|
|
528
|
+
it('refuses before session create when Host metadata persist is not configured', async () => {
|
|
529
|
+
const history = createHistoryStore();
|
|
530
|
+
const client = createSuccessfulClient();
|
|
531
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
532
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
533
|
+
}));
|
|
534
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
535
|
+
runHistoryStore: history,
|
|
536
|
+
persistSessionMetadata: null,
|
|
537
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
538
|
+
});
|
|
539
|
+
await runtime.syncProject('project-1');
|
|
540
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
541
|
+
|
|
542
|
+
expect(result.ok).toBe(false);
|
|
543
|
+
expect(result.error).toMatch(/session metadata persist is not configured/);
|
|
544
|
+
expect(client.create).not.toHaveBeenCalled();
|
|
545
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('fails the run before prompt when Host metadata persist throws', async () => {
|
|
549
|
+
const history = createHistoryStore();
|
|
550
|
+
const client = createSuccessfulClient();
|
|
551
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
552
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
553
|
+
}));
|
|
554
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
555
|
+
runHistoryStore: history,
|
|
556
|
+
persistSessionMetadata: vi.fn(async () => {
|
|
557
|
+
throw new Error('store unavailable');
|
|
558
|
+
}),
|
|
559
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
560
|
+
});
|
|
561
|
+
await runtime.syncProject('project-1');
|
|
562
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
563
|
+
|
|
564
|
+
expect(result.ok).toBe(false);
|
|
565
|
+
expect(result.error).toMatch(/store unavailable/);
|
|
566
|
+
expect(client.create).toHaveBeenCalled();
|
|
567
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
497
568
|
});
|
|
498
569
|
|
|
499
570
|
it('records error outcome and real duration when the assistant turn fails after admission', async () => {
|
|
500
571
|
const history = createHistoryStore();
|
|
501
572
|
const client = createSuccessfulClient({ settlement: 'assistant-error' });
|
|
502
|
-
const fetchMock = vi.fn(async (url) => {
|
|
503
|
-
if (String(url).includes('prompt_async')) {
|
|
504
|
-
return { ok: true, text: async () => '' };
|
|
505
|
-
}
|
|
506
|
-
throw new Error(`unexpected fetch ${url}`);
|
|
507
|
-
});
|
|
508
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
509
573
|
|
|
510
574
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
511
575
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -528,22 +592,13 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
528
592
|
durationMs: expect.any(Number),
|
|
529
593
|
}),
|
|
530
594
|
);
|
|
531
|
-
expect(client.
|
|
532
|
-
|
|
533
|
-
vi.unstubAllGlobals();
|
|
595
|
+
expect(client.messageList).toHaveBeenCalled();
|
|
534
596
|
});
|
|
535
597
|
|
|
536
|
-
it('keeps the run open while the session is
|
|
598
|
+
it('keeps the run open while the session is in the active map and only finalizes after idle settlement', async () => {
|
|
537
599
|
vi.useFakeTimers();
|
|
538
600
|
const history = createHistoryStore();
|
|
539
601
|
createSuccessfulClient({ settlement: 'busy-then-success' });
|
|
540
|
-
const fetchMock = vi.fn(async (url) => {
|
|
541
|
-
if (String(url).includes('prompt_async')) {
|
|
542
|
-
return { ok: true, text: async () => '' };
|
|
543
|
-
}
|
|
544
|
-
throw new Error(`unexpected fetch ${url}`);
|
|
545
|
-
});
|
|
546
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
547
602
|
|
|
548
603
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
549
604
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -555,7 +610,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
555
610
|
await runtime.syncProject('project-1');
|
|
556
611
|
|
|
557
612
|
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
558
|
-
// First settlement poll sees
|
|
613
|
+
// First settlement poll sees membership and sleeps — history must still be open.
|
|
559
614
|
await vi.advanceTimersByTimeAsync(0);
|
|
560
615
|
expect(history.finishRun).not.toHaveBeenCalled();
|
|
561
616
|
|
|
@@ -568,17 +623,15 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
568
623
|
expect.objectContaining({ status: 'success', sessionId: 'ses_1' }),
|
|
569
624
|
);
|
|
570
625
|
|
|
571
|
-
vi.unstubAllGlobals();
|
|
572
626
|
vi.useRealTimers();
|
|
573
627
|
});
|
|
574
628
|
|
|
575
|
-
it('does not prompt when
|
|
629
|
+
it('does not prompt when attachSession fails and finalizes the run as error', async () => {
|
|
576
630
|
const history = createHistoryStore();
|
|
577
|
-
createSuccessfulClient(
|
|
578
|
-
|
|
631
|
+
const client = createSuccessfulClient();
|
|
632
|
+
history.attachSession.mockImplementation(() => {
|
|
633
|
+
throw new Error('attach exploded');
|
|
579
634
|
});
|
|
580
|
-
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
|
|
581
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
582
635
|
|
|
583
636
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
584
637
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -592,28 +645,22 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
592
645
|
|
|
593
646
|
expect(result.ok).toBe(false);
|
|
594
647
|
expect(result.status).toBe('error');
|
|
595
|
-
expect(
|
|
648
|
+
expect(result.error).toMatch(/attachSession failed/);
|
|
649
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
596
650
|
expect(history.finishRun).toHaveBeenCalledWith(
|
|
597
651
|
expect.any(String),
|
|
598
652
|
expect.objectContaining({ status: 'error' }),
|
|
599
653
|
);
|
|
600
|
-
|
|
601
|
-
vi.unstubAllGlobals();
|
|
602
654
|
});
|
|
603
655
|
|
|
604
|
-
it('
|
|
656
|
+
it('does not treat session.active failure as idle success', async () => {
|
|
605
657
|
vi.useFakeTimers();
|
|
606
658
|
const history = createHistoryStore();
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
createOpencodeClient.mockReturnValue({
|
|
612
|
-
session: { create, update, command: vi.fn(), ...settledSessionApis('ses_404') },
|
|
613
|
-
command: { list: vi.fn(async () => ({ data: [] })) },
|
|
659
|
+
createSuccessfulClient({
|
|
660
|
+
activeImpl: async () => {
|
|
661
|
+
throw new Error('active unavailable');
|
|
662
|
+
},
|
|
614
663
|
});
|
|
615
|
-
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
|
|
616
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
617
664
|
|
|
618
665
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
619
666
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -621,63 +668,67 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
621
668
|
const runtime = createRuntime(updateScheduledTaskState, {
|
|
622
669
|
runHistoryStore: history,
|
|
623
670
|
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
671
|
+
maxRunDurationMs: 2_500,
|
|
624
672
|
});
|
|
625
673
|
await runtime.syncProject('project-1');
|
|
626
674
|
|
|
627
675
|
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
628
|
-
await vi.advanceTimersByTimeAsync(
|
|
676
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
677
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
678
|
+
|
|
679
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
629
680
|
const result = await runPromise;
|
|
630
681
|
|
|
631
|
-
expect(result.ok).toBe(
|
|
632
|
-
expect(
|
|
633
|
-
expect(
|
|
682
|
+
expect(result.ok).toBe(false);
|
|
683
|
+
expect(result.status).toBe('error');
|
|
684
|
+
expect(result.error).toBe('schedule run timed out');
|
|
685
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
686
|
+
expect.any(String),
|
|
687
|
+
expect.objectContaining({
|
|
688
|
+
status: 'error',
|
|
689
|
+
error: 'schedule run timed out',
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
634
692
|
|
|
635
|
-
vi.unstubAllGlobals();
|
|
636
693
|
vi.useRealTimers();
|
|
637
694
|
});
|
|
638
695
|
|
|
639
|
-
it('watchdog timeout
|
|
696
|
+
it('watchdog timeout interrupts in-flight work and does not continue later stages', async () => {
|
|
640
697
|
vi.useFakeTimers();
|
|
641
698
|
const history = createHistoryStore();
|
|
642
699
|
let resolvePrompt;
|
|
643
700
|
let promptStarted = false;
|
|
644
701
|
let promptCompleted = false;
|
|
645
|
-
const create = vi.fn(async () => ({
|
|
646
|
-
const
|
|
647
|
-
const abort = vi.fn(async () => ({ data: true }));
|
|
702
|
+
const create = vi.fn(async () => ({ id: 'ses_timeout' }));
|
|
703
|
+
const interrupt = vi.fn(async () => undefined);
|
|
648
704
|
const command = vi.fn(async () => {
|
|
649
705
|
throw new Error('command must not run after hanging prompt path');
|
|
650
706
|
});
|
|
651
707
|
const list = vi.fn(async () => ({ data: [] }));
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
resolve({ ok: true, text: async () => '' });
|
|
708
|
+
const prompt = vi.fn((_params, options) => {
|
|
709
|
+
promptStarted = true;
|
|
710
|
+
return new Promise((resolve, reject) => {
|
|
711
|
+
resolvePrompt = () => {
|
|
712
|
+
promptCompleted = true;
|
|
713
|
+
resolve({ id: 'inbox_1', type: 'user' });
|
|
714
|
+
};
|
|
715
|
+
const signal = options?.signal;
|
|
716
|
+
if (signal) {
|
|
717
|
+
const onAbort = () => {
|
|
718
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
664
719
|
};
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
669
|
-
};
|
|
670
|
-
if (signal.aborted) {
|
|
671
|
-
onAbort();
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
720
|
+
if (signal.aborted) {
|
|
721
|
+
onAbort();
|
|
722
|
+
return;
|
|
675
723
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
724
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
729
|
+
session: { create, prompt, command, interrupt },
|
|
730
|
+
command: { list },
|
|
679
731
|
});
|
|
680
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
681
732
|
|
|
682
733
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
683
734
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -706,9 +757,9 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
706
757
|
expect(result.ok).toBe(false);
|
|
707
758
|
expect(result.status).toBe('error');
|
|
708
759
|
expect(result.error).toBe('schedule run timed out');
|
|
709
|
-
expect(
|
|
760
|
+
expect(interrupt).toHaveBeenCalledWith({
|
|
710
761
|
sessionID: 'ses_timeout',
|
|
711
|
-
|
|
762
|
+
continue: false,
|
|
712
763
|
});
|
|
713
764
|
expect(promptCompleted).toBe(false);
|
|
714
765
|
expect(command).not.toHaveBeenCalled();
|
|
@@ -731,18 +782,16 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
731
782
|
} finally {
|
|
732
783
|
process.off('unhandledRejection', onUnhandled);
|
|
733
784
|
resolvePrompt?.();
|
|
734
|
-
vi.unstubAllGlobals();
|
|
735
785
|
vi.useRealTimers();
|
|
736
786
|
}
|
|
737
787
|
});
|
|
738
788
|
|
|
739
|
-
it('watchdog timeout during hanging command
|
|
789
|
+
it('watchdog timeout during hanging command interrupts session and never session.prompt', async () => {
|
|
740
790
|
vi.useFakeTimers();
|
|
741
791
|
const history = createHistoryStore();
|
|
742
792
|
let commandReached = false;
|
|
743
|
-
const create = vi.fn(async () => ({
|
|
744
|
-
const
|
|
745
|
-
const abort = vi.fn(async () => ({ data: true }));
|
|
793
|
+
const create = vi.fn(async () => ({ id: 'ses_cmd' }));
|
|
794
|
+
const interrupt = vi.fn(async () => undefined);
|
|
746
795
|
const command = vi.fn((_params, options) => {
|
|
747
796
|
commandReached = true;
|
|
748
797
|
return new Promise((_resolve, reject) => {
|
|
@@ -761,14 +810,13 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
761
810
|
});
|
|
762
811
|
});
|
|
763
812
|
const list = vi.fn(async () => ({ data: [{ name: 'review' }] }));
|
|
764
|
-
|
|
765
|
-
session
|
|
766
|
-
command: { list },
|
|
813
|
+
const prompt = vi.fn(async () => {
|
|
814
|
+
throw new Error('session.prompt must not run when command is selected');
|
|
767
815
|
});
|
|
768
|
-
|
|
769
|
-
|
|
816
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
817
|
+
session: { create, prompt, command, interrupt },
|
|
818
|
+
command: { list },
|
|
770
819
|
});
|
|
771
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
772
820
|
|
|
773
821
|
const taskWithCommand = {
|
|
774
822
|
...scheduledTask,
|
|
@@ -790,6 +838,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
790
838
|
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
791
839
|
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
792
840
|
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
841
|
+
persistSessionMetadata: vi.fn(async () => ({})),
|
|
793
842
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
794
843
|
runHistoryStore: history,
|
|
795
844
|
maxRunDurationMs: 500,
|
|
@@ -808,47 +857,41 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
808
857
|
status: 'error',
|
|
809
858
|
error: 'schedule run timed out',
|
|
810
859
|
});
|
|
811
|
-
expect(
|
|
860
|
+
expect(interrupt).toHaveBeenCalledWith({
|
|
812
861
|
sessionID: 'ses_cmd',
|
|
813
|
-
|
|
862
|
+
continue: false,
|
|
814
863
|
});
|
|
815
|
-
expect(
|
|
864
|
+
expect(command).toHaveBeenCalledWith(expect.objectContaining({
|
|
865
|
+
sessionID: 'ses_cmd',
|
|
866
|
+
command: 'review',
|
|
867
|
+
arguments: 'src',
|
|
868
|
+
model: { id: 'gpt-4.1', providerID: 'openai' },
|
|
869
|
+
}), expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
870
|
+
expect(list).toHaveBeenCalledWith({
|
|
871
|
+
location: { directory: '/tmp/project-1' },
|
|
872
|
+
}, expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
873
|
+
expect(prompt).not.toHaveBeenCalled();
|
|
816
874
|
|
|
817
|
-
vi.unstubAllGlobals();
|
|
818
875
|
vi.useRealTimers();
|
|
819
876
|
});
|
|
820
877
|
|
|
821
|
-
it('
|
|
822
|
-
vi.useFakeTimers();
|
|
878
|
+
it('goalEnabled without persist refuses before OpenCode ready / distill / session create (no side effects)', async () => {
|
|
823
879
|
const history = createHistoryStore();
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
const abort = vi.fn(async () => ({ data: true }));
|
|
829
|
-
const command = vi.fn(async () => {
|
|
830
|
-
throw new Error('command must not run after hanging distill');
|
|
880
|
+
const create = vi.fn(async () => ({ id: 'ses_distill' }));
|
|
881
|
+
const interrupt = vi.fn(async () => undefined);
|
|
882
|
+
const prompt = vi.fn(async () => {
|
|
883
|
+
throw new Error('session.prompt must not run without goal persist');
|
|
831
884
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
command: { list },
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
const fetchMock = vi.fn(async (url, init) => {
|
|
839
|
-
throw new Error(`unexpected fetch ${url} ${init?.method}`);
|
|
885
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
886
|
+
session: { create, prompt, command: vi.fn(), interrupt },
|
|
887
|
+
command: { list: vi.fn(async () => ({ data: [] })) },
|
|
840
888
|
});
|
|
841
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
842
889
|
|
|
843
|
-
const generateSmallModelText = vi.fn(() => {
|
|
844
|
-
distillStarted = true;
|
|
845
|
-
return new Promise((resolve) => {
|
|
846
|
-
resolveDistill = () => resolve({ text: 'distilled criteria' });
|
|
847
|
-
});
|
|
848
|
-
});
|
|
890
|
+
const generateSmallModelText = vi.fn(async () => ({ text: 'should not distill' }));
|
|
849
891
|
const getSmallModelService = vi.fn(async () => ({ generateSmallModelText }));
|
|
892
|
+
const waitForOpenCodeReady = vi.fn(async () => {});
|
|
850
893
|
|
|
851
|
-
// Oversized prompt
|
|
894
|
+
// Oversized prompt would force distill if goal were admitted — it must not.
|
|
852
895
|
const largePrompt = `finish the migration ${'x'.repeat(5100)}`;
|
|
853
896
|
const goalTask = {
|
|
854
897
|
...scheduledTask,
|
|
@@ -872,78 +915,162 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
872
915
|
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
873
916
|
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
874
917
|
getSmallModelService,
|
|
875
|
-
waitForOpenCodeReady
|
|
918
|
+
waitForOpenCodeReady,
|
|
876
919
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
877
920
|
runHistoryStore: history,
|
|
878
|
-
|
|
921
|
+
// intentionally no persistSessionGoal
|
|
879
922
|
});
|
|
880
923
|
await runtime.syncProject('project-1');
|
|
924
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
881
925
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
926
|
+
expect(result.ok).toBe(false);
|
|
927
|
+
expect(result.error).toMatch(/session goal persist is not configured/);
|
|
928
|
+
expect(waitForOpenCodeReady).not.toHaveBeenCalled();
|
|
929
|
+
expect(getSmallModelService).not.toHaveBeenCalled();
|
|
930
|
+
expect(generateSmallModelText).not.toHaveBeenCalled();
|
|
931
|
+
expect(create).not.toHaveBeenCalled();
|
|
932
|
+
expect(prompt).not.toHaveBeenCalled();
|
|
933
|
+
expect(writeObjective).not.toHaveBeenCalled();
|
|
934
|
+
expect(history.attachSession).not.toHaveBeenCalled();
|
|
935
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
936
|
+
expect.any(String),
|
|
937
|
+
expect.objectContaining({
|
|
938
|
+
status: 'error',
|
|
939
|
+
error: expect.stringMatching(/session goal persist is not configured/),
|
|
940
|
+
}),
|
|
941
|
+
);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('goalEnabled without persist refuses before session create / objective write', async () => {
|
|
945
|
+
const history = createHistoryStore();
|
|
946
|
+
const client = createSuccessfulClient({ sessionID: 'ses_goal' });
|
|
947
|
+
|
|
948
|
+
const goalTask = {
|
|
949
|
+
...scheduledTask,
|
|
950
|
+
execution: {
|
|
951
|
+
...scheduledTask.execution,
|
|
952
|
+
prompt: 'finish the migration',
|
|
953
|
+
goalEnabled: true,
|
|
954
|
+
goalTokenBudget: 12_000,
|
|
955
|
+
},
|
|
885
956
|
};
|
|
886
|
-
|
|
957
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
958
|
+
task: { ...goalTask, state: { ...goalTask.state, ...state } },
|
|
959
|
+
}));
|
|
960
|
+
const runtime = createScheduledTasksRuntime({
|
|
961
|
+
projectConfigRuntime: {
|
|
962
|
+
listScheduledTasks: vi.fn(async () => [goalTask]),
|
|
963
|
+
updateScheduledTaskState,
|
|
964
|
+
upsertScheduledTask: vi.fn(),
|
|
965
|
+
},
|
|
966
|
+
listProjects: vi.fn(async () => [{ id: 'project-1', path: '/tmp/project-1' }]),
|
|
967
|
+
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
968
|
+
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
969
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
970
|
+
logger: { info: vi.fn(), warn: vi.fn() },
|
|
971
|
+
runHistoryStore: history,
|
|
972
|
+
// intentionally no persistSessionGoal
|
|
973
|
+
});
|
|
974
|
+
await runtime.syncProject('project-1');
|
|
975
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
887
976
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
977
|
+
// Missing persist is an explicit run failure before any side effects.
|
|
978
|
+
expect(result.ok).toBe(false);
|
|
979
|
+
expect(result.status).toBe('error');
|
|
980
|
+
expect(result.error).toMatch(/session goal persist is not configured/);
|
|
981
|
+
expect(client.create).not.toHaveBeenCalled();
|
|
982
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
983
|
+
expect(writeObjective).not.toHaveBeenCalled();
|
|
984
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
985
|
+
expect.any(String),
|
|
986
|
+
expect.objectContaining({
|
|
987
|
+
status: 'error',
|
|
988
|
+
error: expect.stringMatching(/session goal persist is not configured/),
|
|
989
|
+
}),
|
|
990
|
+
);
|
|
991
|
+
// Task config is preserved (goalEnabled stays on the task); only this run fails.
|
|
992
|
+
const finalPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
|
|
993
|
+
expect(finalPatch.lastStatus).toBe('error');
|
|
994
|
+
expect(Object.hasOwn(finalPatch, 'lastSessionId')).toBe(false);
|
|
995
|
+
});
|
|
893
996
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
997
|
+
it('goalEnabled with persist writes objective + active goal then prompts', async () => {
|
|
998
|
+
const history = createHistoryStore();
|
|
999
|
+
const client = createSuccessfulClient({ sessionID: 'ses_goal_ok' });
|
|
1000
|
+
const persistSessionGoal = vi.fn(async (_sessionId, _directory, goal) => ({
|
|
1001
|
+
openchamber: { goal },
|
|
1002
|
+
}));
|
|
1003
|
+
const onGoalPersisted = vi.fn();
|
|
897
1004
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1005
|
+
const goalTask = {
|
|
1006
|
+
...scheduledTask,
|
|
1007
|
+
execution: {
|
|
1008
|
+
...scheduledTask.execution,
|
|
1009
|
+
prompt: 'finish the migration',
|
|
1010
|
+
goalEnabled: true,
|
|
1011
|
+
goalTokenBudget: 12_000,
|
|
1012
|
+
},
|
|
1013
|
+
};
|
|
1014
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1015
|
+
task: { ...goalTask, state: { ...goalTask.state, ...state } },
|
|
1016
|
+
}));
|
|
1017
|
+
const runtime = createScheduledTasksRuntime({
|
|
1018
|
+
projectConfigRuntime: {
|
|
1019
|
+
listScheduledTasks: vi.fn(async () => [goalTask]),
|
|
1020
|
+
updateScheduledTaskState,
|
|
1021
|
+
upsertScheduledTask: vi.fn(),
|
|
1022
|
+
},
|
|
1023
|
+
listProjects: vi.fn(async () => [{ id: 'project-1', path: '/tmp/project-1' }]),
|
|
1024
|
+
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
1025
|
+
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
1026
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1027
|
+
persistSessionMetadata: vi.fn(async () => ({})),
|
|
1028
|
+
logger: { info: vi.fn(), warn: vi.fn() },
|
|
1029
|
+
runHistoryStore: history,
|
|
1030
|
+
persistSessionGoal,
|
|
1031
|
+
onGoalPersisted,
|
|
1032
|
+
});
|
|
1033
|
+
await runtime.syncProject('project-1');
|
|
1034
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
907
1035
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1036
|
+
expect(result.ok).toBe(true);
|
|
1037
|
+
expect(client.create).toHaveBeenCalled();
|
|
1038
|
+
expect(writeObjective).toHaveBeenCalledWith('ses_goal_ok', 'finish the migration');
|
|
1039
|
+
expect(persistSessionGoal).toHaveBeenCalledTimes(1);
|
|
1040
|
+
expect(persistSessionGoal.mock.calls[0][0]).toBe('ses_goal_ok');
|
|
1041
|
+
expect(persistSessionGoal.mock.calls[0][1]).toBe('/tmp/project-1');
|
|
1042
|
+
expect(persistSessionGoal.mock.calls[0][2]).toMatchObject({
|
|
1043
|
+
status: 'active',
|
|
1044
|
+
objectiveFile: true,
|
|
1045
|
+
objective: '',
|
|
1046
|
+
tokenBudget: 12_000,
|
|
1047
|
+
tokensUsed: 0,
|
|
1048
|
+
turnsUsed: 0,
|
|
1049
|
+
blockedStreak: 0,
|
|
1050
|
+
note: '',
|
|
1051
|
+
statusReason: '',
|
|
1052
|
+
lastAccountedMessageID: '',
|
|
1053
|
+
});
|
|
1054
|
+
expect(typeof persistSessionGoal.mock.calls[0][2].id).toBe('string');
|
|
1055
|
+
expect(persistSessionGoal.mock.calls[0][2].id.length).toBeGreaterThan(0);
|
|
1056
|
+
expect(onGoalPersisted).toHaveBeenCalledWith(expect.objectContaining({
|
|
1057
|
+
sessionID: 'ses_goal_ok',
|
|
1058
|
+
directory: '/tmp/project-1',
|
|
1059
|
+
metadata: expect.objectContaining({
|
|
1060
|
+
openchamber: expect.objectContaining({
|
|
1061
|
+
goal: expect.objectContaining({ status: 'active' }),
|
|
915
1062
|
}),
|
|
916
|
-
)
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
resolveDistill?.();
|
|
920
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
921
|
-
await Promise.resolve();
|
|
922
|
-
await Promise.resolve();
|
|
923
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
924
|
-
expect(unhandled).toEqual([]);
|
|
925
|
-
} finally {
|
|
926
|
-
process.off('unhandledRejection', onUnhandled);
|
|
927
|
-
resolveDistill?.();
|
|
928
|
-
vi.unstubAllGlobals();
|
|
929
|
-
vi.useRealTimers();
|
|
930
|
-
}
|
|
1063
|
+
}),
|
|
1064
|
+
}));
|
|
1065
|
+
expect(client.prompt).toHaveBeenCalled();
|
|
931
1066
|
});
|
|
932
1067
|
|
|
933
|
-
it('goalEnabled
|
|
1068
|
+
it('goalEnabled persist failure aborts before prompt', async () => {
|
|
934
1069
|
const history = createHistoryStore();
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
if (String(url).includes('/session/ses_goal') && init?.method === 'PATCH') {
|
|
939
|
-
return { ok: true, text: async () => '' };
|
|
940
|
-
}
|
|
941
|
-
if (String(url).includes('prompt_async')) {
|
|
942
|
-
return { ok: true, text: async () => '' };
|
|
943
|
-
}
|
|
944
|
-
throw new Error(`unexpected fetch ${url} ${init?.method}`);
|
|
1070
|
+
const client = createSuccessfulClient({ sessionID: 'ses_goal_fail' });
|
|
1071
|
+
const persistSessionGoal = vi.fn(async () => {
|
|
1072
|
+
throw new Error('store unavailable');
|
|
945
1073
|
});
|
|
946
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
947
1074
|
|
|
948
1075
|
const goalTask = {
|
|
949
1076
|
...scheduledTask,
|
|
@@ -951,7 +1078,6 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
951
1078
|
...scheduledTask.execution,
|
|
952
1079
|
prompt: 'finish the migration',
|
|
953
1080
|
goalEnabled: true,
|
|
954
|
-
goalTokenBudget: 12_000,
|
|
955
1081
|
},
|
|
956
1082
|
};
|
|
957
1083
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
@@ -967,44 +1093,228 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
967
1093
|
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
968
1094
|
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
969
1095
|
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1096
|
+
persistSessionMetadata: vi.fn(async () => ({})),
|
|
970
1097
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
971
1098
|
runHistoryStore: history,
|
|
1099
|
+
persistSessionGoal,
|
|
1100
|
+
});
|
|
1101
|
+
await runtime.syncProject('project-1');
|
|
1102
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
1103
|
+
|
|
1104
|
+
expect(result.ok).toBe(false);
|
|
1105
|
+
expect(result.error).toMatch(/store unavailable/);
|
|
1106
|
+
expect(writeObjective).toHaveBeenCalled();
|
|
1107
|
+
expect(persistSessionGoal).toHaveBeenCalled();
|
|
1108
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('ordinary non-goal runs still create a session and settle on the assistant tail', async () => {
|
|
1112
|
+
const history = createHistoryStore();
|
|
1113
|
+
const client = createSuccessfulClient({ sessionID: 'ses_plain' });
|
|
1114
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1115
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1116
|
+
}));
|
|
1117
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1118
|
+
runHistoryStore: history,
|
|
1119
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
972
1120
|
});
|
|
973
1121
|
await runtime.syncProject('project-1');
|
|
974
1122
|
const result = await runtime.runNow('project-1', 'task-1');
|
|
975
1123
|
|
|
976
1124
|
expect(result.ok).toBe(true);
|
|
977
|
-
expect(client.
|
|
978
|
-
|
|
979
|
-
|
|
1125
|
+
expect(client.create).toHaveBeenCalled();
|
|
1126
|
+
expect(client.prompt).toHaveBeenCalled();
|
|
1127
|
+
expect(writeObjective).not.toHaveBeenCalled();
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it('settles from the latest-side page: >50 older completed + newest error → error', async () => {
|
|
1131
|
+
const history = createHistoryStore();
|
|
1132
|
+
// order=desc page: newest first. Older completed assistants must not win.
|
|
1133
|
+
const latestPage = [
|
|
1134
|
+
erroredAssistant('msg_latest'),
|
|
1135
|
+
...Array.from({ length: 49 }, (_, index) => completedAssistant(`msg_old_${index}`)),
|
|
1136
|
+
];
|
|
1137
|
+
createSuccessfulClient({
|
|
1138
|
+
messageListImpl: async () => ({ data: latestPage }),
|
|
1139
|
+
});
|
|
1140
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1141
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1142
|
+
}));
|
|
1143
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1144
|
+
runHistoryStore: history,
|
|
1145
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1146
|
+
});
|
|
1147
|
+
await runtime.syncProject('project-1');
|
|
1148
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
1149
|
+
|
|
1150
|
+
expect(result.ok).toBe(false);
|
|
1151
|
+
expect(result.status).toBe('error');
|
|
1152
|
+
expect(result.error).toBe('ProviderError');
|
|
1153
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
1154
|
+
expect.any(String),
|
|
1155
|
+
expect.objectContaining({ status: 'error', error: 'ProviderError' }),
|
|
980
1156
|
);
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it('settles from the latest-side page: >50 older errors + newest completed → success', async () => {
|
|
1160
|
+
const history = createHistoryStore();
|
|
1161
|
+
const latestPage = [
|
|
1162
|
+
completedAssistant('msg_latest'),
|
|
1163
|
+
...Array.from({ length: 49 }, (_, index) => erroredAssistant(`msg_old_${index}`)),
|
|
1164
|
+
];
|
|
1165
|
+
createSuccessfulClient({
|
|
1166
|
+
messageListImpl: async () => ({ data: latestPage }),
|
|
988
1167
|
});
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1168
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1169
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1170
|
+
}));
|
|
1171
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1172
|
+
runHistoryStore: history,
|
|
1173
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
992
1174
|
});
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (!body.metadata.openchamber.goal.objectiveFile) {
|
|
996
|
-
expect(body.metadata.openchamber.goal.objective.length).toBeGreaterThan(0);
|
|
997
|
-
}
|
|
998
|
-
expect(client.abort).not.toHaveBeenCalled();
|
|
1175
|
+
await runtime.syncProject('project-1');
|
|
1176
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
999
1177
|
|
|
1000
|
-
|
|
1178
|
+
expect(result.ok).toBe(true);
|
|
1179
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
1180
|
+
expect.any(String),
|
|
1181
|
+
expect.objectContaining({ status: 'success' }),
|
|
1182
|
+
);
|
|
1001
1183
|
});
|
|
1002
1184
|
|
|
1003
|
-
it('
|
|
1185
|
+
it('does not succeed when the latest message is model-switched', async () => {
|
|
1186
|
+
vi.useFakeTimers();
|
|
1187
|
+
const history = createHistoryStore();
|
|
1188
|
+
createSuccessfulClient({
|
|
1189
|
+
messageListImpl: async () => ({
|
|
1190
|
+
data: [{
|
|
1191
|
+
id: 'msg_switch',
|
|
1192
|
+
type: 'model-switched',
|
|
1193
|
+
time: { created: Date.now() },
|
|
1194
|
+
model: { providerID: 'openai', id: 'gpt-4.1' },
|
|
1195
|
+
}],
|
|
1196
|
+
}),
|
|
1197
|
+
});
|
|
1198
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1199
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1200
|
+
}));
|
|
1201
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1202
|
+
runHistoryStore: history,
|
|
1203
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1204
|
+
maxRunDurationMs: 2_500,
|
|
1205
|
+
});
|
|
1206
|
+
await runtime.syncProject('project-1');
|
|
1207
|
+
|
|
1208
|
+
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
1209
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1210
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1211
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
1212
|
+
const result = await runPromise;
|
|
1213
|
+
|
|
1214
|
+
expect(result.ok).toBe(false);
|
|
1215
|
+
expect(result.error).toBe('schedule run timed out');
|
|
1216
|
+
vi.useRealTimers();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it('does not succeed when the assistant finish is tool-calls', async () => {
|
|
1220
|
+
vi.useFakeTimers();
|
|
1221
|
+
const history = createHistoryStore();
|
|
1222
|
+
createSuccessfulClient({
|
|
1223
|
+
messageListImpl: async () => ({
|
|
1224
|
+
data: [{
|
|
1225
|
+
id: 'msg_tools',
|
|
1226
|
+
type: 'assistant',
|
|
1227
|
+
time: { created: 1, completed: 2 },
|
|
1228
|
+
finish: 'tool-calls',
|
|
1229
|
+
}],
|
|
1230
|
+
}),
|
|
1231
|
+
});
|
|
1232
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1233
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1234
|
+
}));
|
|
1235
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1236
|
+
runHistoryStore: history,
|
|
1237
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1238
|
+
maxRunDurationMs: 2_500,
|
|
1239
|
+
});
|
|
1240
|
+
await runtime.syncProject('project-1');
|
|
1241
|
+
|
|
1242
|
+
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
1243
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1244
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1245
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
1246
|
+
const result = await runPromise;
|
|
1247
|
+
|
|
1248
|
+
expect(result.ok).toBe(false);
|
|
1249
|
+
expect(result.error).toBe('schedule run timed out');
|
|
1250
|
+
vi.useRealTimers();
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it('does not succeed after two incomplete assistant idle probes', async () => {
|
|
1254
|
+
vi.useFakeTimers();
|
|
1255
|
+
const history = createHistoryStore();
|
|
1256
|
+
createSuccessfulClient({
|
|
1257
|
+
messageListImpl: async () => ({
|
|
1258
|
+
data: [{
|
|
1259
|
+
id: 'msg_incomplete',
|
|
1260
|
+
type: 'assistant',
|
|
1261
|
+
time: { created: 1 },
|
|
1262
|
+
// no time.completed, no finish
|
|
1263
|
+
}],
|
|
1264
|
+
}),
|
|
1265
|
+
});
|
|
1266
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1267
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1268
|
+
}));
|
|
1269
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1270
|
+
runHistoryStore: history,
|
|
1271
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1272
|
+
maxRunDurationMs: 3_500,
|
|
1273
|
+
});
|
|
1274
|
+
await runtime.syncProject('project-1');
|
|
1275
|
+
|
|
1276
|
+
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
1277
|
+
// Two idle polls with incomplete assistant must not finalize as success.
|
|
1278
|
+
await vi.advanceTimersByTimeAsync(2_000);
|
|
1279
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1280
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
1281
|
+
const result = await runPromise;
|
|
1282
|
+
|
|
1283
|
+
expect(result.ok).toBe(false);
|
|
1284
|
+
expect(result.error).toBe('schedule run timed out');
|
|
1285
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
1286
|
+
expect.any(String),
|
|
1287
|
+
expect.objectContaining({ status: 'error', error: 'schedule run timed out' }),
|
|
1288
|
+
);
|
|
1289
|
+
vi.useRealTimers();
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('lists settlement messages with order=desc (latest-side page)', async () => {
|
|
1293
|
+
const history = createHistoryStore();
|
|
1294
|
+
const client = createSuccessfulClient();
|
|
1295
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1296
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1297
|
+
}));
|
|
1298
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1299
|
+
runHistoryStore: history,
|
|
1300
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1301
|
+
});
|
|
1302
|
+
await runtime.syncProject('project-1');
|
|
1303
|
+
await runtime.runNow('project-1', 'task-1');
|
|
1304
|
+
|
|
1305
|
+
expect(client.messageList).toHaveBeenCalledWith(
|
|
1306
|
+
expect.objectContaining({
|
|
1307
|
+
sessionID: 'ses_1',
|
|
1308
|
+
limit: 50,
|
|
1309
|
+
order: 'desc',
|
|
1310
|
+
}),
|
|
1311
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
1312
|
+
);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
it('successful runs never call session.interrupt', async () => {
|
|
1004
1316
|
const history = createHistoryStore();
|
|
1005
1317
|
const client = createSuccessfulClient();
|
|
1006
|
-
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
|
|
1007
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
1008
1318
|
|
|
1009
1319
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1010
1320
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -1017,9 +1327,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1017
1327
|
const result = await runtime.runNow('project-1', 'task-1');
|
|
1018
1328
|
|
|
1019
1329
|
expect(result.ok).toBe(true);
|
|
1020
|
-
expect(client.
|
|
1021
|
-
|
|
1022
|
-
vi.unstubAllGlobals();
|
|
1330
|
+
expect(client.interrupt).not.toHaveBeenCalled();
|
|
1023
1331
|
});
|
|
1024
1332
|
|
|
1025
1333
|
it('persists lastSessionId when an error run still created a session', async () => {
|
|
@@ -1114,14 +1422,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1114
1422
|
sessionID: 'ses_1',
|
|
1115
1423
|
}));
|
|
1116
1424
|
|
|
1117
|
-
client.
|
|
1118
|
-
data: [
|
|
1119
|
-
info: {
|
|
1120
|
-
id: 'msg_ok',
|
|
1121
|
-
role: 'assistant',
|
|
1122
|
-
time: { completed: Date.now() },
|
|
1123
|
-
},
|
|
1124
|
-
}],
|
|
1425
|
+
client.messageList.mockImplementation(async () => ({
|
|
1426
|
+
data: [completedAssistant('msg_ok')],
|
|
1125
1427
|
}));
|
|
1126
1428
|
|
|
1127
1429
|
await runtime.observeSessionEvent({
|
|
@@ -1165,14 +1467,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1165
1467
|
expect(result.status).toBe('error');
|
|
1166
1468
|
expect(history.finishRun).toHaveBeenCalledTimes(1);
|
|
1167
1469
|
|
|
1168
|
-
client.
|
|
1169
|
-
data: [
|
|
1170
|
-
info: {
|
|
1171
|
-
id: 'msg_ok',
|
|
1172
|
-
role: 'assistant',
|
|
1173
|
-
time: { completed: Date.now() },
|
|
1174
|
-
},
|
|
1175
|
-
}],
|
|
1470
|
+
client.messageList.mockImplementation(async () => ({
|
|
1471
|
+
data: [completedAssistant('msg_ok')],
|
|
1176
1472
|
}));
|
|
1177
1473
|
|
|
1178
1474
|
await runtime.observeSessionEvent({
|
|
@@ -1303,14 +1599,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1303
1599
|
const stateWritesAfterRun = updateScheduledTaskState.mock.calls.length;
|
|
1304
1600
|
const successEmitsAfterRun = emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success').length;
|
|
1305
1601
|
|
|
1306
|
-
client.
|
|
1307
|
-
data: [
|
|
1308
|
-
info: {
|
|
1309
|
-
id: 'msg_ok',
|
|
1310
|
-
role: 'assistant',
|
|
1311
|
-
time: { completed: Date.now() },
|
|
1312
|
-
},
|
|
1313
|
-
}],
|
|
1602
|
+
client.messageList.mockImplementation(async () => ({
|
|
1603
|
+
data: [completedAssistant('msg_ok')],
|
|
1314
1604
|
}));
|
|
1315
1605
|
|
|
1316
1606
|
await runtime.observeSessionEvent({
|
|
@@ -1369,6 +1659,7 @@ describe('scheduled-tasks cross-process slot occupancy', () => {
|
|
|
1369
1659
|
waitForOpenCodeReady: vi.fn(async () => {
|
|
1370
1660
|
throw new Error('OpenCode unavailable');
|
|
1371
1661
|
}),
|
|
1662
|
+
persistSessionMetadata: vi.fn(async () => ({})),
|
|
1372
1663
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
1373
1664
|
runHistoryStore: store,
|
|
1374
1665
|
notifyTaskRun,
|