@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
|
@@ -9,12 +9,14 @@ vi.mock('node:child_process', () => ({
|
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
11
|
const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
|
|
12
|
+
const { createOpenCodeNetworkRuntime } = await import('./network-runtime.js');
|
|
12
13
|
|
|
13
14
|
const originalOpencodeBinary = process.env.OPENCODE_BINARY;
|
|
14
15
|
const originalPath = process.env.PATH;
|
|
15
16
|
|
|
16
17
|
afterEach(() => {
|
|
17
18
|
spawnMock.mockReset();
|
|
19
|
+
vi.useRealTimers();
|
|
18
20
|
vi.unstubAllGlobals();
|
|
19
21
|
if (typeof originalOpencodeBinary === 'string') {
|
|
20
22
|
process.env.OPENCODE_BINARY = originalOpencodeBinary;
|
|
@@ -57,6 +59,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
|
|
|
57
59
|
openCodeApiDetectionTimer: null,
|
|
58
60
|
lastOpenCodeError: null,
|
|
59
61
|
isOpenCodeReady: false,
|
|
62
|
+
v1Migration: null,
|
|
60
63
|
openCodeNotReadySince: 0,
|
|
61
64
|
isExternalOpenCode: false,
|
|
62
65
|
isShuttingDown: false,
|
|
@@ -109,6 +112,53 @@ const createRuntime = (overrides = {}, stateRef = null) => {
|
|
|
109
112
|
});
|
|
110
113
|
};
|
|
111
114
|
|
|
115
|
+
const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
116
|
+
status,
|
|
117
|
+
headers: { 'content-type': 'application/json' },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const DEFAULT_V2_HEALTH = { version: '2.0.12', pid: 1, urls: [], paths: { tmp: '/tmp' } };
|
|
121
|
+
|
|
122
|
+
const stubOpenCodeFetch = (overrides = {}) => {
|
|
123
|
+
const fetchMock = vi.fn(async (url, init) => {
|
|
124
|
+
const href = String(url);
|
|
125
|
+
if (href.includes('/api/experimental/migration/v1')) {
|
|
126
|
+
const migration = overrides.migration;
|
|
127
|
+
if (typeof migration === 'function') {
|
|
128
|
+
return migration(url, init);
|
|
129
|
+
}
|
|
130
|
+
if (migration && typeof migration === 'object' && Number.isInteger(migration.httpStatus)) {
|
|
131
|
+
return jsonResponse(migration.body ?? {}, migration.httpStatus);
|
|
132
|
+
}
|
|
133
|
+
return jsonResponse(migration ?? { status: 'completed' });
|
|
134
|
+
}
|
|
135
|
+
if (href.includes('/api/info') || href.includes('/global/health')) {
|
|
136
|
+
if (overrides.reuseDefaultServe !== true && href.includes(':4096/')) {
|
|
137
|
+
return new Response('not found', { status: 404 });
|
|
138
|
+
}
|
|
139
|
+
const health = overrides.health;
|
|
140
|
+
if (typeof health === 'function') {
|
|
141
|
+
return health(url, init);
|
|
142
|
+
}
|
|
143
|
+
return jsonResponse(health ?? DEFAULT_V2_HEALTH);
|
|
144
|
+
}
|
|
145
|
+
return new Response('not found', { status: 404 });
|
|
146
|
+
});
|
|
147
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
148
|
+
return fetchMock;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const startListeningChild = () => {
|
|
152
|
+
const child = createMockChild();
|
|
153
|
+
spawnMock.mockImplementationOnce(() => {
|
|
154
|
+
queueMicrotask(() => {
|
|
155
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
156
|
+
});
|
|
157
|
+
return child;
|
|
158
|
+
});
|
|
159
|
+
return child;
|
|
160
|
+
};
|
|
161
|
+
|
|
112
162
|
describe('OpenCode lifecycle', () => {
|
|
113
163
|
it('launches managed OpenCode with the managed PATH', async () => {
|
|
114
164
|
delete process.env.OPENCODE_BINARY;
|
|
@@ -133,6 +183,173 @@ describe('OpenCode lifecycle', () => {
|
|
|
133
183
|
await server.close();
|
|
134
184
|
});
|
|
135
185
|
|
|
186
|
+
it('allows a polite SIGTERM close that finishes after 400ms without escalating early', async () => {
|
|
187
|
+
vi.useFakeTimers();
|
|
188
|
+
delete process.env.OPENCODE_BINARY;
|
|
189
|
+
const child = createMockChild();
|
|
190
|
+
child.kill = vi.fn((signal) => {
|
|
191
|
+
if (signal === 'SIGTERM') {
|
|
192
|
+
// Slow-but-healthy flush: exits after 800ms (>400ms), still under 2500ms grace.
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
child.signalCode = 'SIGTERM';
|
|
195
|
+
child.emit('close', null, 'SIGTERM');
|
|
196
|
+
}, 800);
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
spawnMock.mockImplementationOnce(() => {
|
|
201
|
+
queueMicrotask(() => {
|
|
202
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
203
|
+
});
|
|
204
|
+
return child;
|
|
205
|
+
});
|
|
206
|
+
stubOpenCodeFetch();
|
|
207
|
+
|
|
208
|
+
const runtime = createRuntime();
|
|
209
|
+
const server = await runtime.startOpenCode();
|
|
210
|
+
const closePromise = server.close();
|
|
211
|
+
|
|
212
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
213
|
+
expect(child.kill).toHaveBeenCalledWith('SIGTERM');
|
|
214
|
+
expect(child.kill).not.toHaveBeenCalledWith('SIGKILL');
|
|
215
|
+
|
|
216
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
217
|
+
await closePromise;
|
|
218
|
+
expect(child.kill).not.toHaveBeenCalledWith('SIGKILL');
|
|
219
|
+
vi.useRealTimers();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('escalates a hung managed child to SIGKILL only after the full 2500ms SIGTERM grace', async () => {
|
|
223
|
+
vi.useFakeTimers();
|
|
224
|
+
delete process.env.OPENCODE_BINARY;
|
|
225
|
+
const child = createMockChild();
|
|
226
|
+
child.kill = vi.fn((signal) => {
|
|
227
|
+
if (signal === 'SIGKILL') {
|
|
228
|
+
child.signalCode = 'SIGKILL';
|
|
229
|
+
queueMicrotask(() => child.emit('close', null, 'SIGKILL'));
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
});
|
|
233
|
+
spawnMock.mockImplementationOnce(() => {
|
|
234
|
+
queueMicrotask(() => {
|
|
235
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
236
|
+
});
|
|
237
|
+
return child;
|
|
238
|
+
});
|
|
239
|
+
stubOpenCodeFetch();
|
|
240
|
+
|
|
241
|
+
const runtime = createRuntime();
|
|
242
|
+
const server = await runtime.startOpenCode();
|
|
243
|
+
const closePromise = server.close();
|
|
244
|
+
|
|
245
|
+
await vi.advanceTimersByTimeAsync(2499);
|
|
246
|
+
expect(child.kill).toHaveBeenCalledWith('SIGTERM');
|
|
247
|
+
expect(child.kill).not.toHaveBeenCalledWith('SIGKILL');
|
|
248
|
+
|
|
249
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
250
|
+
expect(child.kill).toHaveBeenCalledWith('SIGKILL');
|
|
251
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
252
|
+
await closePromise;
|
|
253
|
+
vi.useRealTimers();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('killProcessOnPort only signals the owned pid and never mass-kills by port', async () => {
|
|
257
|
+
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
|
|
258
|
+
delete process.env.OPENCODE_BINARY;
|
|
259
|
+
const child = createMockChild();
|
|
260
|
+
spawnMock.mockImplementationOnce(() => {
|
|
261
|
+
queueMicrotask(() => {
|
|
262
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
263
|
+
});
|
|
264
|
+
return child;
|
|
265
|
+
});
|
|
266
|
+
stubOpenCodeFetch();
|
|
267
|
+
|
|
268
|
+
const runtime = createRuntime();
|
|
269
|
+
await runtime.startOpenCode();
|
|
270
|
+
killSpy.mockClear();
|
|
271
|
+
|
|
272
|
+
runtime.killProcessOnPort(45678);
|
|
273
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
274
|
+
|
|
275
|
+
runtime.killProcessOnPort(45678, 4242);
|
|
276
|
+
expect(killSpy).toHaveBeenCalledWith(-4242, 'SIGKILL');
|
|
277
|
+
expect(killSpy).toHaveBeenCalledWith(4242, 'SIGKILL');
|
|
278
|
+
expect(killSpy.mock.calls.every(([target]) => target === -4242 || target === 4242)).toBe(true);
|
|
279
|
+
|
|
280
|
+
killSpy.mockRestore();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('parses a v2 server listening line without the opencode prefix', async () => {
|
|
284
|
+
delete process.env.OPENCODE_BINARY;
|
|
285
|
+
const child = createMockChild();
|
|
286
|
+
spawnMock.mockImplementationOnce(() => {
|
|
287
|
+
queueMicrotask(() => {
|
|
288
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
289
|
+
});
|
|
290
|
+
return child;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const runtime = createRuntime();
|
|
294
|
+
const server = await runtime.startOpenCode();
|
|
295
|
+
|
|
296
|
+
expect(server.url).toBe('http://127.0.0.1:45678');
|
|
297
|
+
|
|
298
|
+
await server.close();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('probes /api/info with Basic auth after the managed server is listening', async () => {
|
|
302
|
+
delete process.env.OPENCODE_BINARY;
|
|
303
|
+
const child = createMockChild();
|
|
304
|
+
spawnMock.mockImplementationOnce(() => {
|
|
305
|
+
queueMicrotask(() => {
|
|
306
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
307
|
+
});
|
|
308
|
+
return child;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const fetchMock = vi.fn(async (url) => {
|
|
312
|
+
if (String(url).includes('/api/info')) {
|
|
313
|
+
return new Response(JSON.stringify(DEFAULT_V2_HEALTH), {
|
|
314
|
+
status: 200,
|
|
315
|
+
headers: { 'content-type': 'application/json' },
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (String(url).includes('/api/experimental/migration/v1')) {
|
|
319
|
+
return new Response(JSON.stringify({ status: 'completed' }), {
|
|
320
|
+
status: 200,
|
|
321
|
+
headers: { 'content-type': 'application/json' },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return new Response('not found', { status: 404 });
|
|
325
|
+
});
|
|
326
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
327
|
+
|
|
328
|
+
const authHeaders = { Authorization: 'Basic Zml4dHVyZQ==' };
|
|
329
|
+
const network = createOpenCodeNetworkRuntime({
|
|
330
|
+
state: {
|
|
331
|
+
openCodePort: 45678,
|
|
332
|
+
openCodeBaseUrl: null,
|
|
333
|
+
openCodeApiPrefix: '',
|
|
334
|
+
openCodeApiPrefixDetected: false,
|
|
335
|
+
openCodeApiDetectionTimer: null,
|
|
336
|
+
},
|
|
337
|
+
getOpenCodeAuthHeaders: () => authHeaders,
|
|
338
|
+
});
|
|
339
|
+
const runtime = createRuntime({
|
|
340
|
+
getOpenCodeAuthHeaders: () => authHeaders,
|
|
341
|
+
waitForReady: network.waitForReady,
|
|
342
|
+
});
|
|
343
|
+
const server = await runtime.startOpenCode();
|
|
344
|
+
const healthCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/info'));
|
|
345
|
+
|
|
346
|
+
expect(healthCall).toBeDefined();
|
|
347
|
+
expect(String(healthCall[0])).toContain('/api/info');
|
|
348
|
+
expect(healthCall[1].headers.Authorization).toMatch(/^Basic /);
|
|
349
|
+
|
|
350
|
+
await server.close();
|
|
351
|
+
});
|
|
352
|
+
|
|
136
353
|
it('prepares a fresh managed capability environment and records the spawned child pid', async () => {
|
|
137
354
|
const child = createMockChild();
|
|
138
355
|
spawnMock.mockImplementationOnce(() => {
|
|
@@ -298,7 +515,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
298
515
|
stateRef.current.openCodeProcess = managedChild;
|
|
299
516
|
stateRef.current.openCodePort = 45678;
|
|
300
517
|
stateRef.current.isExternalOpenCode = false;
|
|
301
|
-
|
|
518
|
+
stubOpenCodeFetch();
|
|
302
519
|
await runtime.bootstrapOpenCodeAtStartup();
|
|
303
520
|
expect(managedChild.close).toHaveBeenCalledTimes(1);
|
|
304
521
|
expect(stateRef.current.openCodeProcess).toBeNull();
|
|
@@ -324,7 +541,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
324
541
|
}, stateRef);
|
|
325
542
|
stateRef.current.isExternalOpenCode = true;
|
|
326
543
|
stateRef.current.openCodeBaseUrl = 'http://127.0.0.1:3001';
|
|
327
|
-
|
|
544
|
+
stubOpenCodeFetch();
|
|
328
545
|
|
|
329
546
|
await runtime.bootstrapOpenCodeAtStartup();
|
|
330
547
|
|
|
@@ -367,6 +584,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
367
584
|
clearResolvedOpenCodeBinary,
|
|
368
585
|
}, stateRef);
|
|
369
586
|
const errorLog = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
587
|
+
stubOpenCodeFetch();
|
|
370
588
|
|
|
371
589
|
await runtime.bootstrapOpenCodeAtStartup();
|
|
372
590
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
@@ -378,10 +596,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
378
596
|
});
|
|
379
597
|
return child;
|
|
380
598
|
});
|
|
381
|
-
|
|
382
|
-
status: 200,
|
|
383
|
-
headers: { 'content-type': 'application/json' },
|
|
384
|
-
})));
|
|
599
|
+
stubOpenCodeFetch();
|
|
385
600
|
|
|
386
601
|
try {
|
|
387
602
|
await runtime.retryOpenCodeStartup();
|
|
@@ -393,4 +608,481 @@ describe('OpenCode lifecycle', () => {
|
|
|
393
608
|
await stateRef.current.openCodeProcess?.close();
|
|
394
609
|
}
|
|
395
610
|
});
|
|
611
|
+
|
|
612
|
+
it('does not mark OpenCode ready for messages while V1 migration is required', async () => {
|
|
613
|
+
delete process.env.OPENCODE_BINARY;
|
|
614
|
+
startListeningChild();
|
|
615
|
+
const stateRef = {};
|
|
616
|
+
stubOpenCodeFetch({ migration: { status: 'required' } });
|
|
617
|
+
const runtime = createRuntime({}, stateRef);
|
|
618
|
+
|
|
619
|
+
const server = await runtime.startOpenCode();
|
|
620
|
+
|
|
621
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
622
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
623
|
+
admitTranscript: false,
|
|
624
|
+
phase: 'required',
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
await server.close();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('keeps the ready gate closed while waitForOpenCodeReady sees required migration', async () => {
|
|
631
|
+
const stateRef = {};
|
|
632
|
+
const runtime = createRuntime({}, stateRef);
|
|
633
|
+
stateRef.current.openCodePort = 45678;
|
|
634
|
+
stubOpenCodeFetch({
|
|
635
|
+
migration: { status: 'required' },
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
await expect(runtime.waitForOpenCodeReady(250, 40)).rejects.toThrow(/V1 migration is required/);
|
|
639
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
640
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
641
|
+
admitTranscript: false,
|
|
642
|
+
phase: 'required',
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('marks OpenCode ready for messages after V1 migration completed', async () => {
|
|
647
|
+
delete process.env.OPENCODE_BINARY;
|
|
648
|
+
startListeningChild();
|
|
649
|
+
const stateRef = {};
|
|
650
|
+
stubOpenCodeFetch({ migration: { status: 'completed' } });
|
|
651
|
+
const runtime = createRuntime({}, stateRef);
|
|
652
|
+
|
|
653
|
+
const server = await runtime.startOpenCode();
|
|
654
|
+
|
|
655
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
656
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
657
|
+
admitTranscript: true,
|
|
658
|
+
phase: 'completed',
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await server.close();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('admits messages when V1 migration is absent', async () => {
|
|
665
|
+
const stateRef = {};
|
|
666
|
+
const runtime = createRuntime({}, stateRef);
|
|
667
|
+
stateRef.current.openCodePort = 45678;
|
|
668
|
+
stubOpenCodeFetch({
|
|
669
|
+
migration: { httpStatus: 404, body: {} },
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
await runtime.waitForOpenCodeReady(1000, 40);
|
|
673
|
+
|
|
674
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
675
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
676
|
+
admitTranscript: true,
|
|
677
|
+
phase: 'absent',
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('retries V1 migration error and still blocks messages until completed', async () => {
|
|
682
|
+
const stateRef = {};
|
|
683
|
+
const runtime = createRuntime({}, stateRef);
|
|
684
|
+
stateRef.current.openCodePort = 45678;
|
|
685
|
+
let migrationCalls = 0;
|
|
686
|
+
const fetchMock = stubOpenCodeFetch({
|
|
687
|
+
migration: () => {
|
|
688
|
+
migrationCalls += 1;
|
|
689
|
+
if (migrationCalls === 1) {
|
|
690
|
+
return jsonResponse({ status: 'error', error: 'locked' });
|
|
691
|
+
}
|
|
692
|
+
return jsonResponse({ status: 'completed' });
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
await runtime.waitForOpenCodeReady(2000, 40);
|
|
697
|
+
|
|
698
|
+
expect(migrationCalls).toBeGreaterThan(1);
|
|
699
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/experimental/migration/v1'))).toBe(true);
|
|
700
|
+
expect(fetchMock.mock.calls.every((call) => !call[1] || call[1].method !== 'POST')).toBe(true);
|
|
701
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
702
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
703
|
+
admitTranscript: true,
|
|
704
|
+
phase: 'completed',
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('publishes v1Migration on lifecycle state for health snapshot', async () => {
|
|
709
|
+
const stateRef = {};
|
|
710
|
+
const runtime = createRuntime({}, stateRef);
|
|
711
|
+
stateRef.current.openCodePort = 45678;
|
|
712
|
+
stubOpenCodeFetch({
|
|
713
|
+
migration: {
|
|
714
|
+
status: 'running',
|
|
715
|
+
progress: { label: 'Sessions', numerator: 2, denominator: 8 },
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
await expect(runtime.waitForOpenCodeReady(200, 40)).rejects.toThrow(/V1 migration is running/);
|
|
720
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
721
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
722
|
+
admitTranscript: false,
|
|
723
|
+
phase: 'running',
|
|
724
|
+
progress: { label: 'Sessions', numerator: 2, denominator: 8 },
|
|
725
|
+
});
|
|
726
|
+
expect(stateRef.current.v1Migration.userNotice).toContain('V1 subtasks do not appear in v2');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('clears v1Migration when an external OpenCode restart probe fails', async () => {
|
|
730
|
+
const stateRef = {};
|
|
731
|
+
const runtime = createRuntime({
|
|
732
|
+
env: {
|
|
733
|
+
ENV_CONFIGURED_OPENCODE_PORT: null,
|
|
734
|
+
ENV_CONFIGURED_OPENCODE_HOST: null,
|
|
735
|
+
ENV_EFFECTIVE_PORT: 45678,
|
|
736
|
+
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
|
|
737
|
+
ENV_SKIP_OPENCODE_START: false,
|
|
738
|
+
},
|
|
739
|
+
}, stateRef);
|
|
740
|
+
stateRef.current.isExternalOpenCode = true;
|
|
741
|
+
stateRef.current.openCodePort = 45678;
|
|
742
|
+
stateRef.current.openCodeBaseUrl = 'http://127.0.0.1:45678';
|
|
743
|
+
stateRef.current.v1Migration = { admitTranscript: true, phase: 'completed' };
|
|
744
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ healthy: false }), {
|
|
745
|
+
status: 200,
|
|
746
|
+
headers: { 'content-type': 'application/json' },
|
|
747
|
+
})));
|
|
748
|
+
|
|
749
|
+
await expect(runtime.restartOpenCode()).rejects.toThrow('External OpenCode server on port 45678 is not responding');
|
|
750
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
751
|
+
expect(stateRef.current.v1Migration).toBeNull();
|
|
752
|
+
// Read-only external mount: failed re-probe keeps port + ownership; never spawn.
|
|
753
|
+
expect(stateRef.current.openCodePort).toBe(45678);
|
|
754
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
755
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('rejects healthy 1.x health bodies instead of admitting the process', async () => {
|
|
759
|
+
const stateRef = {};
|
|
760
|
+
const runtime = createRuntime({}, stateRef);
|
|
761
|
+
stateRef.current.openCodePort = 45678;
|
|
762
|
+
stubOpenCodeFetch({ health: { healthy: true, version: '1.15.0' } });
|
|
763
|
+
|
|
764
|
+
await expect(runtime.waitForOpenCodeReady(300, 40)).rejects.toThrow(/unhealthy|Timed out|1\.|version/i);
|
|
765
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('rejects healthy responses with a missing version', async () => {
|
|
769
|
+
const stateRef = {};
|
|
770
|
+
const runtime = createRuntime({}, stateRef);
|
|
771
|
+
stateRef.current.openCodePort = 45678;
|
|
772
|
+
stubOpenCodeFetch({ health: { healthy: true } });
|
|
773
|
+
|
|
774
|
+
await expect(runtime.waitForOpenCodeReady(300, 40)).rejects.toThrow();
|
|
775
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('health version + migration matrix: v2+completed admits, v2+running blocks, 1.x blocks', async () => {
|
|
779
|
+
const cases = [
|
|
780
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'completed' }, admit: true },
|
|
781
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'running' }, admit: false },
|
|
782
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'required' }, admit: false },
|
|
783
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'error', error: 'disk' }, admit: false },
|
|
784
|
+
{ health: { healthy: true, version: '1.18.18' }, migration: { status: 'completed' }, admit: false },
|
|
785
|
+
];
|
|
786
|
+
|
|
787
|
+
for (const entry of cases) {
|
|
788
|
+
const stateRef = {};
|
|
789
|
+
const runtime = createRuntime({}, stateRef);
|
|
790
|
+
stateRef.current.openCodePort = 45678;
|
|
791
|
+
stubOpenCodeFetch({ health: entry.health, migration: entry.migration });
|
|
792
|
+
if (entry.admit) {
|
|
793
|
+
await runtime.waitForOpenCodeReady(1000, 40);
|
|
794
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
795
|
+
} else {
|
|
796
|
+
await expect(runtime.waitForOpenCodeReady(250, 40)).rejects.toThrow();
|
|
797
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('aborts a hung migration probe with the same per-attempt signal after health', async () => {
|
|
803
|
+
const stateRef = {};
|
|
804
|
+
const runtime = createRuntime({}, stateRef);
|
|
805
|
+
stateRef.current.openCodePort = 45678;
|
|
806
|
+
let migrationStarted = 0;
|
|
807
|
+
let sawAbort = false;
|
|
808
|
+
stubOpenCodeFetch({
|
|
809
|
+
health: DEFAULT_V2_HEALTH,
|
|
810
|
+
migration: (_url, init) => {
|
|
811
|
+
migrationStarted += 1;
|
|
812
|
+
return new Promise((_resolve, reject) => {
|
|
813
|
+
const signal = init?.signal;
|
|
814
|
+
if (!signal) {
|
|
815
|
+
reject(new Error('migration probe missing abort signal'));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (signal.aborted) {
|
|
819
|
+
sawAbort = true;
|
|
820
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
signal.addEventListener('abort', () => {
|
|
824
|
+
sawAbort = true;
|
|
825
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
826
|
+
}, { once: true });
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Default per-attempt health timeout is 5s; outer deadline must exceed it once
|
|
832
|
+
// so the hung migration is aborted by the attempt timer, not only the loop exit.
|
|
833
|
+
const started = Date.now();
|
|
834
|
+
await expect(runtime.waitForOpenCodeReady(6500, 50)).rejects.toThrow();
|
|
835
|
+
const elapsed = Date.now() - started;
|
|
836
|
+
expect(migrationStarted).toBeGreaterThan(0);
|
|
837
|
+
expect(sawAbort).toBe(true);
|
|
838
|
+
expect(elapsed).toBeLessThan(12_000);
|
|
839
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
840
|
+
}, 15_000);
|
|
841
|
+
|
|
842
|
+
it('revokes execution permit on restart and ignores stale async health for a new generation', async () => {
|
|
843
|
+
const stateRef = {};
|
|
844
|
+
const runtime = createRuntime({}, stateRef);
|
|
845
|
+
stateRef.current.openCodePort = 45678;
|
|
846
|
+
stateRef.current.runtimeContractGeneration = 1;
|
|
847
|
+
stateRef.current.runtimeContract = {
|
|
848
|
+
executionAllowed: true,
|
|
849
|
+
phase: 'ready',
|
|
850
|
+
serveVersion: '2.0.12',
|
|
851
|
+
instanceGeneration: 1,
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const genBefore = runtime.revokeRuntimeExecutionPermit('test-restart');
|
|
855
|
+
expect(genBefore).toBe(2);
|
|
856
|
+
expect(stateRef.current.runtimeContract.executionAllowed).toBe(false);
|
|
857
|
+
expect(stateRef.current.runtimeContract.phase).toBe('pending');
|
|
858
|
+
expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
|
|
859
|
+
|
|
860
|
+
// Stale probe from generation 1 must not re-open execution on generation 2.
|
|
861
|
+
runtime.refreshRuntimeContractFromProbe(
|
|
862
|
+
{
|
|
863
|
+
ok: true,
|
|
864
|
+
version: '2.0.12',
|
|
865
|
+
authenticated: true,
|
|
866
|
+
healthOk: true,
|
|
867
|
+
},
|
|
868
|
+
{ admitTranscript: true, phase: 'done', error: null },
|
|
869
|
+
1,
|
|
870
|
+
);
|
|
871
|
+
expect(stateRef.current.runtimeContract.executionAllowed).toBe(false);
|
|
872
|
+
expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
|
|
873
|
+
|
|
874
|
+
// Matching generation publishes verified permit.
|
|
875
|
+
runtime.refreshRuntimeContractFromProbe(
|
|
876
|
+
{
|
|
877
|
+
ok: true,
|
|
878
|
+
version: '2.0.12',
|
|
879
|
+
authenticated: true,
|
|
880
|
+
healthOk: true,
|
|
881
|
+
},
|
|
882
|
+
{ admitTranscript: true, phase: 'done', error: null },
|
|
883
|
+
2,
|
|
884
|
+
);
|
|
885
|
+
expect(stateRef.current.runtimeContract.executionAllowed).toBe(true);
|
|
886
|
+
expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
|
|
887
|
+
expect(stateRef.current.runtimeContract.serveVersion).toBe('2.0.12');
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('does not mark external skip-start ready before version and migration admit', async () => {
|
|
891
|
+
const stateRef = {};
|
|
892
|
+
const runtime = createRuntime({
|
|
893
|
+
env: {
|
|
894
|
+
ENV_CONFIGURED_OPENCODE_PORT: null,
|
|
895
|
+
ENV_CONFIGURED_OPENCODE_HOST: { origin: 'http://127.0.0.1:3001' },
|
|
896
|
+
ENV_EFFECTIVE_PORT: 3001,
|
|
897
|
+
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
|
|
898
|
+
ENV_SKIP_OPENCODE_START: true,
|
|
899
|
+
},
|
|
900
|
+
syncFromHmrState: vi.fn(),
|
|
901
|
+
}, stateRef);
|
|
902
|
+
// Reject health so bootstrap's waitForOpenCodeReady fails quickly without a
|
|
903
|
+
// 20s migration spin, while still proving skip-start never pre-admits ready.
|
|
904
|
+
stubOpenCodeFetch({ health: { healthy: true, version: '1.15.0' } });
|
|
905
|
+
|
|
906
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
907
|
+
|
|
908
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
909
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
910
|
+
expect(stateRef.current.openCodePort).toBe(3001);
|
|
911
|
+
}, 25_000);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
describe('shared official OpenCode service', () => {
|
|
915
|
+
const UNCONFIGURED_ENV = {
|
|
916
|
+
ENV_CONFIGURED_OPENCODE_PORT: null,
|
|
917
|
+
ENV_CONFIGURED_OPENCODE_HOST: null,
|
|
918
|
+
ENV_EFFECTIVE_PORT: null,
|
|
919
|
+
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
|
|
920
|
+
ENV_SKIP_OPENCODE_START: false,
|
|
921
|
+
};
|
|
922
|
+
const REGISTRATION = {
|
|
923
|
+
origin: 'http://127.0.0.1:49374',
|
|
924
|
+
port: 49374,
|
|
925
|
+
pid: process.pid,
|
|
926
|
+
password: 'service-password',
|
|
927
|
+
version: '2.0.12',
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const createSharedService = (overrides = {}) => ({
|
|
931
|
+
registrationFile: '/state/opencode/service.json',
|
|
932
|
+
buildStartEnv: (env) => {
|
|
933
|
+
const { OPENCODE_SERVER_PASSWORD: _password, ...rest } = env;
|
|
934
|
+
return rest;
|
|
935
|
+
},
|
|
936
|
+
start: vi.fn(async () => {}),
|
|
937
|
+
readRegistration: vi.fn(async () => REGISTRATION),
|
|
938
|
+
...overrides,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
const stubSharedFetch = () => {
|
|
942
|
+
const base = stubOpenCodeFetch();
|
|
943
|
+
const fetchMock = vi.fn(async (url, init) => (String(url).includes('/api/location/reload')
|
|
944
|
+
? new Response(null, { status: 204 })
|
|
945
|
+
: base(url, init)));
|
|
946
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
947
|
+
return fetchMock;
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
it('attaches to the registered service instead of spawning a private serve', async () => {
|
|
951
|
+
process.env.OPENCODE_BINARY = '/mock/opencode';
|
|
952
|
+
const sharedService = createSharedService();
|
|
953
|
+
const adoptOpenCodeServerPassword = vi.fn();
|
|
954
|
+
const stateRef = {};
|
|
955
|
+
const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword }, stateRef);
|
|
956
|
+
const fetchMock = stubSharedFetch();
|
|
957
|
+
|
|
958
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
959
|
+
|
|
960
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
961
|
+
expect(sharedService.start).toHaveBeenCalledTimes(1);
|
|
962
|
+
const startInput = sharedService.start.mock.calls[0][0];
|
|
963
|
+
expect(startInput.binary).toBe('/mock/opencode');
|
|
964
|
+
expect(startInput.env.OPENCODE_SERVER_PASSWORD).toBeUndefined();
|
|
965
|
+
expect(adoptOpenCodeServerPassword).toHaveBeenCalledWith('service-password', 'shared-service');
|
|
966
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
967
|
+
expect(stateRef.current.openCodeBaseUrl).toBe('http://127.0.0.1:49374');
|
|
968
|
+
expect(stateRef.current.openCodePort).toBe(49374);
|
|
969
|
+
expect(runtime.isSharedOpenCodeService()).toBe(true);
|
|
970
|
+
// The leftover private serve on 4096 is never probed once the service wins.
|
|
971
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes(':4096/'))).toBe(false);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('reports shared startup failure without probing or spawning a private serve, and can retry', async () => {
|
|
975
|
+
process.env.OPENCODE_BINARY = '/mock/opencode';
|
|
976
|
+
const sharedService = createSharedService({ start: vi.fn(async () => { throw new Error('boom'); }) });
|
|
977
|
+
const adoptOpenCodeServerPassword = vi.fn();
|
|
978
|
+
const stateRef = {};
|
|
979
|
+
const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword }, stateRef);
|
|
980
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
981
|
+
const fetchMock = stubSharedFetch();
|
|
982
|
+
|
|
983
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
984
|
+
|
|
985
|
+
expect(adoptOpenCodeServerPassword).toHaveBeenLastCalledWith(null, null);
|
|
986
|
+
expect(runtime.isSharedOpenCodeService()).toBe(false);
|
|
987
|
+
expect(stateRef.current.isExternalOpenCode).toBe(false);
|
|
988
|
+
expect(stateRef.current.openCodeBaseUrl).toBeNull();
|
|
989
|
+
expect(stateRef.current.openCodeProcess).toBeNull();
|
|
990
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
991
|
+
expect(stateRef.current.lastOpenCodeError).toBe('boom');
|
|
992
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
993
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
994
|
+
|
|
995
|
+
sharedService.start.mockResolvedValue(undefined);
|
|
996
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
997
|
+
expect(runtime.isSharedOpenCodeService()).toBe(true);
|
|
998
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('reconnects using updated registration without replacing the service', async () => {
|
|
1002
|
+
const sharedService = createSharedService();
|
|
1003
|
+
const adoptOpenCodeServerPassword = vi.fn();
|
|
1004
|
+
const stateRef = {};
|
|
1005
|
+
const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword }, stateRef);
|
|
1006
|
+
stubSharedFetch();
|
|
1007
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
1008
|
+
sharedService.readRegistration.mockResolvedValue({ ...REGISTRATION, port: 49375, origin: 'http://127.0.0.1:49375', password: 'rotated' });
|
|
1009
|
+
|
|
1010
|
+
await runtime.restartOpenCode();
|
|
1011
|
+
|
|
1012
|
+
expect(sharedService.start.mock.lastCall[0].replace).toBe(false);
|
|
1013
|
+
expect(stateRef.current.openCodePort).toBe(49375);
|
|
1014
|
+
expect(adoptOpenCodeServerPassword).toHaveBeenLastCalledWith('rotated', 'shared-service');
|
|
1015
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it('reattaches shared ownership across HMR and retires an old private child', async () => {
|
|
1019
|
+
const sharedService = createSharedService();
|
|
1020
|
+
const stateRef = {};
|
|
1021
|
+
const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService }, stateRef);
|
|
1022
|
+
const close = vi.fn();
|
|
1023
|
+
stateRef.current.openCodeProcess = { pid: 12345, close };
|
|
1024
|
+
stateRef.current.openCodePort = 4096;
|
|
1025
|
+
stubSharedFetch();
|
|
1026
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
1027
|
+
expect(close).toHaveBeenCalledOnce();
|
|
1028
|
+
expect(stateRef.current.openCodeProcess).toBeNull();
|
|
1029
|
+
expect(runtime.isSharedOpenCodeService()).toBe(true);
|
|
1030
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
1031
|
+
expect(runtime.isSharedOpenCodeService()).toBe(true);
|
|
1032
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
1033
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('uses the explicit upgrade binary without settings rediscovery and propagates replacement failures', async () => {
|
|
1037
|
+
const sharedService = createSharedService();
|
|
1038
|
+
const applyOpencodeBinaryFromSettings = vi.fn();
|
|
1039
|
+
const stateRef = {};
|
|
1040
|
+
const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, applyOpencodeBinaryFromSettings }, stateRef);
|
|
1041
|
+
stubSharedFetch();
|
|
1042
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
1043
|
+
applyOpencodeBinaryFromSettings.mockClear();
|
|
1044
|
+
|
|
1045
|
+
await runtime.restartOpenCode({ binaryPath: '/cache/opencode-cli/2.0.14/opencode' });
|
|
1046
|
+
|
|
1047
|
+
expect(sharedService.start.mock.lastCall[0]).toMatchObject({ binary: '/cache/opencode-cli/2.0.14/opencode', replace: true });
|
|
1048
|
+
expect(applyOpencodeBinaryFromSettings).not.toHaveBeenCalled();
|
|
1049
|
+
sharedService.start.mockRejectedValue(new Error('replacement failed'));
|
|
1050
|
+
await expect(runtime.restartOpenCode({ binaryPath: '/cache/opencode-cli/2.0.14/opencode' })).rejects.toThrow('replacement failed');
|
|
1051
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
1052
|
+
expect(stateRef.current.runtimeContract.executionAllowed).toBe(false);
|
|
1053
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('reloads config changes in place instead of restarting the shared service', async () => {
|
|
1057
|
+
process.env.OPENCODE_BINARY = '/mock/opencode';
|
|
1058
|
+
const sharedService = createSharedService();
|
|
1059
|
+
const stateRef = {};
|
|
1060
|
+
const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword: vi.fn() }, stateRef);
|
|
1061
|
+
const fetchMock = stubSharedFetch();
|
|
1062
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
1063
|
+
fetchMock.mockClear();
|
|
1064
|
+
|
|
1065
|
+
const result = await runtime.refreshOpenCodeAfterConfigChange('plugin add');
|
|
1066
|
+
|
|
1067
|
+
expect(result).toEqual({ reloaded: true, external: true });
|
|
1068
|
+
const reloadCalls = fetchMock.mock.calls.filter(([url]) => String(url).includes('/api/location/reload'));
|
|
1069
|
+
expect(reloadCalls).toHaveLength(1);
|
|
1070
|
+
expect(reloadCalls[0][1]?.method).toBe('POST');
|
|
1071
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
1072
|
+
expect(sharedService.start).toHaveBeenCalledTimes(1);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
it('keeps an explicitly configured OpenCode port instead of the shared service', async () => {
|
|
1076
|
+
const sharedService = createSharedService();
|
|
1077
|
+
const stateRef = {};
|
|
1078
|
+
const runtime = createRuntime({ sharedService, adoptOpenCodeServerPassword: vi.fn() }, stateRef);
|
|
1079
|
+
stubSharedFetch();
|
|
1080
|
+
startListeningChild();
|
|
1081
|
+
|
|
1082
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
1083
|
+
|
|
1084
|
+
expect(sharedService.start).not.toHaveBeenCalled();
|
|
1085
|
+
expect(runtime.isSharedOpenCodeService()).toBe(false);
|
|
1086
|
+
await stateRef.current.openCodeProcess?.close();
|
|
1087
|
+
});
|
|
396
1088
|
});
|