@openchambery/web 1.19.20 → 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-C_JfbxNl.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-B1iUwzOz.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-BPtMj3-X.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-CKClHsWN.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-DiA24bnN.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-XlzRSS7E.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-DdIaaLba.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BbwuLdXU.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-ChzfD7mc.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-DyjmxUEN.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-8fkF0Ji_.js → multirun-BvSG94mW.js} +1 -1
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/{projectMeta--y6tyjrv.js → projectMeta-DOe49dM_.js} +2 -2
- 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-DYWtBrkW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BSWg99jc.js → runtimeEndpointReset-B6BxhvR0.js} +5 -5
- package/dist/assets/{sessionChangeSummary-BwupJF-Q.js → sessionChangeSummary-VVBlEXuh.js} +1 -1
- package/dist/assets/{sessionLookup-DP2X1qRR.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-DZ60kHBP.css → src-BzX5UI-c.css} +1 -1
- 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-BnD799LW.js → textarea-3XR8sUfy.js} +1 -1
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-DWLxpWlS.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/{useDesktopWindowControlsLayout-BGPR_rq6.js → useDesktopWindowControlsLayout-B1GX59-L.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-CbV9_r1s.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-Dnors7hB.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DQqqrOBU.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-BjvfkSRS.js +0 -3
- package/dist/assets/MarkdownRenderer-Cg82L3fN.js +0 -2
- package/dist/assets/MobileDetailNavigation-s_vvUD_F.js +0 -1
- package/dist/assets/MobileSurface-DHpXHbJp.js +0 -2
- package/dist/assets/SettingsView-l0J7pfYo.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CHpibtno.js +0 -7
- package/dist/assets/insertionBoundaries-Yd1Lorjw.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-BoRXxmB3.js +0 -2
- package/dist/assets/main-DCDcdV5o.js +0 -169
- package/dist/assets/miniChat-12fxk5KR.js +0 -2
- package/dist/assets/mobile-CkZem9Ad.js +0 -2
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-COFc0ZDJ.js +0 -2
- package/dist/assets/renderMobileApp-BKzx7JL3.js +0 -28
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useMobileNavigationStore-DrB_awgk.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
|
@@ -2,12 +2,20 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
5
8
|
|
|
6
|
-
vi.mock('
|
|
7
|
-
|
|
9
|
+
vi.mock('../opencode/v2-client.js', () => ({
|
|
10
|
+
makeOpenCodeV2Client: vi.fn(),
|
|
8
11
|
}));
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
vi.mock('../session-goal/objectives.js', () => ({
|
|
14
|
+
writeObjective: vi.fn(async () => ({ content: 'ok' })),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const { makeOpenCodeV2Client } = await import('../opencode/v2-client.js');
|
|
18
|
+
const { writeObjective } = await import('../session-goal/objectives.js');
|
|
11
19
|
const { createScheduledTaskRunHistoryStore } = await import('./run-history-store.js');
|
|
12
20
|
const {
|
|
13
21
|
computeNextRunAt,
|
|
@@ -310,10 +318,26 @@ describe('scheduled-tasks project sync isolation', () => {
|
|
|
310
318
|
});
|
|
311
319
|
});
|
|
312
320
|
|
|
321
|
+
describe('scheduled-tasks v2 cutover residuals', () => {
|
|
322
|
+
it('runtime source has no 1.x SDK, HeyAPI wrappers, or retired session APIs', () => {
|
|
323
|
+
const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'runtime.js'), 'utf8');
|
|
324
|
+
expect(source).not.toContain('@opencode-ai/sdk');
|
|
325
|
+
expect(source).not.toContain('createOpencodeClient');
|
|
326
|
+
expect(source).not.toContain('prompt_async');
|
|
327
|
+
expect(source).not.toContain('session.abort');
|
|
328
|
+
expect(source).not.toContain('session.status({');
|
|
329
|
+
expect(source).not.toContain('session.messages');
|
|
330
|
+
expect(source).not.toContain('session.update');
|
|
331
|
+
expect(source).not.toContain('time: { archived');
|
|
332
|
+
expect(source).not.toContain('result?.error');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
313
336
|
describe('scheduled-tasks run history and session lifecycle', () => {
|
|
314
337
|
beforeEach(() => {
|
|
315
338
|
vi.restoreAllMocks();
|
|
316
|
-
|
|
339
|
+
makeOpenCodeV2Client.mockReset();
|
|
340
|
+
writeObjective.mockClear();
|
|
317
341
|
});
|
|
318
342
|
|
|
319
343
|
const createHistoryStore = () => {
|
|
@@ -341,105 +365,84 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
341
365
|
};
|
|
342
366
|
|
|
343
367
|
const createSuccessfulClient = ({
|
|
344
|
-
updateResult,
|
|
345
368
|
sessionID = 'ses_1',
|
|
346
369
|
/** Override session settlement polls after prompt/command admission. */
|
|
347
370
|
settlement = 'success',
|
|
371
|
+
activeImpl,
|
|
372
|
+
messageListImpl,
|
|
348
373
|
} = {}) => {
|
|
349
|
-
const create = vi.fn(async () => ({
|
|
350
|
-
const
|
|
351
|
-
const command = vi.fn(async () => ({
|
|
352
|
-
const
|
|
374
|
+
const create = vi.fn(async () => ({ id: sessionID }));
|
|
375
|
+
const prompt = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }));
|
|
376
|
+
const command = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }));
|
|
377
|
+
const interrupt = vi.fn(async () => undefined);
|
|
353
378
|
const list = vi.fn(async () => ({ data: [] }));
|
|
354
|
-
// Default settlement: idle
|
|
355
|
-
// turn outcome (not
|
|
379
|
+
// Default settlement: idle (absent from active map) + completed assistant
|
|
380
|
+
// so history records the real turn outcome (not prompt admission alone).
|
|
356
381
|
let poll = 0;
|
|
357
|
-
const
|
|
382
|
+
const active = vi.fn(async () => {
|
|
383
|
+
if (typeof activeImpl === 'function') {
|
|
384
|
+
return activeImpl();
|
|
385
|
+
}
|
|
358
386
|
poll += 1;
|
|
359
387
|
if (settlement === 'busy-then-success' && poll < 2) {
|
|
360
|
-
return {
|
|
388
|
+
return { [sessionID]: { type: 'running' } };
|
|
361
389
|
}
|
|
362
|
-
return {
|
|
390
|
+
return {};
|
|
363
391
|
});
|
|
364
|
-
const
|
|
392
|
+
const messageList = vi.fn(async () => {
|
|
393
|
+
if (typeof messageListImpl === 'function') {
|
|
394
|
+
return messageListImpl();
|
|
395
|
+
}
|
|
365
396
|
if (settlement === 'assistant-error') {
|
|
366
397
|
return {
|
|
367
398
|
data: [{
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
error: { name: 'ProviderError', message: 'upstream failed' },
|
|
372
|
-
},
|
|
399
|
+
id: 'msg_err',
|
|
400
|
+
type: 'assistant',
|
|
401
|
+
error: { name: 'ProviderError', message: 'upstream failed' },
|
|
373
402
|
}],
|
|
374
403
|
};
|
|
375
404
|
}
|
|
376
|
-
if (settlement === 'busy-then-success') {
|
|
377
|
-
|
|
378
|
-
return { data: [{ info: { id: 'msg_user', role: 'user' } }] };
|
|
379
|
-
}
|
|
405
|
+
if (settlement === 'busy-then-success' && poll < 2) {
|
|
406
|
+
return { data: [{ id: 'msg_user', type: 'user' }] };
|
|
380
407
|
}
|
|
381
408
|
return {
|
|
382
409
|
data: [{
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
},
|
|
410
|
+
id: 'msg_ok',
|
|
411
|
+
type: 'assistant',
|
|
412
|
+
time: { created: Date.now(), completed: Date.now() },
|
|
413
|
+
finish: 'stop',
|
|
388
414
|
}],
|
|
389
415
|
};
|
|
390
416
|
});
|
|
391
|
-
const get = vi.fn(async () => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
goal: { id: 'g1', status: 'complete', objective: 'done' },
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if (settlement === 'goal-blocked') {
|
|
405
|
-
return {
|
|
406
|
-
data: {
|
|
407
|
-
id: sessionID,
|
|
408
|
-
metadata: {
|
|
409
|
-
openchamber: {
|
|
410
|
-
goal: { id: 'g1', status: 'blocked', objective: 'x', note: 'stuck' },
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
return { data: { id: sessionID, metadata: {} } };
|
|
417
|
-
});
|
|
418
|
-
createOpencodeClient.mockReturnValue({
|
|
419
|
-
session: { create, update, command, abort, status, messages, get },
|
|
417
|
+
const get = vi.fn(async () => ({
|
|
418
|
+
id: sessionID,
|
|
419
|
+
location: { directory: '/tmp/project-1' },
|
|
420
|
+
}));
|
|
421
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
422
|
+
session: { create, prompt, command, interrupt, active, get },
|
|
423
|
+
message: { list: messageList },
|
|
420
424
|
command: { list },
|
|
421
425
|
});
|
|
422
|
-
return { create,
|
|
426
|
+
return { create, prompt, command, interrupt, list, active, messageList, get };
|
|
423
427
|
};
|
|
424
428
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
429
|
+
const completedAssistant = (id = 'msg_ok') => ({
|
|
430
|
+
id,
|
|
431
|
+
type: 'assistant',
|
|
432
|
+
time: { created: 1_000, completed: 1_100 },
|
|
433
|
+
finish: 'stop',
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const erroredAssistant = (id = 'msg_err') => ({
|
|
437
|
+
id,
|
|
438
|
+
type: 'assistant',
|
|
439
|
+
time: { created: 2_000, completed: 2_100 },
|
|
440
|
+
error: { name: 'ProviderError', message: 'upstream failed' },
|
|
431
441
|
});
|
|
432
442
|
|
|
433
|
-
it('creates session with Scheduled title and
|
|
443
|
+
it('creates session with Scheduled title and location, attaches history, then prompts via session.prompt', async () => {
|
|
434
444
|
const history = createHistoryStore();
|
|
435
445
|
const client = createSuccessfulClient();
|
|
436
|
-
const fetchMock = vi.fn(async (url, init) => {
|
|
437
|
-
if (String(url).includes('prompt_async')) {
|
|
438
|
-
return { ok: true, text: async () => '' };
|
|
439
|
-
}
|
|
440
|
-
throw new Error(`unexpected fetch ${url} ${init?.method}`);
|
|
441
|
-
});
|
|
442
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
443
446
|
|
|
444
447
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
445
448
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -463,49 +466,35 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
463
466
|
});
|
|
464
467
|
expect(typeof started.id).toBe('string');
|
|
465
468
|
|
|
469
|
+
expect(makeOpenCodeV2Client).toHaveBeenCalledWith({
|
|
470
|
+
baseUrl: 'http://127.0.0.1:4096',
|
|
471
|
+
authHeaders: {},
|
|
472
|
+
});
|
|
466
473
|
expect(client.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
467
|
-
directory: '/tmp/project-1',
|
|
474
|
+
location: { directory: '/tmp/project-1' },
|
|
468
475
|
title: expect.stringMatching(/^\[Scheduled\] Task /),
|
|
469
|
-
|
|
470
|
-
openchamber: {
|
|
471
|
-
scheduledTask: {
|
|
472
|
-
projectID: 'project-1',
|
|
473
|
-
taskID: 'task-1',
|
|
474
|
-
runID: started.id,
|
|
475
|
-
name: 'Task',
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
|
-
},
|
|
476
|
+
model: { id: 'gpt-4.1', providerID: 'openai' },
|
|
479
477
|
}), expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
478
|
+
expect(client.create.mock.calls[0][0].metadata).toBeUndefined();
|
|
479
|
+
expect(client.create.mock.calls[0][0].directory).toBeUndefined();
|
|
480
480
|
expect(history.attachSession).toHaveBeenCalledWith(started.id, 'ses_1');
|
|
481
|
-
expect(client.
|
|
481
|
+
expect(client.prompt).toHaveBeenCalledWith({
|
|
482
482
|
sessionID: 'ses_1',
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}), expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
486
|
-
expect(client.update.mock.invocationCallOrder[0]).toBeLessThan(fetchMock.mock.invocationCallOrder[0]);
|
|
483
|
+
text: 'run',
|
|
484
|
+
}, expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
487
485
|
expect(history.finishRun).toHaveBeenCalledWith(started.id, expect.objectContaining({
|
|
488
486
|
status: 'success',
|
|
489
487
|
sessionId: 'ses_1',
|
|
490
488
|
}));
|
|
491
|
-
// Must wait for session settlement, not finish on
|
|
492
|
-
expect(client.
|
|
493
|
-
expect(client.
|
|
494
|
-
expect(client.
|
|
495
|
-
|
|
496
|
-
vi.unstubAllGlobals();
|
|
489
|
+
// Must wait for session settlement, not finish on prompt admission alone.
|
|
490
|
+
expect(client.active).toHaveBeenCalled();
|
|
491
|
+
expect(client.messageList).toHaveBeenCalled();
|
|
492
|
+
expect(client.active.mock.invocationCallOrder[0]).toBeGreaterThan(client.prompt.mock.invocationCallOrder[0]);
|
|
497
493
|
});
|
|
498
494
|
|
|
499
495
|
it('records error outcome and real duration when the assistant turn fails after admission', async () => {
|
|
500
496
|
const history = createHistoryStore();
|
|
501
497
|
const client = createSuccessfulClient({ settlement: 'assistant-error' });
|
|
502
|
-
const fetchMock = vi.fn(async (url) => {
|
|
503
|
-
if (String(url).includes('prompt_async')) {
|
|
504
|
-
return { ok: true, text: async () => '' };
|
|
505
|
-
}
|
|
506
|
-
throw new Error(`unexpected fetch ${url}`);
|
|
507
|
-
});
|
|
508
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
509
498
|
|
|
510
499
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
511
500
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -528,22 +517,13 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
528
517
|
durationMs: expect.any(Number),
|
|
529
518
|
}),
|
|
530
519
|
);
|
|
531
|
-
expect(client.
|
|
532
|
-
|
|
533
|
-
vi.unstubAllGlobals();
|
|
520
|
+
expect(client.messageList).toHaveBeenCalled();
|
|
534
521
|
});
|
|
535
522
|
|
|
536
|
-
it('keeps the run open while the session is
|
|
523
|
+
it('keeps the run open while the session is in the active map and only finalizes after idle settlement', async () => {
|
|
537
524
|
vi.useFakeTimers();
|
|
538
525
|
const history = createHistoryStore();
|
|
539
526
|
createSuccessfulClient({ settlement: 'busy-then-success' });
|
|
540
|
-
const fetchMock = vi.fn(async (url) => {
|
|
541
|
-
if (String(url).includes('prompt_async')) {
|
|
542
|
-
return { ok: true, text: async () => '' };
|
|
543
|
-
}
|
|
544
|
-
throw new Error(`unexpected fetch ${url}`);
|
|
545
|
-
});
|
|
546
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
547
527
|
|
|
548
528
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
549
529
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -555,7 +535,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
555
535
|
await runtime.syncProject('project-1');
|
|
556
536
|
|
|
557
537
|
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
558
|
-
// First settlement poll sees
|
|
538
|
+
// First settlement poll sees membership and sleeps — history must still be open.
|
|
559
539
|
await vi.advanceTimersByTimeAsync(0);
|
|
560
540
|
expect(history.finishRun).not.toHaveBeenCalled();
|
|
561
541
|
|
|
@@ -568,17 +548,15 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
568
548
|
expect.objectContaining({ status: 'success', sessionId: 'ses_1' }),
|
|
569
549
|
);
|
|
570
550
|
|
|
571
|
-
vi.unstubAllGlobals();
|
|
572
551
|
vi.useRealTimers();
|
|
573
552
|
});
|
|
574
553
|
|
|
575
|
-
it('does not prompt when
|
|
554
|
+
it('does not prompt when attachSession fails and finalizes the run as error', async () => {
|
|
576
555
|
const history = createHistoryStore();
|
|
577
|
-
createSuccessfulClient(
|
|
578
|
-
|
|
556
|
+
const client = createSuccessfulClient();
|
|
557
|
+
history.attachSession.mockImplementation(() => {
|
|
558
|
+
throw new Error('attach exploded');
|
|
579
559
|
});
|
|
580
|
-
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
|
|
581
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
582
560
|
|
|
583
561
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
584
562
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -592,28 +570,22 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
592
570
|
|
|
593
571
|
expect(result.ok).toBe(false);
|
|
594
572
|
expect(result.status).toBe('error');
|
|
595
|
-
expect(
|
|
573
|
+
expect(result.error).toMatch(/attachSession failed/);
|
|
574
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
596
575
|
expect(history.finishRun).toHaveBeenCalledWith(
|
|
597
576
|
expect.any(String),
|
|
598
577
|
expect.objectContaining({ status: 'error' }),
|
|
599
578
|
);
|
|
600
|
-
|
|
601
|
-
vi.unstubAllGlobals();
|
|
602
579
|
});
|
|
603
580
|
|
|
604
|
-
it('
|
|
581
|
+
it('does not treat session.active failure as idle success', async () => {
|
|
605
582
|
vi.useFakeTimers();
|
|
606
583
|
const history = createHistoryStore();
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
createOpencodeClient.mockReturnValue({
|
|
612
|
-
session: { create, update, command: vi.fn(), ...settledSessionApis('ses_404') },
|
|
613
|
-
command: { list: vi.fn(async () => ({ data: [] })) },
|
|
584
|
+
createSuccessfulClient({
|
|
585
|
+
activeImpl: async () => {
|
|
586
|
+
throw new Error('active unavailable');
|
|
587
|
+
},
|
|
614
588
|
});
|
|
615
|
-
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
|
|
616
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
617
589
|
|
|
618
590
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
619
591
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -621,63 +593,67 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
621
593
|
const runtime = createRuntime(updateScheduledTaskState, {
|
|
622
594
|
runHistoryStore: history,
|
|
623
595
|
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
596
|
+
maxRunDurationMs: 2_500,
|
|
624
597
|
});
|
|
625
598
|
await runtime.syncProject('project-1');
|
|
626
599
|
|
|
627
600
|
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
628
|
-
await vi.advanceTimersByTimeAsync(
|
|
601
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
602
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
603
|
+
|
|
604
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
629
605
|
const result = await runPromise;
|
|
630
606
|
|
|
631
|
-
expect(result.ok).toBe(
|
|
632
|
-
expect(
|
|
633
|
-
expect(
|
|
607
|
+
expect(result.ok).toBe(false);
|
|
608
|
+
expect(result.status).toBe('error');
|
|
609
|
+
expect(result.error).toBe('schedule run timed out');
|
|
610
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
611
|
+
expect.any(String),
|
|
612
|
+
expect.objectContaining({
|
|
613
|
+
status: 'error',
|
|
614
|
+
error: 'schedule run timed out',
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
634
617
|
|
|
635
|
-
vi.unstubAllGlobals();
|
|
636
618
|
vi.useRealTimers();
|
|
637
619
|
});
|
|
638
620
|
|
|
639
|
-
it('watchdog timeout
|
|
621
|
+
it('watchdog timeout interrupts in-flight work and does not continue later stages', async () => {
|
|
640
622
|
vi.useFakeTimers();
|
|
641
623
|
const history = createHistoryStore();
|
|
642
624
|
let resolvePrompt;
|
|
643
625
|
let promptStarted = false;
|
|
644
626
|
let promptCompleted = false;
|
|
645
|
-
const create = vi.fn(async () => ({
|
|
646
|
-
const
|
|
647
|
-
const abort = vi.fn(async () => ({ data: true }));
|
|
627
|
+
const create = vi.fn(async () => ({ id: 'ses_timeout' }));
|
|
628
|
+
const interrupt = vi.fn(async () => undefined);
|
|
648
629
|
const command = vi.fn(async () => {
|
|
649
630
|
throw new Error('command must not run after hanging prompt path');
|
|
650
631
|
});
|
|
651
632
|
const list = vi.fn(async () => ({ data: [] }));
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
resolve({ ok: true, text: async () => '' });
|
|
633
|
+
const prompt = vi.fn((_params, options) => {
|
|
634
|
+
promptStarted = true;
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
resolvePrompt = () => {
|
|
637
|
+
promptCompleted = true;
|
|
638
|
+
resolve({ id: 'inbox_1', type: 'user' });
|
|
639
|
+
};
|
|
640
|
+
const signal = options?.signal;
|
|
641
|
+
if (signal) {
|
|
642
|
+
const onAbort = () => {
|
|
643
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
664
644
|
};
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
669
|
-
};
|
|
670
|
-
if (signal.aborted) {
|
|
671
|
-
onAbort();
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
645
|
+
if (signal.aborted) {
|
|
646
|
+
onAbort();
|
|
647
|
+
return;
|
|
675
648
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
649
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
654
|
+
session: { create, prompt, command, interrupt },
|
|
655
|
+
command: { list },
|
|
679
656
|
});
|
|
680
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
681
657
|
|
|
682
658
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
683
659
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -706,9 +682,9 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
706
682
|
expect(result.ok).toBe(false);
|
|
707
683
|
expect(result.status).toBe('error');
|
|
708
684
|
expect(result.error).toBe('schedule run timed out');
|
|
709
|
-
expect(
|
|
685
|
+
expect(interrupt).toHaveBeenCalledWith({
|
|
710
686
|
sessionID: 'ses_timeout',
|
|
711
|
-
|
|
687
|
+
continue: false,
|
|
712
688
|
});
|
|
713
689
|
expect(promptCompleted).toBe(false);
|
|
714
690
|
expect(command).not.toHaveBeenCalled();
|
|
@@ -731,18 +707,16 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
731
707
|
} finally {
|
|
732
708
|
process.off('unhandledRejection', onUnhandled);
|
|
733
709
|
resolvePrompt?.();
|
|
734
|
-
vi.unstubAllGlobals();
|
|
735
710
|
vi.useRealTimers();
|
|
736
711
|
}
|
|
737
712
|
});
|
|
738
713
|
|
|
739
|
-
it('watchdog timeout during hanging command
|
|
714
|
+
it('watchdog timeout during hanging command interrupts session and never session.prompt', async () => {
|
|
740
715
|
vi.useFakeTimers();
|
|
741
716
|
const history = createHistoryStore();
|
|
742
717
|
let commandReached = false;
|
|
743
|
-
const create = vi.fn(async () => ({
|
|
744
|
-
const
|
|
745
|
-
const abort = vi.fn(async () => ({ data: true }));
|
|
718
|
+
const create = vi.fn(async () => ({ id: 'ses_cmd' }));
|
|
719
|
+
const interrupt = vi.fn(async () => undefined);
|
|
746
720
|
const command = vi.fn((_params, options) => {
|
|
747
721
|
commandReached = true;
|
|
748
722
|
return new Promise((_resolve, reject) => {
|
|
@@ -761,14 +735,13 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
761
735
|
});
|
|
762
736
|
});
|
|
763
737
|
const list = vi.fn(async () => ({ data: [{ name: 'review' }] }));
|
|
764
|
-
|
|
765
|
-
session
|
|
766
|
-
command: { list },
|
|
738
|
+
const prompt = vi.fn(async () => {
|
|
739
|
+
throw new Error('session.prompt must not run when command is selected');
|
|
767
740
|
});
|
|
768
|
-
|
|
769
|
-
|
|
741
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
742
|
+
session: { create, prompt, command, interrupt },
|
|
743
|
+
command: { list },
|
|
770
744
|
});
|
|
771
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
772
745
|
|
|
773
746
|
const taskWithCommand = {
|
|
774
747
|
...scheduledTask,
|
|
@@ -808,47 +781,41 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
808
781
|
status: 'error',
|
|
809
782
|
error: 'schedule run timed out',
|
|
810
783
|
});
|
|
811
|
-
expect(
|
|
784
|
+
expect(interrupt).toHaveBeenCalledWith({
|
|
812
785
|
sessionID: 'ses_cmd',
|
|
813
|
-
|
|
786
|
+
continue: false,
|
|
814
787
|
});
|
|
815
|
-
expect(
|
|
788
|
+
expect(command).toHaveBeenCalledWith(expect.objectContaining({
|
|
789
|
+
sessionID: 'ses_cmd',
|
|
790
|
+
command: 'review',
|
|
791
|
+
arguments: 'src',
|
|
792
|
+
model: { id: 'gpt-4.1', providerID: 'openai' },
|
|
793
|
+
}), expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
794
|
+
expect(list).toHaveBeenCalledWith({
|
|
795
|
+
location: { directory: '/tmp/project-1' },
|
|
796
|
+
}, expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
797
|
+
expect(prompt).not.toHaveBeenCalled();
|
|
816
798
|
|
|
817
|
-
vi.unstubAllGlobals();
|
|
818
799
|
vi.useRealTimers();
|
|
819
800
|
});
|
|
820
801
|
|
|
821
|
-
it('
|
|
822
|
-
vi.useFakeTimers();
|
|
802
|
+
it('goalEnabled without persist refuses before OpenCode ready / distill / session create (no side effects)', async () => {
|
|
823
803
|
const history = createHistoryStore();
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
const abort = vi.fn(async () => ({ data: true }));
|
|
829
|
-
const command = vi.fn(async () => {
|
|
830
|
-
throw new Error('command must not run after hanging distill');
|
|
804
|
+
const create = vi.fn(async () => ({ id: 'ses_distill' }));
|
|
805
|
+
const interrupt = vi.fn(async () => undefined);
|
|
806
|
+
const prompt = vi.fn(async () => {
|
|
807
|
+
throw new Error('session.prompt must not run without goal persist');
|
|
831
808
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
command: { list },
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
const fetchMock = vi.fn(async (url, init) => {
|
|
839
|
-
throw new Error(`unexpected fetch ${url} ${init?.method}`);
|
|
809
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
810
|
+
session: { create, prompt, command: vi.fn(), interrupt },
|
|
811
|
+
command: { list: vi.fn(async () => ({ data: [] })) },
|
|
840
812
|
});
|
|
841
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
842
813
|
|
|
843
|
-
const generateSmallModelText = vi.fn(() => {
|
|
844
|
-
distillStarted = true;
|
|
845
|
-
return new Promise((resolve) => {
|
|
846
|
-
resolveDistill = () => resolve({ text: 'distilled criteria' });
|
|
847
|
-
});
|
|
848
|
-
});
|
|
814
|
+
const generateSmallModelText = vi.fn(async () => ({ text: 'should not distill' }));
|
|
849
815
|
const getSmallModelService = vi.fn(async () => ({ generateSmallModelText }));
|
|
816
|
+
const waitForOpenCodeReady = vi.fn(async () => {});
|
|
850
817
|
|
|
851
|
-
// Oversized prompt
|
|
818
|
+
// Oversized prompt would force distill if goal were admitted — it must not.
|
|
852
819
|
const largePrompt = `finish the migration ${'x'.repeat(5100)}`;
|
|
853
820
|
const goalTask = {
|
|
854
821
|
...scheduledTask,
|
|
@@ -872,78 +839,161 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
872
839
|
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
873
840
|
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
874
841
|
getSmallModelService,
|
|
875
|
-
waitForOpenCodeReady
|
|
842
|
+
waitForOpenCodeReady,
|
|
876
843
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
877
844
|
runHistoryStore: history,
|
|
878
|
-
|
|
845
|
+
// intentionally no persistSessionGoal
|
|
879
846
|
});
|
|
880
847
|
await runtime.syncProject('project-1');
|
|
848
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
881
849
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
850
|
+
expect(result.ok).toBe(false);
|
|
851
|
+
expect(result.error).toMatch(/session goal persist is not configured/);
|
|
852
|
+
expect(waitForOpenCodeReady).not.toHaveBeenCalled();
|
|
853
|
+
expect(getSmallModelService).not.toHaveBeenCalled();
|
|
854
|
+
expect(generateSmallModelText).not.toHaveBeenCalled();
|
|
855
|
+
expect(create).not.toHaveBeenCalled();
|
|
856
|
+
expect(prompt).not.toHaveBeenCalled();
|
|
857
|
+
expect(writeObjective).not.toHaveBeenCalled();
|
|
858
|
+
expect(history.attachSession).not.toHaveBeenCalled();
|
|
859
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
860
|
+
expect.any(String),
|
|
861
|
+
expect.objectContaining({
|
|
862
|
+
status: 'error',
|
|
863
|
+
error: expect.stringMatching(/session goal persist is not configured/),
|
|
864
|
+
}),
|
|
865
|
+
);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('goalEnabled without persist refuses before session create / objective write', async () => {
|
|
869
|
+
const history = createHistoryStore();
|
|
870
|
+
const client = createSuccessfulClient({ sessionID: 'ses_goal' });
|
|
871
|
+
|
|
872
|
+
const goalTask = {
|
|
873
|
+
...scheduledTask,
|
|
874
|
+
execution: {
|
|
875
|
+
...scheduledTask.execution,
|
|
876
|
+
prompt: 'finish the migration',
|
|
877
|
+
goalEnabled: true,
|
|
878
|
+
goalTokenBudget: 12_000,
|
|
879
|
+
},
|
|
885
880
|
};
|
|
886
|
-
|
|
881
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
882
|
+
task: { ...goalTask, state: { ...goalTask.state, ...state } },
|
|
883
|
+
}));
|
|
884
|
+
const runtime = createScheduledTasksRuntime({
|
|
885
|
+
projectConfigRuntime: {
|
|
886
|
+
listScheduledTasks: vi.fn(async () => [goalTask]),
|
|
887
|
+
updateScheduledTaskState,
|
|
888
|
+
upsertScheduledTask: vi.fn(),
|
|
889
|
+
},
|
|
890
|
+
listProjects: vi.fn(async () => [{ id: 'project-1', path: '/tmp/project-1' }]),
|
|
891
|
+
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
892
|
+
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
893
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
894
|
+
logger: { info: vi.fn(), warn: vi.fn() },
|
|
895
|
+
runHistoryStore: history,
|
|
896
|
+
// intentionally no persistSessionGoal
|
|
897
|
+
});
|
|
898
|
+
await runtime.syncProject('project-1');
|
|
899
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
887
900
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
901
|
+
// Missing persist is an explicit run failure before any side effects.
|
|
902
|
+
expect(result.ok).toBe(false);
|
|
903
|
+
expect(result.status).toBe('error');
|
|
904
|
+
expect(result.error).toMatch(/session goal persist is not configured/);
|
|
905
|
+
expect(client.create).not.toHaveBeenCalled();
|
|
906
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
907
|
+
expect(writeObjective).not.toHaveBeenCalled();
|
|
908
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
909
|
+
expect.any(String),
|
|
910
|
+
expect.objectContaining({
|
|
911
|
+
status: 'error',
|
|
912
|
+
error: expect.stringMatching(/session goal persist is not configured/),
|
|
913
|
+
}),
|
|
914
|
+
);
|
|
915
|
+
// Task config is preserved (goalEnabled stays on the task); only this run fails.
|
|
916
|
+
const finalPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
|
|
917
|
+
expect(finalPatch.lastStatus).toBe('error');
|
|
918
|
+
expect(Object.hasOwn(finalPatch, 'lastSessionId')).toBe(false);
|
|
919
|
+
});
|
|
893
920
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
921
|
+
it('goalEnabled with persist writes objective + active goal then prompts', async () => {
|
|
922
|
+
const history = createHistoryStore();
|
|
923
|
+
const client = createSuccessfulClient({ sessionID: 'ses_goal_ok' });
|
|
924
|
+
const persistSessionGoal = vi.fn(async (_sessionId, _directory, goal) => ({
|
|
925
|
+
openchamber: { goal },
|
|
926
|
+
}));
|
|
927
|
+
const onGoalPersisted = vi.fn();
|
|
897
928
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
929
|
+
const goalTask = {
|
|
930
|
+
...scheduledTask,
|
|
931
|
+
execution: {
|
|
932
|
+
...scheduledTask.execution,
|
|
933
|
+
prompt: 'finish the migration',
|
|
934
|
+
goalEnabled: true,
|
|
935
|
+
goalTokenBudget: 12_000,
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
939
|
+
task: { ...goalTask, state: { ...goalTask.state, ...state } },
|
|
940
|
+
}));
|
|
941
|
+
const runtime = createScheduledTasksRuntime({
|
|
942
|
+
projectConfigRuntime: {
|
|
943
|
+
listScheduledTasks: vi.fn(async () => [goalTask]),
|
|
944
|
+
updateScheduledTaskState,
|
|
945
|
+
upsertScheduledTask: vi.fn(),
|
|
946
|
+
},
|
|
947
|
+
listProjects: vi.fn(async () => [{ id: 'project-1', path: '/tmp/project-1' }]),
|
|
948
|
+
buildOpenCodeUrl: vi.fn(() => 'http://127.0.0.1:4096'),
|
|
949
|
+
getOpenCodeAuthHeaders: vi.fn(() => ({})),
|
|
950
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
951
|
+
logger: { info: vi.fn(), warn: vi.fn() },
|
|
952
|
+
runHistoryStore: history,
|
|
953
|
+
persistSessionGoal,
|
|
954
|
+
onGoalPersisted,
|
|
955
|
+
});
|
|
956
|
+
await runtime.syncProject('project-1');
|
|
957
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
907
958
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
959
|
+
expect(result.ok).toBe(true);
|
|
960
|
+
expect(client.create).toHaveBeenCalled();
|
|
961
|
+
expect(writeObjective).toHaveBeenCalledWith('ses_goal_ok', 'finish the migration');
|
|
962
|
+
expect(persistSessionGoal).toHaveBeenCalledTimes(1);
|
|
963
|
+
expect(persistSessionGoal.mock.calls[0][0]).toBe('ses_goal_ok');
|
|
964
|
+
expect(persistSessionGoal.mock.calls[0][1]).toBe('/tmp/project-1');
|
|
965
|
+
expect(persistSessionGoal.mock.calls[0][2]).toMatchObject({
|
|
966
|
+
status: 'active',
|
|
967
|
+
objectiveFile: true,
|
|
968
|
+
objective: '',
|
|
969
|
+
tokenBudget: 12_000,
|
|
970
|
+
tokensUsed: 0,
|
|
971
|
+
turnsUsed: 0,
|
|
972
|
+
blockedStreak: 0,
|
|
973
|
+
note: '',
|
|
974
|
+
statusReason: '',
|
|
975
|
+
lastAccountedMessageID: '',
|
|
976
|
+
});
|
|
977
|
+
expect(typeof persistSessionGoal.mock.calls[0][2].id).toBe('string');
|
|
978
|
+
expect(persistSessionGoal.mock.calls[0][2].id.length).toBeGreaterThan(0);
|
|
979
|
+
expect(onGoalPersisted).toHaveBeenCalledWith(expect.objectContaining({
|
|
980
|
+
sessionID: 'ses_goal_ok',
|
|
981
|
+
directory: '/tmp/project-1',
|
|
982
|
+
metadata: expect.objectContaining({
|
|
983
|
+
openchamber: expect.objectContaining({
|
|
984
|
+
goal: expect.objectContaining({ status: 'active' }),
|
|
915
985
|
}),
|
|
916
|
-
)
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
resolveDistill?.();
|
|
920
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
921
|
-
await Promise.resolve();
|
|
922
|
-
await Promise.resolve();
|
|
923
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
924
|
-
expect(unhandled).toEqual([]);
|
|
925
|
-
} finally {
|
|
926
|
-
process.off('unhandledRejection', onUnhandled);
|
|
927
|
-
resolveDistill?.();
|
|
928
|
-
vi.unstubAllGlobals();
|
|
929
|
-
vi.useRealTimers();
|
|
930
|
-
}
|
|
986
|
+
}),
|
|
987
|
+
}));
|
|
988
|
+
expect(client.prompt).toHaveBeenCalled();
|
|
931
989
|
});
|
|
932
990
|
|
|
933
|
-
it('goalEnabled
|
|
991
|
+
it('goalEnabled persist failure aborts before prompt', async () => {
|
|
934
992
|
const history = createHistoryStore();
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
if (String(url).includes('/session/ses_goal') && init?.method === 'PATCH') {
|
|
939
|
-
return { ok: true, text: async () => '' };
|
|
940
|
-
}
|
|
941
|
-
if (String(url).includes('prompt_async')) {
|
|
942
|
-
return { ok: true, text: async () => '' };
|
|
943
|
-
}
|
|
944
|
-
throw new Error(`unexpected fetch ${url} ${init?.method}`);
|
|
993
|
+
const client = createSuccessfulClient({ sessionID: 'ses_goal_fail' });
|
|
994
|
+
const persistSessionGoal = vi.fn(async () => {
|
|
995
|
+
throw new Error('store unavailable');
|
|
945
996
|
});
|
|
946
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
947
997
|
|
|
948
998
|
const goalTask = {
|
|
949
999
|
...scheduledTask,
|
|
@@ -951,7 +1001,6 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
951
1001
|
...scheduledTask.execution,
|
|
952
1002
|
prompt: 'finish the migration',
|
|
953
1003
|
goalEnabled: true,
|
|
954
|
-
goalTokenBudget: 12_000,
|
|
955
1004
|
},
|
|
956
1005
|
};
|
|
957
1006
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
@@ -969,42 +1018,225 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
969
1018
|
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
970
1019
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
971
1020
|
runHistoryStore: history,
|
|
1021
|
+
persistSessionGoal,
|
|
1022
|
+
});
|
|
1023
|
+
await runtime.syncProject('project-1');
|
|
1024
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
1025
|
+
|
|
1026
|
+
expect(result.ok).toBe(false);
|
|
1027
|
+
expect(result.error).toMatch(/store unavailable/);
|
|
1028
|
+
expect(writeObjective).toHaveBeenCalled();
|
|
1029
|
+
expect(persistSessionGoal).toHaveBeenCalled();
|
|
1030
|
+
expect(client.prompt).not.toHaveBeenCalled();
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
it('ordinary non-goal runs still create a session and settle on the assistant tail', async () => {
|
|
1034
|
+
const history = createHistoryStore();
|
|
1035
|
+
const client = createSuccessfulClient({ sessionID: 'ses_plain' });
|
|
1036
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1037
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1038
|
+
}));
|
|
1039
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1040
|
+
runHistoryStore: history,
|
|
1041
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
972
1042
|
});
|
|
973
1043
|
await runtime.syncProject('project-1');
|
|
974
1044
|
const result = await runtime.runNow('project-1', 'task-1');
|
|
975
1045
|
|
|
976
1046
|
expect(result.ok).toBe(true);
|
|
977
|
-
expect(client.
|
|
978
|
-
|
|
979
|
-
|
|
1047
|
+
expect(client.create).toHaveBeenCalled();
|
|
1048
|
+
expect(client.prompt).toHaveBeenCalled();
|
|
1049
|
+
expect(writeObjective).not.toHaveBeenCalled();
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
it('settles from the latest-side page: >50 older completed + newest error → error', async () => {
|
|
1053
|
+
const history = createHistoryStore();
|
|
1054
|
+
// order=desc page: newest first. Older completed assistants must not win.
|
|
1055
|
+
const latestPage = [
|
|
1056
|
+
erroredAssistant('msg_latest'),
|
|
1057
|
+
...Array.from({ length: 49 }, (_, index) => completedAssistant(`msg_old_${index}`)),
|
|
1058
|
+
];
|
|
1059
|
+
createSuccessfulClient({
|
|
1060
|
+
messageListImpl: async () => ({ data: latestPage }),
|
|
1061
|
+
});
|
|
1062
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1063
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1064
|
+
}));
|
|
1065
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1066
|
+
runHistoryStore: history,
|
|
1067
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1068
|
+
});
|
|
1069
|
+
await runtime.syncProject('project-1');
|
|
1070
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
1071
|
+
|
|
1072
|
+
expect(result.ok).toBe(false);
|
|
1073
|
+
expect(result.status).toBe('error');
|
|
1074
|
+
expect(result.error).toBe('ProviderError');
|
|
1075
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
1076
|
+
expect.any(String),
|
|
1077
|
+
expect.objectContaining({ status: 'error', error: 'ProviderError' }),
|
|
980
1078
|
);
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('settles from the latest-side page: >50 older errors + newest completed → success', async () => {
|
|
1082
|
+
const history = createHistoryStore();
|
|
1083
|
+
const latestPage = [
|
|
1084
|
+
completedAssistant('msg_latest'),
|
|
1085
|
+
...Array.from({ length: 49 }, (_, index) => erroredAssistant(`msg_old_${index}`)),
|
|
1086
|
+
];
|
|
1087
|
+
createSuccessfulClient({
|
|
1088
|
+
messageListImpl: async () => ({ data: latestPage }),
|
|
988
1089
|
});
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1090
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1091
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1092
|
+
}));
|
|
1093
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1094
|
+
runHistoryStore: history,
|
|
1095
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
992
1096
|
});
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (!body.metadata.openchamber.goal.objectiveFile) {
|
|
996
|
-
expect(body.metadata.openchamber.goal.objective.length).toBeGreaterThan(0);
|
|
997
|
-
}
|
|
998
|
-
expect(client.abort).not.toHaveBeenCalled();
|
|
1097
|
+
await runtime.syncProject('project-1');
|
|
1098
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
999
1099
|
|
|
1000
|
-
|
|
1100
|
+
expect(result.ok).toBe(true);
|
|
1101
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
1102
|
+
expect.any(String),
|
|
1103
|
+
expect.objectContaining({ status: 'success' }),
|
|
1104
|
+
);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it('does not succeed when the latest message is model-switched', async () => {
|
|
1108
|
+
vi.useFakeTimers();
|
|
1109
|
+
const history = createHistoryStore();
|
|
1110
|
+
createSuccessfulClient({
|
|
1111
|
+
messageListImpl: async () => ({
|
|
1112
|
+
data: [{
|
|
1113
|
+
id: 'msg_switch',
|
|
1114
|
+
type: 'model-switched',
|
|
1115
|
+
time: { created: Date.now() },
|
|
1116
|
+
model: { providerID: 'openai', id: 'gpt-4.1' },
|
|
1117
|
+
}],
|
|
1118
|
+
}),
|
|
1119
|
+
});
|
|
1120
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1121
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1122
|
+
}));
|
|
1123
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1124
|
+
runHistoryStore: history,
|
|
1125
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1126
|
+
maxRunDurationMs: 2_500,
|
|
1127
|
+
});
|
|
1128
|
+
await runtime.syncProject('project-1');
|
|
1129
|
+
|
|
1130
|
+
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
1131
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1132
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1133
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
1134
|
+
const result = await runPromise;
|
|
1135
|
+
|
|
1136
|
+
expect(result.ok).toBe(false);
|
|
1137
|
+
expect(result.error).toBe('schedule run timed out');
|
|
1138
|
+
vi.useRealTimers();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it('does not succeed when the assistant finish is tool-calls', async () => {
|
|
1142
|
+
vi.useFakeTimers();
|
|
1143
|
+
const history = createHistoryStore();
|
|
1144
|
+
createSuccessfulClient({
|
|
1145
|
+
messageListImpl: async () => ({
|
|
1146
|
+
data: [{
|
|
1147
|
+
id: 'msg_tools',
|
|
1148
|
+
type: 'assistant',
|
|
1149
|
+
time: { created: 1, completed: 2 },
|
|
1150
|
+
finish: 'tool-calls',
|
|
1151
|
+
}],
|
|
1152
|
+
}),
|
|
1153
|
+
});
|
|
1154
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1155
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1156
|
+
}));
|
|
1157
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1158
|
+
runHistoryStore: history,
|
|
1159
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1160
|
+
maxRunDurationMs: 2_500,
|
|
1161
|
+
});
|
|
1162
|
+
await runtime.syncProject('project-1');
|
|
1163
|
+
|
|
1164
|
+
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
1165
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1166
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1167
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
1168
|
+
const result = await runPromise;
|
|
1169
|
+
|
|
1170
|
+
expect(result.ok).toBe(false);
|
|
1171
|
+
expect(result.error).toBe('schedule run timed out');
|
|
1172
|
+
vi.useRealTimers();
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it('does not succeed after two incomplete assistant idle probes', async () => {
|
|
1176
|
+
vi.useFakeTimers();
|
|
1177
|
+
const history = createHistoryStore();
|
|
1178
|
+
createSuccessfulClient({
|
|
1179
|
+
messageListImpl: async () => ({
|
|
1180
|
+
data: [{
|
|
1181
|
+
id: 'msg_incomplete',
|
|
1182
|
+
type: 'assistant',
|
|
1183
|
+
time: { created: 1 },
|
|
1184
|
+
// no time.completed, no finish
|
|
1185
|
+
}],
|
|
1186
|
+
}),
|
|
1187
|
+
});
|
|
1188
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1189
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1190
|
+
}));
|
|
1191
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1192
|
+
runHistoryStore: history,
|
|
1193
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1194
|
+
maxRunDurationMs: 3_500,
|
|
1195
|
+
});
|
|
1196
|
+
await runtime.syncProject('project-1');
|
|
1197
|
+
|
|
1198
|
+
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
1199
|
+
// Two idle polls with incomplete assistant must not finalize as success.
|
|
1200
|
+
await vi.advanceTimersByTimeAsync(2_000);
|
|
1201
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1202
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
1203
|
+
const result = await runPromise;
|
|
1204
|
+
|
|
1205
|
+
expect(result.ok).toBe(false);
|
|
1206
|
+
expect(result.error).toBe('schedule run timed out');
|
|
1207
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
1208
|
+
expect.any(String),
|
|
1209
|
+
expect.objectContaining({ status: 'error', error: 'schedule run timed out' }),
|
|
1210
|
+
);
|
|
1211
|
+
vi.useRealTimers();
|
|
1001
1212
|
});
|
|
1002
1213
|
|
|
1003
|
-
it('
|
|
1214
|
+
it('lists settlement messages with order=desc (latest-side page)', async () => {
|
|
1215
|
+
const history = createHistoryStore();
|
|
1216
|
+
const client = createSuccessfulClient();
|
|
1217
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1218
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1219
|
+
}));
|
|
1220
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1221
|
+
runHistoryStore: history,
|
|
1222
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1223
|
+
});
|
|
1224
|
+
await runtime.syncProject('project-1');
|
|
1225
|
+
await runtime.runNow('project-1', 'task-1');
|
|
1226
|
+
|
|
1227
|
+
expect(client.messageList).toHaveBeenCalledWith(
|
|
1228
|
+
expect.objectContaining({
|
|
1229
|
+
sessionID: 'ses_1',
|
|
1230
|
+
limit: 50,
|
|
1231
|
+
order: 'desc',
|
|
1232
|
+
}),
|
|
1233
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
1234
|
+
);
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
it('successful runs never call session.interrupt', async () => {
|
|
1004
1238
|
const history = createHistoryStore();
|
|
1005
1239
|
const client = createSuccessfulClient();
|
|
1006
|
-
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
|
|
1007
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
1008
1240
|
|
|
1009
1241
|
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1010
1242
|
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
@@ -1017,9 +1249,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1017
1249
|
const result = await runtime.runNow('project-1', 'task-1');
|
|
1018
1250
|
|
|
1019
1251
|
expect(result.ok).toBe(true);
|
|
1020
|
-
expect(client.
|
|
1021
|
-
|
|
1022
|
-
vi.unstubAllGlobals();
|
|
1252
|
+
expect(client.interrupt).not.toHaveBeenCalled();
|
|
1023
1253
|
});
|
|
1024
1254
|
|
|
1025
1255
|
it('persists lastSessionId when an error run still created a session', async () => {
|
|
@@ -1114,14 +1344,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1114
1344
|
sessionID: 'ses_1',
|
|
1115
1345
|
}));
|
|
1116
1346
|
|
|
1117
|
-
client.
|
|
1118
|
-
data: [
|
|
1119
|
-
info: {
|
|
1120
|
-
id: 'msg_ok',
|
|
1121
|
-
role: 'assistant',
|
|
1122
|
-
time: { completed: Date.now() },
|
|
1123
|
-
},
|
|
1124
|
-
}],
|
|
1347
|
+
client.messageList.mockImplementation(async () => ({
|
|
1348
|
+
data: [completedAssistant('msg_ok')],
|
|
1125
1349
|
}));
|
|
1126
1350
|
|
|
1127
1351
|
await runtime.observeSessionEvent({
|
|
@@ -1165,14 +1389,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1165
1389
|
expect(result.status).toBe('error');
|
|
1166
1390
|
expect(history.finishRun).toHaveBeenCalledTimes(1);
|
|
1167
1391
|
|
|
1168
|
-
client.
|
|
1169
|
-
data: [
|
|
1170
|
-
info: {
|
|
1171
|
-
id: 'msg_ok',
|
|
1172
|
-
role: 'assistant',
|
|
1173
|
-
time: { completed: Date.now() },
|
|
1174
|
-
},
|
|
1175
|
-
}],
|
|
1392
|
+
client.messageList.mockImplementation(async () => ({
|
|
1393
|
+
data: [completedAssistant('msg_ok')],
|
|
1176
1394
|
}));
|
|
1177
1395
|
|
|
1178
1396
|
await runtime.observeSessionEvent({
|
|
@@ -1303,14 +1521,8 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1303
1521
|
const stateWritesAfterRun = updateScheduledTaskState.mock.calls.length;
|
|
1304
1522
|
const successEmitsAfterRun = emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success').length;
|
|
1305
1523
|
|
|
1306
|
-
client.
|
|
1307
|
-
data: [
|
|
1308
|
-
info: {
|
|
1309
|
-
id: 'msg_ok',
|
|
1310
|
-
role: 'assistant',
|
|
1311
|
-
time: { completed: Date.now() },
|
|
1312
|
-
},
|
|
1313
|
-
}],
|
|
1524
|
+
client.messageList.mockImplementation(async () => ({
|
|
1525
|
+
data: [completedAssistant('msg_ok')],
|
|
1314
1526
|
}));
|
|
1315
1527
|
|
|
1316
1528
|
await runtime.observeSessionEvent({
|