@openchambery/web 1.19.21-beta.1 → 2.0.0-beta.1
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 -11
- 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 +165 -0
- package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
- package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
- package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
- package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
- package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
- package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
- package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
- package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
- package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
- package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
- package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
- package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
- package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
- package/dist/assets/es-c52LTHHe.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
- package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
- package/dist/assets/gitApi-ButwzQ0N.js +9 -0
- package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
- package/dist/assets/ja-C8Ikun3Z.js +15 -0
- package/dist/assets/ko-ClFys5o2.js +15 -0
- package/dist/assets/main-CCE_4y5F.js +2 -0
- package/dist/assets/main-Dn9rpsgl.js +169 -0
- package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
- package/dist/assets/miniChat-BpjxVqgg.js +2 -0
- package/dist/assets/mobile-nMJCaNq1.js +2 -0
- package/dist/assets/multirun-BvSG94mW.js +4 -0
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/projectMeta-DOe49dM_.js +3 -0
- package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
- package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
- package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
- package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
- package/dist/assets/src-BzX5UI-c.css +2 -0
- package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
- package/dist/assets/textarea-3XR8sUfy.js +1 -0
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
- package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
- package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-20MYEjha.js +15 -0
- package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +2 -2
- package/server/index.js +107 -0
- package/server/lib/assistants/DOCUMENTATION.md +18 -18
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/projection-history.test.js +207 -0
- package/server/lib/assistants/routes.js +5 -5
- package/server/lib/assistants/service.js +493 -123
- package/server/lib/assistants/service.test.js +203 -133
- package/server/lib/conversations/DOCUMENTATION.md +4 -4
- package/server/lib/conversations/service.js +86 -57
- package/server/lib/conversations/service.test.js +116 -100
- package/server/lib/event-stream/global-hub.js +2 -1
- package/server/lib/llm/DOCUMENTATION.md +71 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +37 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +421 -450
- package/server/lib/llm/generate.test.js +553 -487
- package/server/lib/llm/routes.js +6 -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.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/opencode/DOCUMENTATION.md +28 -5
- package/server/lib/opencode/config-entity-routes.js +84 -13
- package/server/lib/opencode/config-entity-routes.test.js +41 -10
- package/server/lib/opencode/ensure-cli.js +239 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +193 -110
- package/server/lib/opencode/env-runtime.test.js +221 -17
- package/server/lib/opencode/feature-routes-runtime.js +37 -1
- package/server/lib/opencode/lifecycle.js +204 -70
- package/server/lib/opencode/lifecycle.test.js +376 -6
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +87 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/proxy.js +153 -16
- package/server/lib/opencode/proxy.test.js +120 -0
- package/server/lib/opencode/routes.js +25 -32
- package/server/lib/opencode/routes.test.js +51 -52
- package/server/lib/opencode/server-utils-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +6 -7
- package/server/lib/opencode/skill-routes.test.js +36 -5
- 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 +49 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -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 +331 -308
- package/server/lib/scheduled-tasks/runtime.test.js +530 -318
- package/server/lib/session-goal/DOCUMENTATION.md +65 -14
- 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 +231 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +46 -4
- package/server/lib/session-goal/runtime.test.js +62 -1
- package/server/lib/session-index/sync-runtime.js +20 -17
- package/server/lib/session-index/sync-runtime.test.js +9 -8
- package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
- package/server/lib/session-metadata/routes.js +94 -0
- package/server/lib/session-metadata/routes.test.js +111 -0
- package/server/lib/session-metadata/session-metadata-store.js +235 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
- 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 +204 -71
- package/server/lib/session-turn-pages/routes.test.js +285 -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/small-model/DOCUMENTATION.md +5 -4
- package/server/lib/small-model/catalog.js +56 -6
- package/server/lib/small-model/catalog.test.js +47 -0
- package/server/lib/small-model/index.js +3 -3
- package/server/lib/small-model/opencode-session.js +50 -206
- package/server/lib/small-model/opencode-session.test.js +58 -88
- package/server/opencode-proxy.test.js +61 -12
- package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
- package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
- package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
- package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
- package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CcIuekQ3.js +0 -7
- package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
- package/dist/assets/ja-Bgr8-dOg.js +0 -15
- package/dist/assets/ko-B_Gxh-0l.js +0 -15
- package/dist/assets/main-CFLmpNC5.js +0 -169
- package/dist/assets/main-Ckj_IpUy.js +0 -2
- package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
- package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
- package/dist/assets/multirun-BGUKIJys.js +0 -4
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
- package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
- package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
- package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
|
@@ -9,6 +9,7 @@ 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;
|
|
@@ -57,6 +58,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
|
|
|
57
58
|
openCodeApiDetectionTimer: null,
|
|
58
59
|
lastOpenCodeError: null,
|
|
59
60
|
isOpenCodeReady: false,
|
|
61
|
+
v1Migration: null,
|
|
60
62
|
openCodeNotReadySince: 0,
|
|
61
63
|
isExternalOpenCode: false,
|
|
62
64
|
isShuttingDown: false,
|
|
@@ -109,6 +111,53 @@ const createRuntime = (overrides = {}, stateRef = null) => {
|
|
|
109
111
|
});
|
|
110
112
|
};
|
|
111
113
|
|
|
114
|
+
const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
115
|
+
status,
|
|
116
|
+
headers: { 'content-type': 'application/json' },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const DEFAULT_V2_HEALTH = { version: '2.0.12', pid: 1, urls: [], paths: { tmp: '/tmp' } };
|
|
120
|
+
|
|
121
|
+
const stubOpenCodeFetch = (overrides = {}) => {
|
|
122
|
+
const fetchMock = vi.fn(async (url, init) => {
|
|
123
|
+
const href = String(url);
|
|
124
|
+
if (href.includes('/api/experimental/migration/v1')) {
|
|
125
|
+
const migration = overrides.migration;
|
|
126
|
+
if (typeof migration === 'function') {
|
|
127
|
+
return migration(url, init);
|
|
128
|
+
}
|
|
129
|
+
if (migration && typeof migration === 'object' && Number.isInteger(migration.httpStatus)) {
|
|
130
|
+
return jsonResponse(migration.body ?? {}, migration.httpStatus);
|
|
131
|
+
}
|
|
132
|
+
return jsonResponse(migration ?? { status: 'completed' });
|
|
133
|
+
}
|
|
134
|
+
if (href.includes('/api/info') || href.includes('/global/health')) {
|
|
135
|
+
if (overrides.reuseDefaultServe !== true && href.includes(':4096/')) {
|
|
136
|
+
return new Response('not found', { status: 404 });
|
|
137
|
+
}
|
|
138
|
+
const health = overrides.health;
|
|
139
|
+
if (typeof health === 'function') {
|
|
140
|
+
return health(url, init);
|
|
141
|
+
}
|
|
142
|
+
return jsonResponse(health ?? DEFAULT_V2_HEALTH);
|
|
143
|
+
}
|
|
144
|
+
return new Response('not found', { status: 404 });
|
|
145
|
+
});
|
|
146
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
147
|
+
return fetchMock;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const startListeningChild = () => {
|
|
151
|
+
const child = createMockChild();
|
|
152
|
+
spawnMock.mockImplementationOnce(() => {
|
|
153
|
+
queueMicrotask(() => {
|
|
154
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
155
|
+
});
|
|
156
|
+
return child;
|
|
157
|
+
});
|
|
158
|
+
return child;
|
|
159
|
+
};
|
|
160
|
+
|
|
112
161
|
describe('OpenCode lifecycle', () => {
|
|
113
162
|
it('launches managed OpenCode with the managed PATH', async () => {
|
|
114
163
|
delete process.env.OPENCODE_BINARY;
|
|
@@ -133,6 +182,76 @@ describe('OpenCode lifecycle', () => {
|
|
|
133
182
|
await server.close();
|
|
134
183
|
});
|
|
135
184
|
|
|
185
|
+
it('parses a v2 server listening line without the opencode prefix', async () => {
|
|
186
|
+
delete process.env.OPENCODE_BINARY;
|
|
187
|
+
const child = createMockChild();
|
|
188
|
+
spawnMock.mockImplementationOnce(() => {
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
191
|
+
});
|
|
192
|
+
return child;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const runtime = createRuntime();
|
|
196
|
+
const server = await runtime.startOpenCode();
|
|
197
|
+
|
|
198
|
+
expect(server.url).toBe('http://127.0.0.1:45678');
|
|
199
|
+
|
|
200
|
+
await server.close();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('probes /api/info with Basic auth after the managed server is listening', async () => {
|
|
204
|
+
delete process.env.OPENCODE_BINARY;
|
|
205
|
+
const child = createMockChild();
|
|
206
|
+
spawnMock.mockImplementationOnce(() => {
|
|
207
|
+
queueMicrotask(() => {
|
|
208
|
+
child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
|
|
209
|
+
});
|
|
210
|
+
return child;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const fetchMock = vi.fn(async (url) => {
|
|
214
|
+
if (String(url).includes('/api/info')) {
|
|
215
|
+
return new Response(JSON.stringify(DEFAULT_V2_HEALTH), {
|
|
216
|
+
status: 200,
|
|
217
|
+
headers: { 'content-type': 'application/json' },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (String(url).includes('/api/experimental/migration/v1')) {
|
|
221
|
+
return new Response(JSON.stringify({ status: 'completed' }), {
|
|
222
|
+
status: 200,
|
|
223
|
+
headers: { 'content-type': 'application/json' },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return new Response('not found', { status: 404 });
|
|
227
|
+
});
|
|
228
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
229
|
+
|
|
230
|
+
const authHeaders = { Authorization: 'Basic Zml4dHVyZQ==' };
|
|
231
|
+
const network = createOpenCodeNetworkRuntime({
|
|
232
|
+
state: {
|
|
233
|
+
openCodePort: 45678,
|
|
234
|
+
openCodeBaseUrl: null,
|
|
235
|
+
openCodeApiPrefix: '',
|
|
236
|
+
openCodeApiPrefixDetected: false,
|
|
237
|
+
openCodeApiDetectionTimer: null,
|
|
238
|
+
},
|
|
239
|
+
getOpenCodeAuthHeaders: () => authHeaders,
|
|
240
|
+
});
|
|
241
|
+
const runtime = createRuntime({
|
|
242
|
+
getOpenCodeAuthHeaders: () => authHeaders,
|
|
243
|
+
waitForReady: network.waitForReady,
|
|
244
|
+
});
|
|
245
|
+
const server = await runtime.startOpenCode();
|
|
246
|
+
const healthCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/info'));
|
|
247
|
+
|
|
248
|
+
expect(healthCall).toBeDefined();
|
|
249
|
+
expect(String(healthCall[0])).toContain('/api/info');
|
|
250
|
+
expect(healthCall[1].headers.Authorization).toMatch(/^Basic /);
|
|
251
|
+
|
|
252
|
+
await server.close();
|
|
253
|
+
});
|
|
254
|
+
|
|
136
255
|
it('prepares a fresh managed capability environment and records the spawned child pid', async () => {
|
|
137
256
|
const child = createMockChild();
|
|
138
257
|
spawnMock.mockImplementationOnce(() => {
|
|
@@ -298,7 +417,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
298
417
|
stateRef.current.openCodeProcess = managedChild;
|
|
299
418
|
stateRef.current.openCodePort = 45678;
|
|
300
419
|
stateRef.current.isExternalOpenCode = false;
|
|
301
|
-
|
|
420
|
+
stubOpenCodeFetch();
|
|
302
421
|
await runtime.bootstrapOpenCodeAtStartup();
|
|
303
422
|
expect(managedChild.close).toHaveBeenCalledTimes(1);
|
|
304
423
|
expect(stateRef.current.openCodeProcess).toBeNull();
|
|
@@ -324,7 +443,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
324
443
|
}, stateRef);
|
|
325
444
|
stateRef.current.isExternalOpenCode = true;
|
|
326
445
|
stateRef.current.openCodeBaseUrl = 'http://127.0.0.1:3001';
|
|
327
|
-
|
|
446
|
+
stubOpenCodeFetch();
|
|
328
447
|
|
|
329
448
|
await runtime.bootstrapOpenCodeAtStartup();
|
|
330
449
|
|
|
@@ -367,6 +486,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
367
486
|
clearResolvedOpenCodeBinary,
|
|
368
487
|
}, stateRef);
|
|
369
488
|
const errorLog = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
489
|
+
stubOpenCodeFetch();
|
|
370
490
|
|
|
371
491
|
await runtime.bootstrapOpenCodeAtStartup();
|
|
372
492
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
@@ -378,10 +498,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
378
498
|
});
|
|
379
499
|
return child;
|
|
380
500
|
});
|
|
381
|
-
|
|
382
|
-
status: 200,
|
|
383
|
-
headers: { 'content-type': 'application/json' },
|
|
384
|
-
})));
|
|
501
|
+
stubOpenCodeFetch();
|
|
385
502
|
|
|
386
503
|
try {
|
|
387
504
|
await runtime.retryOpenCodeStartup();
|
|
@@ -393,4 +510,257 @@ describe('OpenCode lifecycle', () => {
|
|
|
393
510
|
await stateRef.current.openCodeProcess?.close();
|
|
394
511
|
}
|
|
395
512
|
});
|
|
513
|
+
|
|
514
|
+
it('does not mark OpenCode ready for messages while V1 migration is required', async () => {
|
|
515
|
+
delete process.env.OPENCODE_BINARY;
|
|
516
|
+
startListeningChild();
|
|
517
|
+
const stateRef = {};
|
|
518
|
+
stubOpenCodeFetch({ migration: { status: 'required' } });
|
|
519
|
+
const runtime = createRuntime({}, stateRef);
|
|
520
|
+
|
|
521
|
+
const server = await runtime.startOpenCode();
|
|
522
|
+
|
|
523
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
524
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
525
|
+
admitTranscript: false,
|
|
526
|
+
phase: 'required',
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
await server.close();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('keeps the ready gate closed while waitForOpenCodeReady sees required migration', async () => {
|
|
533
|
+
const stateRef = {};
|
|
534
|
+
const runtime = createRuntime({}, stateRef);
|
|
535
|
+
stateRef.current.openCodePort = 45678;
|
|
536
|
+
stubOpenCodeFetch({
|
|
537
|
+
migration: { status: 'required' },
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await expect(runtime.waitForOpenCodeReady(250, 40)).rejects.toThrow(/V1 migration is required/);
|
|
541
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
542
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
543
|
+
admitTranscript: false,
|
|
544
|
+
phase: 'required',
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('marks OpenCode ready for messages after V1 migration completed', async () => {
|
|
549
|
+
delete process.env.OPENCODE_BINARY;
|
|
550
|
+
startListeningChild();
|
|
551
|
+
const stateRef = {};
|
|
552
|
+
stubOpenCodeFetch({ migration: { status: 'completed' } });
|
|
553
|
+
const runtime = createRuntime({}, stateRef);
|
|
554
|
+
|
|
555
|
+
const server = await runtime.startOpenCode();
|
|
556
|
+
|
|
557
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
558
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
559
|
+
admitTranscript: true,
|
|
560
|
+
phase: 'completed',
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await server.close();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('admits messages when V1 migration is absent', async () => {
|
|
567
|
+
const stateRef = {};
|
|
568
|
+
const runtime = createRuntime({}, stateRef);
|
|
569
|
+
stateRef.current.openCodePort = 45678;
|
|
570
|
+
stubOpenCodeFetch({
|
|
571
|
+
migration: { httpStatus: 404, body: {} },
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
await runtime.waitForOpenCodeReady(1000, 40);
|
|
575
|
+
|
|
576
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
577
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
578
|
+
admitTranscript: true,
|
|
579
|
+
phase: 'absent',
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('retries V1 migration error and still blocks messages until completed', async () => {
|
|
584
|
+
const stateRef = {};
|
|
585
|
+
const runtime = createRuntime({}, stateRef);
|
|
586
|
+
stateRef.current.openCodePort = 45678;
|
|
587
|
+
let migrationCalls = 0;
|
|
588
|
+
const fetchMock = stubOpenCodeFetch({
|
|
589
|
+
migration: () => {
|
|
590
|
+
migrationCalls += 1;
|
|
591
|
+
if (migrationCalls === 1) {
|
|
592
|
+
return jsonResponse({ status: 'error', error: 'locked' });
|
|
593
|
+
}
|
|
594
|
+
return jsonResponse({ status: 'completed' });
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
await runtime.waitForOpenCodeReady(2000, 40);
|
|
599
|
+
|
|
600
|
+
expect(migrationCalls).toBeGreaterThan(1);
|
|
601
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/experimental/migration/v1'))).toBe(true);
|
|
602
|
+
expect(fetchMock.mock.calls.every((call) => !call[1] || call[1].method !== 'POST')).toBe(true);
|
|
603
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
604
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
605
|
+
admitTranscript: true,
|
|
606
|
+
phase: 'completed',
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('publishes v1Migration on lifecycle state for health snapshot', async () => {
|
|
611
|
+
const stateRef = {};
|
|
612
|
+
const runtime = createRuntime({}, stateRef);
|
|
613
|
+
stateRef.current.openCodePort = 45678;
|
|
614
|
+
stubOpenCodeFetch({
|
|
615
|
+
migration: {
|
|
616
|
+
status: 'running',
|
|
617
|
+
progress: { label: 'Sessions', numerator: 2, denominator: 8 },
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
await expect(runtime.waitForOpenCodeReady(200, 40)).rejects.toThrow(/V1 migration is running/);
|
|
622
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
623
|
+
expect(stateRef.current.v1Migration).toMatchObject({
|
|
624
|
+
admitTranscript: false,
|
|
625
|
+
phase: 'running',
|
|
626
|
+
progress: { label: 'Sessions', numerator: 2, denominator: 8 },
|
|
627
|
+
});
|
|
628
|
+
expect(stateRef.current.v1Migration.userNotice).toContain('V1 subtasks do not appear in v2');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('clears v1Migration when an external OpenCode restart probe fails', async () => {
|
|
632
|
+
const stateRef = {};
|
|
633
|
+
const runtime = createRuntime({
|
|
634
|
+
env: {
|
|
635
|
+
ENV_CONFIGURED_OPENCODE_PORT: null,
|
|
636
|
+
ENV_CONFIGURED_OPENCODE_HOST: null,
|
|
637
|
+
ENV_EFFECTIVE_PORT: 45678,
|
|
638
|
+
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
|
|
639
|
+
ENV_SKIP_OPENCODE_START: false,
|
|
640
|
+
},
|
|
641
|
+
}, stateRef);
|
|
642
|
+
stateRef.current.isExternalOpenCode = true;
|
|
643
|
+
stateRef.current.openCodePort = 45678;
|
|
644
|
+
stateRef.current.openCodeBaseUrl = 'http://127.0.0.1:45678';
|
|
645
|
+
stateRef.current.v1Migration = { admitTranscript: true, phase: 'completed' };
|
|
646
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ healthy: false }), {
|
|
647
|
+
status: 200,
|
|
648
|
+
headers: { 'content-type': 'application/json' },
|
|
649
|
+
})));
|
|
650
|
+
|
|
651
|
+
await expect(runtime.restartOpenCode()).rejects.toThrow('External OpenCode server on port 45678 is not responding');
|
|
652
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
653
|
+
expect(stateRef.current.v1Migration).toBeNull();
|
|
654
|
+
// Read-only external mount: failed re-probe keeps port + ownership; never spawn.
|
|
655
|
+
expect(stateRef.current.openCodePort).toBe(45678);
|
|
656
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
657
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('rejects healthy 1.x health bodies instead of admitting the process', async () => {
|
|
661
|
+
const stateRef = {};
|
|
662
|
+
const runtime = createRuntime({}, stateRef);
|
|
663
|
+
stateRef.current.openCodePort = 45678;
|
|
664
|
+
stubOpenCodeFetch({ health: { healthy: true, version: '1.15.0' } });
|
|
665
|
+
|
|
666
|
+
await expect(runtime.waitForOpenCodeReady(300, 40)).rejects.toThrow(/unhealthy|Timed out|1\.|version/i);
|
|
667
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('rejects healthy responses with a missing version', async () => {
|
|
671
|
+
const stateRef = {};
|
|
672
|
+
const runtime = createRuntime({}, stateRef);
|
|
673
|
+
stateRef.current.openCodePort = 45678;
|
|
674
|
+
stubOpenCodeFetch({ health: { healthy: true } });
|
|
675
|
+
|
|
676
|
+
await expect(runtime.waitForOpenCodeReady(300, 40)).rejects.toThrow();
|
|
677
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('health version + migration matrix: v2+completed admits, v2+running blocks, 1.x blocks', async () => {
|
|
681
|
+
const cases = [
|
|
682
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'completed' }, admit: true },
|
|
683
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'running' }, admit: false },
|
|
684
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'required' }, admit: false },
|
|
685
|
+
{ health: DEFAULT_V2_HEALTH, migration: { status: 'error', error: 'disk' }, admit: false },
|
|
686
|
+
{ health: { healthy: true, version: '1.18.18' }, migration: { status: 'completed' }, admit: false },
|
|
687
|
+
];
|
|
688
|
+
|
|
689
|
+
for (const entry of cases) {
|
|
690
|
+
const stateRef = {};
|
|
691
|
+
const runtime = createRuntime({}, stateRef);
|
|
692
|
+
stateRef.current.openCodePort = 45678;
|
|
693
|
+
stubOpenCodeFetch({ health: entry.health, migration: entry.migration });
|
|
694
|
+
if (entry.admit) {
|
|
695
|
+
await runtime.waitForOpenCodeReady(1000, 40);
|
|
696
|
+
expect(stateRef.current.isOpenCodeReady).toBe(true);
|
|
697
|
+
} else {
|
|
698
|
+
await expect(runtime.waitForOpenCodeReady(250, 40)).rejects.toThrow();
|
|
699
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('aborts a hung migration probe with the same per-attempt signal after health', async () => {
|
|
705
|
+
const stateRef = {};
|
|
706
|
+
const runtime = createRuntime({}, stateRef);
|
|
707
|
+
stateRef.current.openCodePort = 45678;
|
|
708
|
+
let migrationStarted = 0;
|
|
709
|
+
let sawAbort = false;
|
|
710
|
+
stubOpenCodeFetch({
|
|
711
|
+
health: DEFAULT_V2_HEALTH,
|
|
712
|
+
migration: (_url, init) => {
|
|
713
|
+
migrationStarted += 1;
|
|
714
|
+
return new Promise((_resolve, reject) => {
|
|
715
|
+
const signal = init?.signal;
|
|
716
|
+
if (!signal) {
|
|
717
|
+
reject(new Error('migration probe missing abort signal'));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (signal.aborted) {
|
|
721
|
+
sawAbort = true;
|
|
722
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
signal.addEventListener('abort', () => {
|
|
726
|
+
sawAbort = true;
|
|
727
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
728
|
+
}, { once: true });
|
|
729
|
+
});
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// Default per-attempt health timeout is 5s; outer deadline must exceed it once
|
|
734
|
+
// so the hung migration is aborted by the attempt timer, not only the loop exit.
|
|
735
|
+
const started = Date.now();
|
|
736
|
+
await expect(runtime.waitForOpenCodeReady(6500, 50)).rejects.toThrow();
|
|
737
|
+
const elapsed = Date.now() - started;
|
|
738
|
+
expect(migrationStarted).toBeGreaterThan(0);
|
|
739
|
+
expect(sawAbort).toBe(true);
|
|
740
|
+
expect(elapsed).toBeLessThan(12_000);
|
|
741
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
742
|
+
}, 15_000);
|
|
743
|
+
|
|
744
|
+
it('does not mark external skip-start ready before version and migration admit', async () => {
|
|
745
|
+
const stateRef = {};
|
|
746
|
+
const runtime = createRuntime({
|
|
747
|
+
env: {
|
|
748
|
+
ENV_CONFIGURED_OPENCODE_PORT: null,
|
|
749
|
+
ENV_CONFIGURED_OPENCODE_HOST: { origin: 'http://127.0.0.1:3001' },
|
|
750
|
+
ENV_EFFECTIVE_PORT: 3001,
|
|
751
|
+
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
|
|
752
|
+
ENV_SKIP_OPENCODE_START: true,
|
|
753
|
+
},
|
|
754
|
+
syncFromHmrState: vi.fn(),
|
|
755
|
+
}, stateRef);
|
|
756
|
+
// Reject health so bootstrap's waitForOpenCodeReady fails quickly without a
|
|
757
|
+
// 20s migration spin, while still proving skip-start never pre-admits ready.
|
|
758
|
+
stubOpenCodeFetch({ health: { healthy: true, version: '1.15.0' } });
|
|
759
|
+
|
|
760
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
761
|
+
|
|
762
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
763
|
+
expect(stateRef.current.isOpenCodeReady).toBe(false);
|
|
764
|
+
expect(stateRef.current.openCodePort).toBe(3001);
|
|
765
|
+
}, 25_000);
|
|
396
766
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { evaluateOpenCodeHealthBody } from './opencode2-pin.js';
|
|
2
|
+
|
|
1
3
|
export const createOpenCodeNetworkRuntime = (deps) => {
|
|
2
4
|
const {
|
|
3
5
|
state,
|
|
@@ -41,26 +43,29 @@ export const createOpenCodeNetworkRuntime = (deps) => {
|
|
|
41
43
|
|
|
42
44
|
const waitForReady = async (url, timeoutMs = 10000) => {
|
|
43
45
|
const start = Date.now();
|
|
46
|
+
const baseUrl = url.replace(/\/+$/, '');
|
|
44
47
|
while (Date.now() - start < timeoutMs) {
|
|
45
48
|
let timeout = null;
|
|
46
49
|
try {
|
|
47
50
|
const controller = new AbortController();
|
|
48
51
|
timeout = setTimeout(() => controller.abort(), 3000);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
const headers = {
|
|
53
|
+
Accept: 'application/json',
|
|
54
|
+
...getOpenCodeAuthHeaders(),
|
|
55
|
+
};
|
|
56
|
+
for (const healthPath of ['/api/info', '/global/health']) {
|
|
57
|
+
const response = await fetch(`${baseUrl}${healthPath}`, {
|
|
58
|
+
method: 'GET',
|
|
59
|
+
headers,
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
const body = await response.json().catch(() => null);
|
|
64
|
+
// Official 2.x /api/info is ServerInfo (version, no healthy);
|
|
65
|
+
// /global/health may still return { healthy, version }.
|
|
66
|
+
if (evaluateOpenCodeHealthBody(body).ok) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
} catch {
|
|
@@ -88,8 +93,15 @@ export const createOpenCodeNetworkRuntime = (deps) => {
|
|
|
88
93
|
throw new Error('OpenCode port is not available');
|
|
89
94
|
}
|
|
90
95
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
96
|
+
// v2 protocol mounts every route under `/api` (`@opencode-ai/protocol`).
|
|
97
|
+
// Callers pass unprefixed API paths (`/session`, `/event`, …); the bare
|
|
98
|
+
// upstream path falls through to the v2 Web UI HTML fallback. `/` stays the
|
|
99
|
+
// upstream origin for SDK base URLs, and already-prefixed paths are kept.
|
|
100
|
+
const apiPath = normalizedPath === '/' || normalizedPath.startsWith('/api')
|
|
101
|
+
? normalizedPath
|
|
102
|
+
: `/api${normalizedPath}`;
|
|
91
103
|
const prefix = normalizeApiPrefix(prefixOverride !== undefined ? prefixOverride : '');
|
|
92
|
-
const fullPath = `${prefix}${
|
|
104
|
+
const fullPath = `${prefix}${apiPath}`;
|
|
93
105
|
const base = state.openCodeBaseUrl ?? `http://${resolveConnectHostname()}:${state.openCodePort}`;
|
|
94
106
|
return `${base}${fullPath}`;
|
|
95
107
|
};
|
|
@@ -23,6 +23,63 @@ describe('OpenCode network runtime', () => {
|
|
|
23
23
|
globalThis.fetch = originalFetch;
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
it('probes /api/info with Authorization and treats healthy true as ready', async () => {
|
|
27
|
+
const fetchMock = vi.fn(async (url, options) => {
|
|
28
|
+
expect(String(url)).toContain('/api/info');
|
|
29
|
+
expect(options.headers.Authorization).toMatch(/^Basic /);
|
|
30
|
+
return new Response(JSON.stringify({ version: '2.0.12', pid: 1, urls: [], paths: { tmp: '/tmp' } }), {
|
|
31
|
+
status: 200,
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
globalThis.fetch = fetchMock;
|
|
36
|
+
|
|
37
|
+
const runtime = createOpenCodeNetworkRuntime({
|
|
38
|
+
state: {
|
|
39
|
+
openCodePort: 4096,
|
|
40
|
+
openCodeBaseUrl: null,
|
|
41
|
+
openCodeApiPrefix: '',
|
|
42
|
+
openCodeApiPrefixDetected: false,
|
|
43
|
+
openCodeApiDetectionTimer: null,
|
|
44
|
+
},
|
|
45
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic Zml4dHVyZQ==' }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await expect(runtime.waitForReady('http://127.0.0.1:4096', 1000)).resolves.toBe(true);
|
|
49
|
+
expect(String(fetchMock.mock.calls[0][0])).toContain('/api/info');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('falls back to /global/health when /api/info is not healthy', async () => {
|
|
53
|
+
const fetchMock = vi.fn(async (url) => {
|
|
54
|
+
if (String(url).includes('/api/info')) {
|
|
55
|
+
return new Response('not found', { status: 404 });
|
|
56
|
+
}
|
|
57
|
+
if (String(url).includes('/global/health')) {
|
|
58
|
+
return new Response(JSON.stringify({ healthy: true, version: '2.0.12' }), {
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return new Response('no', { status: 500 });
|
|
64
|
+
});
|
|
65
|
+
globalThis.fetch = fetchMock;
|
|
66
|
+
|
|
67
|
+
const runtime = createOpenCodeNetworkRuntime({
|
|
68
|
+
state: {
|
|
69
|
+
openCodePort: 4096,
|
|
70
|
+
openCodeBaseUrl: null,
|
|
71
|
+
openCodeApiPrefix: '',
|
|
72
|
+
openCodeApiPrefixDetected: false,
|
|
73
|
+
openCodeApiDetectionTimer: null,
|
|
74
|
+
},
|
|
75
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic Zml4dHVyZQ==' }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await expect(runtime.waitForReady('http://127.0.0.1:4096', 1000)).resolves.toBe(true);
|
|
79
|
+
expect(String(fetchMock.mock.calls[0][0])).toContain('/api/info');
|
|
80
|
+
expect(String(fetchMock.mock.calls[1][0])).toContain('/global/health');
|
|
81
|
+
});
|
|
82
|
+
|
|
26
83
|
it('returns false when readiness fetch rejects', async () => {
|
|
27
84
|
globalThis.fetch = vi.fn(async () => {
|
|
28
85
|
throw new Error('offline');
|
|
@@ -37,7 +94,7 @@ describe('OpenCode network runtime', () => {
|
|
|
37
94
|
it('builds managed OpenCode URLs against IPv4 loopback by default', () => {
|
|
38
95
|
const runtime = createRuntime();
|
|
39
96
|
|
|
40
|
-
expect(runtime.buildOpenCodeUrl('/provider')).toBe('http://127.0.0.1:4096/provider');
|
|
97
|
+
expect(runtime.buildOpenCodeUrl('/provider')).toBe('http://127.0.0.1:4096/api/provider');
|
|
41
98
|
});
|
|
42
99
|
|
|
43
100
|
it('keeps external OpenCode base URLs authoritative', () => {
|
|
@@ -45,13 +102,20 @@ describe('OpenCode network runtime', () => {
|
|
|
45
102
|
state: { openCodeBaseUrl: 'http://remote.example:4096' },
|
|
46
103
|
});
|
|
47
104
|
|
|
48
|
-
expect(runtime.buildOpenCodeUrl('/provider')).toBe('http://remote.example:4096/provider');
|
|
105
|
+
expect(runtime.buildOpenCodeUrl('/provider')).toBe('http://remote.example:4096/api/provider');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('keeps SDK base URLs (/) and already-prefixed paths untouched', () => {
|
|
109
|
+
const runtime = createRuntime();
|
|
110
|
+
|
|
111
|
+
expect(runtime.buildOpenCodeUrl('/')).toBe('http://127.0.0.1:4096/');
|
|
112
|
+
expect(runtime.buildOpenCodeUrl('/api/info')).toBe('http://127.0.0.1:4096/api/info');
|
|
49
113
|
});
|
|
50
114
|
|
|
51
115
|
it('normalizes wildcard and IPv6 OpenCode bind hosts for local connects', () => {
|
|
52
116
|
expect(createRuntime({ configuredOpenCodeHostname: '0.0.0.0' }).buildOpenCodeUrl('/provider'))
|
|
53
|
-
.toBe('http://127.0.0.1:4096/provider');
|
|
117
|
+
.toBe('http://127.0.0.1:4096/api/provider');
|
|
54
118
|
expect(createRuntime({ configuredOpenCodeHostname: '::1' }).buildOpenCodeUrl('/provider'))
|
|
55
|
-
.toBe('http://[::1]:4096/provider');
|
|
119
|
+
.toBe('http://[::1]:4096/api/provider');
|
|
56
120
|
});
|
|
57
121
|
});
|