@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
2
2
|
import { projectMessageSummaryDiffCounts } from '../event-stream/diff-summary.js';
|
|
3
3
|
import {
|
|
4
4
|
projectMessagesPayloadForReasoning,
|
|
@@ -210,102 +210,235 @@ const requestSignal = (req, res) => {
|
|
|
210
210
|
return controller.signal;
|
|
211
211
|
};
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Native SessionMessageInfo carrier fields that become `parts`.
|
|
215
|
+
* Must not remain on `info` after normalize — otherwise
|
|
216
|
+
* includeReasoning=false can strip parts while leaking content/text.
|
|
217
|
+
*/
|
|
218
|
+
const MESSAGE_PART_CARRIER_KEYS = new Set(['content', 'text', 'files', 'parts', 'agents', 'skills']);
|
|
219
|
+
|
|
220
|
+
const stripMessagePartCarriers = (value) => {
|
|
221
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
|
|
222
|
+
let changed = false;
|
|
223
|
+
const next = {};
|
|
224
|
+
for (const [key, field] of Object.entries(value)) {
|
|
225
|
+
if (MESSAGE_PART_CARRIER_KEYS.has(key)) {
|
|
226
|
+
changed = true;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
next[key] = field;
|
|
230
|
+
}
|
|
231
|
+
return changed ? next : value;
|
|
217
232
|
};
|
|
218
233
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Project SessionMessageInfo (or an already-projected {info, parts} row)
|
|
236
|
+
* into the host HTTP DTO. External route contract stays {info, parts}.
|
|
237
|
+
*/
|
|
238
|
+
const projectV2Parts = (entry) => {
|
|
239
|
+
if (Array.isArray(entry?.parts)) return entry.parts;
|
|
240
|
+
const messageID = typeof entry?.id === 'string' ? entry.id : '';
|
|
241
|
+
const sessionID = typeof entry?.sessionID === 'string' ? entry.sessionID : '';
|
|
242
|
+
const parts = [];
|
|
243
|
+
if (typeof entry?.text === 'string' && entry.text) {
|
|
244
|
+
parts.push({ id: `${messageID}:text`, sessionID, messageID, type: 'text', text: entry.text });
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(entry?.files)) {
|
|
247
|
+
entry.files.forEach((file, index) => {
|
|
248
|
+
parts.push({
|
|
249
|
+
id: `${messageID}:file:${index}`,
|
|
250
|
+
sessionID,
|
|
251
|
+
messageID,
|
|
252
|
+
type: 'file',
|
|
253
|
+
mime: file?.mime || 'application/octet-stream',
|
|
254
|
+
url: file?.uri ?? file?.url,
|
|
255
|
+
...(file?.name ? { filename: file.name } : {}),
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(entry?.content)) {
|
|
260
|
+
entry.content.forEach((item, index) => {
|
|
261
|
+
const partID = item?.id || `${messageID}:content:${index}`;
|
|
262
|
+
if (item?.type === 'text') {
|
|
263
|
+
parts.push({ id: partID, sessionID, messageID, type: 'text', text: item.text ?? '' });
|
|
264
|
+
} else if (item?.type === 'reasoning') {
|
|
265
|
+
parts.push({ id: partID, sessionID, messageID, type: 'reasoning', text: item.text ?? '' });
|
|
266
|
+
} else if (item?.type === 'tool') {
|
|
267
|
+
parts.push({
|
|
268
|
+
id: partID,
|
|
269
|
+
sessionID,
|
|
270
|
+
messageID,
|
|
271
|
+
type: 'tool',
|
|
272
|
+
tool: item.name,
|
|
273
|
+
callID: item.id,
|
|
274
|
+
state: item.state ?? {},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return parts;
|
|
224
280
|
};
|
|
225
281
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const
|
|
282
|
+
const projectSessionMessage = (entry) => {
|
|
283
|
+
if (entry && typeof entry === 'object' && entry.info && typeof entry.info === 'object') {
|
|
284
|
+
const info = stripMessagePartCarriers(entry.info);
|
|
285
|
+
return {
|
|
286
|
+
info: info === entry.info ? entry.info : info,
|
|
287
|
+
parts: Array.isArray(entry.parts) ? entry.parts : [],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (!entry || typeof entry !== 'object' || typeof entry.id !== 'string' || entry.id.length === 0) {
|
|
291
|
+
return entry;
|
|
292
|
+
}
|
|
293
|
+
const role = entry.type === 'user' || entry.type === 'assistant' ? entry.type : (entry.role ?? entry.type);
|
|
294
|
+
const info = {
|
|
295
|
+
...stripMessagePartCarriers(entry),
|
|
296
|
+
id: entry.id,
|
|
297
|
+
sessionID: entry.sessionID,
|
|
298
|
+
role,
|
|
299
|
+
time: entry.time ?? { created: 0 },
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
info,
|
|
303
|
+
parts: projectV2Parts(entry),
|
|
304
|
+
};
|
|
305
|
+
};
|
|
229
306
|
|
|
230
|
-
|
|
307
|
+
const projectRecords = (records) =>
|
|
308
|
+
(Array.isArray(records) ? records : []).map(projectSessionMessage);
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Host outbound order: normalize → slim/L1 bounds → reasoning strip.
|
|
312
|
+
* Slim/reasoning must run on `{info, parts}` so native `content` cannot
|
|
313
|
+
* rebuild filtered parts or leak through info expansion.
|
|
314
|
+
*/
|
|
315
|
+
const projectTurnPageRecords = (records, includeReasoning) =>
|
|
316
|
+
projectMessagesPayloadForReasoning(projectSlimParts(projectRecords(records)), includeReasoning);
|
|
317
|
+
|
|
318
|
+
const projectReconcileRecords = (records, includeReasoning) =>
|
|
319
|
+
projectMessagesPayloadForReasoning(
|
|
320
|
+
projectMessageDiffSummaries(projectRecords(records)),
|
|
321
|
+
includeReasoning,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const createV2FetchPage = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }) => {
|
|
325
|
+
return async ({ sessionID, before, limit, signal }) => {
|
|
326
|
+
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
327
|
+
const headers = typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {};
|
|
328
|
+
const client = makeOpenCodeV2Client({ baseUrl, authHeaders: headers });
|
|
329
|
+
|
|
330
|
+
const result = await client.message.list({
|
|
231
331
|
sessionID,
|
|
232
|
-
...(typeof directory === 'string' && directory.length > 0 ? { directory } : {}),
|
|
233
332
|
...(Number.isFinite(limit) ? { limit } : {}),
|
|
234
|
-
|
|
333
|
+
order: 'desc',
|
|
334
|
+
...(typeof before === 'string' && before.length > 0 ? { cursor: before } : {}),
|
|
235
335
|
}, { signal });
|
|
236
336
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
? data
|
|
244
|
-
: Array.isArray(data?.items)
|
|
245
|
-
? data.items
|
|
246
|
-
: null;
|
|
247
|
-
if (!records) {
|
|
248
|
-
throwUpstream(logger, '[session-turn-pages] session.messages malformed payload');
|
|
337
|
+
const items = Array.isArray(result?.data) ? result.data : null;
|
|
338
|
+
if (!items) {
|
|
339
|
+
logger?.warn?.('[session-turn-pages] message.list malformed payload');
|
|
340
|
+
const error = new Error('upstream');
|
|
341
|
+
error.code = 'upstream';
|
|
342
|
+
throw error;
|
|
249
343
|
}
|
|
250
344
|
|
|
251
|
-
const
|
|
252
|
-
const nextCursor = typeof
|
|
253
|
-
? headerCursor
|
|
254
|
-
: null;
|
|
345
|
+
const previous = result?.cursor?.previous;
|
|
346
|
+
const nextCursor = typeof previous === 'string' && previous.length > 0 ? previous : null;
|
|
255
347
|
|
|
256
348
|
return {
|
|
257
|
-
records,
|
|
349
|
+
records: items.slice().reverse(),
|
|
258
350
|
nextCursor,
|
|
259
351
|
complete: nextCursor == null,
|
|
260
352
|
};
|
|
261
353
|
};
|
|
262
354
|
};
|
|
263
355
|
|
|
356
|
+
const createSdkClient = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders }) => {
|
|
357
|
+
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
358
|
+
const headers = typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {};
|
|
359
|
+
return makeOpenCodeV2Client({ baseUrl, authHeaders: headers });
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const throwUpstream = (logger, label) => {
|
|
363
|
+
logger?.warn?.(label);
|
|
364
|
+
const error = new Error('upstream');
|
|
365
|
+
error.code = 'upstream';
|
|
366
|
+
throw error;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const getHttpStatus = (error) => {
|
|
370
|
+
const candidates = [error?.cause?.status, error?.status, error?.response?.status, error?.statusCode];
|
|
371
|
+
for (const value of candidates) {
|
|
372
|
+
if (Number.isFinite(value)) return value;
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Real `@opencode/client` throws declared JSON bodies with `_tag`
|
|
379
|
+
* (e.g. MessageNotFoundError) — no HTTP status on the object. Also accept
|
|
380
|
+
* legacy status/code shapes from tests and older surfaces.
|
|
381
|
+
*/
|
|
382
|
+
const isNotFoundError = (error) => {
|
|
383
|
+
if (!error || typeof error !== 'object') return false;
|
|
384
|
+
const tag = error._tag ?? error.name;
|
|
385
|
+
if (tag === 'MessageNotFoundError' || tag === 'SessionNotFoundError') return true;
|
|
386
|
+
if (getHttpStatus(error) === 404) return true;
|
|
387
|
+
const code = error.code ?? error.type ?? error.error?.code ?? error.error?.type ?? error.error?._tag;
|
|
388
|
+
return code === 'not_found'
|
|
389
|
+
|| code === 'NotFound'
|
|
390
|
+
|| code === 'MessageNotFoundError'
|
|
391
|
+
|| code === 'SessionNotFoundError';
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const isAbortError = (error) => (
|
|
395
|
+
error?.name === 'AbortError'
|
|
396
|
+
|| error?.code === 'aborted'
|
|
397
|
+
|| error?.cause?.name === 'AbortError'
|
|
398
|
+
);
|
|
399
|
+
|
|
264
400
|
const createSdkFetchMessage = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }) => {
|
|
265
|
-
return async ({ sessionID, messageID,
|
|
401
|
+
return async ({ sessionID, messageID, signal }) => {
|
|
266
402
|
const client = createSdkClient({ buildOpenCodeUrl, getOpenCodeAuthHeaders });
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
messageID,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
error.code = 'not_found';
|
|
278
|
-
throw error;
|
|
403
|
+
let result;
|
|
404
|
+
try {
|
|
405
|
+
result = await client.session.message({ sessionID, messageID }, { signal });
|
|
406
|
+
} catch (error) {
|
|
407
|
+
// Preserve abort so routes map 499; never fold into upstream 502.
|
|
408
|
+
if (isAbortError(error) || signal?.aborted) throw error;
|
|
409
|
+
if (isNotFoundError(error)) {
|
|
410
|
+
const notFound = new Error('not_found');
|
|
411
|
+
notFound.code = 'not_found';
|
|
412
|
+
throw notFound;
|
|
279
413
|
}
|
|
280
414
|
throwUpstream(logger, '[session-turn-pages] session.message SDK error');
|
|
281
415
|
}
|
|
282
416
|
|
|
283
|
-
const data = result?.data;
|
|
417
|
+
const data = result?.data ?? result;
|
|
284
418
|
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
285
419
|
throwUpstream(logger, '[session-turn-pages] session.message malformed payload');
|
|
286
420
|
}
|
|
287
|
-
return data;
|
|
421
|
+
return projectSessionMessage(data);
|
|
288
422
|
};
|
|
289
423
|
};
|
|
290
424
|
|
|
425
|
+
/**
|
|
426
|
+
* L3 diff source. v2 SessionMessageInfo has no summary.diffs; keep failure
|
|
427
|
+
* explicit until the parent lands an authoritative per-message diff API.
|
|
428
|
+
* Do not substitute working-tree / vcs.diff here.
|
|
429
|
+
*/
|
|
291
430
|
const createSdkFetchDiff = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }) => {
|
|
292
431
|
return async ({ sessionID, messageID, directory, signal }) => {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
throwUpstream(logger, '[session-changes] session.diff SDK error');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const data = result?.data;
|
|
305
|
-
if (!Array.isArray(data)) {
|
|
306
|
-
throwUpstream(logger, '[session-changes] session.diff malformed payload');
|
|
432
|
+
const payload = await createSdkFetchMessage({
|
|
433
|
+
buildOpenCodeUrl,
|
|
434
|
+
getOpenCodeAuthHeaders,
|
|
435
|
+
logger,
|
|
436
|
+
})({ sessionID, messageID, directory, signal });
|
|
437
|
+
const diffs = payload?.info?.summary?.diffs;
|
|
438
|
+
if (!Array.isArray(diffs)) {
|
|
439
|
+
throwUpstream(logger, '[session-changes] session.message summary.diffs unavailable (L3 source pending)');
|
|
307
440
|
}
|
|
308
|
-
return
|
|
441
|
+
return diffs;
|
|
309
442
|
};
|
|
310
443
|
};
|
|
311
444
|
|
|
@@ -441,7 +574,7 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
441
574
|
|
|
442
575
|
const needsDefaultFetch = !injectedService || !injectedReconcileService;
|
|
443
576
|
const defaultFetchPage = needsDefaultFetch
|
|
444
|
-
?
|
|
577
|
+
? createV2FetchPage({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger })
|
|
445
578
|
: null;
|
|
446
579
|
|
|
447
580
|
const service = injectedService ?? createSessionTurnPageService({
|
|
@@ -514,8 +647,8 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
514
647
|
}
|
|
515
648
|
|
|
516
649
|
const includeReasoning = readIncludeReasoningQuery(req.query);
|
|
517
|
-
const records =
|
|
518
|
-
|
|
650
|
+
const records = projectReconcileRecords(
|
|
651
|
+
Array.isArray(result.records) ? result.records : [],
|
|
519
652
|
includeReasoning,
|
|
520
653
|
);
|
|
521
654
|
|
|
@@ -609,10 +742,10 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
609
742
|
|
|
610
743
|
// Turn-page responses (first packet and prepend) share slim-v1.
|
|
611
744
|
// Reconcile stays on the other route and keeps full parts.
|
|
612
|
-
//
|
|
745
|
+
// Normalize native SessionMessageInfo first, then slim, then reasoning strip.
|
|
613
746
|
const includeReasoning = readIncludeReasoningQuery(req.query);
|
|
614
|
-
const records =
|
|
615
|
-
|
|
747
|
+
const records = projectTurnPageRecords(
|
|
748
|
+
Array.isArray(result.records) ? result.records : [],
|
|
616
749
|
includeReasoning,
|
|
617
750
|
);
|
|
618
751
|
return res.status(200).json({
|
|
@@ -673,13 +806,13 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
673
806
|
projectMessagesPayloadForReasoning(projectExactMessagePayload(payload), includeReasoning),
|
|
674
807
|
);
|
|
675
808
|
} catch (error) {
|
|
676
|
-
if (error
|
|
809
|
+
if (isAbortError(error) || error?.code === 'aborted') {
|
|
677
810
|
if (!res.headersSent) {
|
|
678
811
|
return res.status(499).json({ error: SAFE_ERRORS.aborted });
|
|
679
812
|
}
|
|
680
813
|
return undefined;
|
|
681
814
|
}
|
|
682
|
-
if (error?.code === 'not_found') {
|
|
815
|
+
if (error?.code === 'not_found' || isNotFoundError(error)) {
|
|
683
816
|
if (!res.headersSent) {
|
|
684
817
|
return res.status(404).json({ error: 'not found', code: 'not_found' });
|
|
685
818
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
3
4
|
import { registerSessionTurnPageRoutes } from './routes.js';
|
|
4
5
|
|
|
6
|
+
vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: vi.fn() }));
|
|
7
|
+
|
|
5
8
|
const registry = () => {
|
|
6
9
|
const routes = new Map();
|
|
7
10
|
return {
|
|
@@ -163,6 +166,59 @@ describe('registerSessionTurnPageRoutes', () => {
|
|
|
163
166
|
expect(kept.body.records[0].parts.some((part) => part.type === 'reasoning')).toBe(true);
|
|
164
167
|
});
|
|
165
168
|
|
|
169
|
+
it('normalizes native SessionMessage content before slim+reasoning so content cannot leak', async () => {
|
|
170
|
+
const loadPage = vi.fn(async () => ({
|
|
171
|
+
ok: true,
|
|
172
|
+
records: [{
|
|
173
|
+
id: 'msg_a1',
|
|
174
|
+
sessionID: 'ses_1',
|
|
175
|
+
type: 'assistant',
|
|
176
|
+
time: { created: 1, completed: 2 },
|
|
177
|
+
agent: 'build',
|
|
178
|
+
model: { providerID: 'p', id: 'm' },
|
|
179
|
+
tokens: { input: 1, output: 2, reasoning: 9 },
|
|
180
|
+
content: [
|
|
181
|
+
{ type: 'reasoning', text: 'SECRET_REASONING_BODY' },
|
|
182
|
+
{ type: 'text', text: 'visible-answer' },
|
|
183
|
+
{
|
|
184
|
+
type: 'tool',
|
|
185
|
+
name: 'bash',
|
|
186
|
+
id: 'call_1',
|
|
187
|
+
state: { status: 'completed', title: 'ran', output: 'HUGE_TOOL_OUTPUT' },
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
}],
|
|
191
|
+
turnCount: 0,
|
|
192
|
+
cursor: null,
|
|
193
|
+
complete: true,
|
|
194
|
+
}));
|
|
195
|
+
const { app, route } = registry();
|
|
196
|
+
registerSessionTurnPageRoutes(app, { sessionTurnPageService: { loadPage } });
|
|
197
|
+
|
|
198
|
+
const stripped = response();
|
|
199
|
+
await route('GET', ROUTE)({
|
|
200
|
+
params: { sessionID: 'ses_1' },
|
|
201
|
+
query: { includeReasoning: 'false' },
|
|
202
|
+
headers: {},
|
|
203
|
+
}, stripped);
|
|
204
|
+
|
|
205
|
+
expect(stripped.statusCode).toBe(200);
|
|
206
|
+
const record = stripped.body.records[0];
|
|
207
|
+
expect(record.info.id).toBe('msg_a1');
|
|
208
|
+
expect(record.info.role).toBe('assistant');
|
|
209
|
+
expect(record.info.tokens.reasoning).toBe(9);
|
|
210
|
+
expect(record.info.content).toBeUndefined();
|
|
211
|
+
expect(record.parts.every((part) => part.type !== 'reasoning')).toBe(true);
|
|
212
|
+
expect(record.parts.some((part) => part.type === 'text' && part.text === 'visible-answer')).toBe(true);
|
|
213
|
+
const tool = record.parts.find((part) => part.type === 'tool');
|
|
214
|
+
expect(tool.slim).toBe(true);
|
|
215
|
+
expect(tool.state.output).toBeUndefined();
|
|
216
|
+
expect(stripped.body.partsProjection).toBe('slim-v1');
|
|
217
|
+
const serialized = JSON.stringify(stripped.body);
|
|
218
|
+
expect(serialized).not.toContain('SECRET_REASONING_BODY');
|
|
219
|
+
expect(serialized).not.toContain('HUGE_TOOL_OUTPUT');
|
|
220
|
+
});
|
|
221
|
+
|
|
166
222
|
it('maps upstream service errors to an upstream HTTP status', async () => {
|
|
167
223
|
const loadPage = vi.fn(async () => ({
|
|
168
224
|
ok: false,
|
|
@@ -637,6 +693,87 @@ describe('registerSessionTurnPageRoutes', () => {
|
|
|
637
693
|
resolveLoad?.();
|
|
638
694
|
await pending;
|
|
639
695
|
});
|
|
696
|
+
|
|
697
|
+
it('default fetch uses message.list SessionMessageInfo[] + cursor and projects {info, parts}', async () => {
|
|
698
|
+
const list = vi.fn(async () => ({
|
|
699
|
+
data: [
|
|
700
|
+
{ id: 'msg_a1', type: 'assistant', time: { created: 2, completed: 3 }, content: [{ type: 'text', text: 'ok' }] },
|
|
701
|
+
{ id: 'msg_u1', type: 'user', time: { created: 1 }, text: 'hi' },
|
|
702
|
+
],
|
|
703
|
+
cursor: { previous: null, next: null },
|
|
704
|
+
}));
|
|
705
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
706
|
+
message: { list },
|
|
707
|
+
session: { messages: undefined, status: undefined, abort: undefined },
|
|
708
|
+
});
|
|
709
|
+
const { app, route } = registry();
|
|
710
|
+
registerSessionTurnPageRoutes(app, {
|
|
711
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
712
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'secret' }),
|
|
713
|
+
});
|
|
714
|
+
const res = response();
|
|
715
|
+
await route('GET', ROUTE)({
|
|
716
|
+
params: { sessionID: 'ses_1' },
|
|
717
|
+
query: { turns: '1' },
|
|
718
|
+
headers: {},
|
|
719
|
+
}, res);
|
|
720
|
+
|
|
721
|
+
expect(list).toHaveBeenCalledWith({
|
|
722
|
+
sessionID: 'ses_1',
|
|
723
|
+
limit: 100,
|
|
724
|
+
order: 'desc',
|
|
725
|
+
}, expect.objectContaining({ signal: expect.any(Object) }));
|
|
726
|
+
expect(res.statusCode).toBe(200);
|
|
727
|
+
expect(res.body.complete).toBe(true);
|
|
728
|
+
expect(res.body.records).toEqual([
|
|
729
|
+
expect.objectContaining({
|
|
730
|
+
info: expect.objectContaining({ id: 'msg_u1', role: 'user' }),
|
|
731
|
+
parts: [expect.objectContaining({ type: 'text', text: 'hi' })],
|
|
732
|
+
}),
|
|
733
|
+
expect.objectContaining({
|
|
734
|
+
info: expect.objectContaining({ id: 'msg_a1', role: 'assistant' }),
|
|
735
|
+
parts: [expect.objectContaining({ type: 'text', text: 'ok' })],
|
|
736
|
+
}),
|
|
737
|
+
]);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('default fetch throws on message.list failure and maps to upstream', async () => {
|
|
741
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
742
|
+
message: { list: vi.fn(async () => { throw Object.assign(new Error('upstream'), { status: 500 }); }) },
|
|
743
|
+
});
|
|
744
|
+
const { app, route } = registry();
|
|
745
|
+
registerSessionTurnPageRoutes(app, {
|
|
746
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
747
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
748
|
+
});
|
|
749
|
+
const res = response();
|
|
750
|
+
await route('GET', ROUTE)({
|
|
751
|
+
params: { sessionID: 'ses_1' },
|
|
752
|
+
query: { turns: '1' },
|
|
753
|
+
headers: {},
|
|
754
|
+
}, res);
|
|
755
|
+
expect(res.statusCode).toBeGreaterThanOrEqual(502);
|
|
756
|
+
expect(res.body).toMatchObject({ error: expect.stringMatching(/upstream/i) });
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('default fetch rejects HeyAPI data/error shapes without a SessionMessageInfo array', async () => {
|
|
760
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
761
|
+
message: { list: vi.fn(async () => ({ error: { status: 500 }, data: undefined })) },
|
|
762
|
+
});
|
|
763
|
+
const { app, route } = registry();
|
|
764
|
+
registerSessionTurnPageRoutes(app, {
|
|
765
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
766
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
767
|
+
});
|
|
768
|
+
const res = response();
|
|
769
|
+
await route('GET', ROUTE)({
|
|
770
|
+
params: { sessionID: 'ses_1' },
|
|
771
|
+
query: { turns: '1' },
|
|
772
|
+
headers: {},
|
|
773
|
+
}, res);
|
|
774
|
+
expect(res.statusCode).toBeGreaterThanOrEqual(502);
|
|
775
|
+
expect(res.body.records).toBeUndefined();
|
|
776
|
+
});
|
|
640
777
|
});
|
|
641
778
|
|
|
642
779
|
const EXACT_MESSAGE_ROUTE = '/api/session/:sessionID/message/:messageID';
|
|
@@ -752,6 +889,154 @@ describe('registerSessionTurnPageRoutes — exact message GET', () => {
|
|
|
752
889
|
expect(res.statusCode).toBe(404);
|
|
753
890
|
expect(res.body.code).toBe('not_found');
|
|
754
891
|
});
|
|
892
|
+
|
|
893
|
+
it('maps AbortError to 499 and does not fold it into upstream 502', async () => {
|
|
894
|
+
const fetchExactMessage = vi.fn(async () => {
|
|
895
|
+
const error = new Error('aborted');
|
|
896
|
+
error.name = 'AbortError';
|
|
897
|
+
throw error;
|
|
898
|
+
});
|
|
899
|
+
const { app, route } = registry();
|
|
900
|
+
registerSessionTurnPageRoutes(app, {
|
|
901
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
902
|
+
fetchExactMessage,
|
|
903
|
+
});
|
|
904
|
+
const res = response();
|
|
905
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
906
|
+
params: { sessionID: 'ses_1', messageID: 'msg_1' },
|
|
907
|
+
query: {},
|
|
908
|
+
headers: {},
|
|
909
|
+
}, res);
|
|
910
|
+
expect(res.statusCode).toBe(499);
|
|
911
|
+
expect(res.statusCode).not.toBe(502);
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
describe('registerSessionTurnPageRoutes — real OpenCode.make + fake HTTP', () => {
|
|
916
|
+
const jsonResponse = (status, body) => ({
|
|
917
|
+
ok: status >= 200 && status < 300,
|
|
918
|
+
status,
|
|
919
|
+
headers: {
|
|
920
|
+
get: (name) => (String(name).toLowerCase() === 'content-type' ? 'application/json' : null),
|
|
921
|
+
},
|
|
922
|
+
json: async () => body,
|
|
923
|
+
text: async () => JSON.stringify(body ?? null),
|
|
924
|
+
arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(body ?? null)).buffer,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('session.message MessageNotFoundError _tag maps to HTTP 404 via real client', async () => {
|
|
928
|
+
const fetchImpl = vi.fn(async (url) => {
|
|
929
|
+
const path = String(url);
|
|
930
|
+
if (path.includes('/message/msg_missing')) {
|
|
931
|
+
return jsonResponse(404, {
|
|
932
|
+
_tag: 'MessageNotFoundError',
|
|
933
|
+
sessionID: 'ses_1',
|
|
934
|
+
messageID: 'msg_missing',
|
|
935
|
+
message: 'Message not found',
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return jsonResponse(500, { _tag: 'UnknownError', message: 'unexpected' });
|
|
939
|
+
});
|
|
940
|
+
const { makeOpenCodeV2Client: realMake } = await vi.importActual('../opencode/v2-client.js');
|
|
941
|
+
makeOpenCodeV2Client.mockImplementation(({ baseUrl, authHeaders }) =>
|
|
942
|
+
realMake({ baseUrl, authHeaders, fetchImpl }));
|
|
943
|
+
|
|
944
|
+
const { app, route } = registry();
|
|
945
|
+
registerSessionTurnPageRoutes(app, {
|
|
946
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
947
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
948
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
|
|
949
|
+
});
|
|
950
|
+
const res = response();
|
|
951
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
952
|
+
params: { sessionID: 'ses_1', messageID: 'msg_missing' },
|
|
953
|
+
query: {},
|
|
954
|
+
headers: {},
|
|
955
|
+
}, res);
|
|
956
|
+
|
|
957
|
+
expect(res.statusCode).toBe(404);
|
|
958
|
+
expect(res.body.code).toBe('not_found');
|
|
959
|
+
expect(fetchImpl).toHaveBeenCalled();
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('exact message serializes native assistant with reasoning strip and L1 slim', async () => {
|
|
963
|
+
const nativeAssistant = {
|
|
964
|
+
id: 'msg_a1',
|
|
965
|
+
type: 'assistant',
|
|
966
|
+
time: { created: 10, completed: 20 },
|
|
967
|
+
agent: 'build',
|
|
968
|
+
model: { providerID: 'openai', id: 'gpt' },
|
|
969
|
+
tokens: { input: 3, output: 4, reasoning: 7 },
|
|
970
|
+
content: [
|
|
971
|
+
{ type: 'reasoning', text: 'PRIVATE_CHAIN' },
|
|
972
|
+
{ type: 'text', text: 'hello-world' },
|
|
973
|
+
],
|
|
974
|
+
// Host may still see legacy summary on some mirrors; L1 must slim it.
|
|
975
|
+
summary: {
|
|
976
|
+
diffs: [{
|
|
977
|
+
file: 'a.ts',
|
|
978
|
+
status: 'modified',
|
|
979
|
+
additions: 1,
|
|
980
|
+
deletions: 0,
|
|
981
|
+
patch: 'PATCH_BODY_SHOULD_NOT_LEAVE',
|
|
982
|
+
}],
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
const fetchImpl = vi.fn(async () => jsonResponse(200, { data: nativeAssistant }));
|
|
986
|
+
const { makeOpenCodeV2Client: realMake } = await vi.importActual('../opencode/v2-client.js');
|
|
987
|
+
makeOpenCodeV2Client.mockImplementation(({ baseUrl, authHeaders }) =>
|
|
988
|
+
realMake({ baseUrl, authHeaders, fetchImpl }));
|
|
989
|
+
|
|
990
|
+
const { app, route } = registry();
|
|
991
|
+
registerSessionTurnPageRoutes(app, {
|
|
992
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
993
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
994
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const stripped = response();
|
|
998
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
999
|
+
params: { sessionID: 'ses_1', messageID: 'msg_a1' },
|
|
1000
|
+
query: { includeReasoning: 'false' },
|
|
1001
|
+
headers: {},
|
|
1002
|
+
}, stripped);
|
|
1003
|
+
|
|
1004
|
+
expect(stripped.statusCode).toBe(200);
|
|
1005
|
+
expect(stripped.body.info.id).toBe('msg_a1');
|
|
1006
|
+
expect(stripped.body.info.role).toBe('assistant');
|
|
1007
|
+
expect(stripped.body.info.content).toBeUndefined();
|
|
1008
|
+
expect(stripped.body.info.tokens.reasoning).toBe(7);
|
|
1009
|
+
expect(stripped.body.parts.every((part) => part.type !== 'reasoning')).toBe(true);
|
|
1010
|
+
expect(stripped.body.parts.some((part) => part.type === 'text' && part.text === 'hello-world')).toBe(true);
|
|
1011
|
+
expect(stripped.body.info.summary?.diffs?.[0]?.patch).toBeUndefined();
|
|
1012
|
+
expect(JSON.stringify(stripped.body)).not.toContain('PRIVATE_CHAIN');
|
|
1013
|
+
expect(JSON.stringify(stripped.body)).not.toContain('PATCH_BODY_SHOULD_NOT_LEAVE');
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it('preserves abort from real client transport instead of 502', async () => {
|
|
1017
|
+
const fetchImpl = vi.fn(async () => {
|
|
1018
|
+
const error = new Error('The operation was aborted');
|
|
1019
|
+
error.name = 'AbortError';
|
|
1020
|
+
throw error;
|
|
1021
|
+
});
|
|
1022
|
+
const { makeOpenCodeV2Client: realMake } = await vi.importActual('../opencode/v2-client.js');
|
|
1023
|
+
makeOpenCodeV2Client.mockImplementation(({ baseUrl, authHeaders }) =>
|
|
1024
|
+
realMake({ baseUrl, authHeaders, fetchImpl }));
|
|
1025
|
+
|
|
1026
|
+
const { app, route } = registry();
|
|
1027
|
+
registerSessionTurnPageRoutes(app, {
|
|
1028
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
1029
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
1030
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
1031
|
+
});
|
|
1032
|
+
const res = response();
|
|
1033
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
1034
|
+
params: { sessionID: 'ses_1', messageID: 'msg_1' },
|
|
1035
|
+
query: {},
|
|
1036
|
+
headers: {},
|
|
1037
|
+
}, res);
|
|
1038
|
+
expect(res.statusCode).toBe(499);
|
|
1039
|
+
});
|
|
755
1040
|
});
|
|
756
1041
|
|
|
757
1042
|
describe('registerSessionTurnPageRoutes — changes API', () => {
|