@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.
Files changed (195) hide show
  1. package/README.md +13 -11
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +165 -0
  6. package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
  7. package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
  8. package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
  10. package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
  13. package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
  15. package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
  16. package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
  17. package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
  19. package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
  20. package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
  21. package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
  22. package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
  24. package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
  25. package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
  26. package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
  27. package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
  28. package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
  29. package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
  30. package/dist/assets/es-c52LTHHe.js +15 -0
  31. package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
  32. package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
  33. package/dist/assets/gitApi-ButwzQ0N.js +9 -0
  34. package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
  36. package/dist/assets/ja-C8Ikun3Z.js +15 -0
  37. package/dist/assets/ko-ClFys5o2.js +15 -0
  38. package/dist/assets/main-CCE_4y5F.js +2 -0
  39. package/dist/assets/main-Dn9rpsgl.js +169 -0
  40. package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
  41. package/dist/assets/miniChat-BpjxVqgg.js +2 -0
  42. package/dist/assets/mobile-nMJCaNq1.js +2 -0
  43. package/dist/assets/multirun-BvSG94mW.js +4 -0
  44. package/dist/assets/pl-CnaKWN5F.js +15 -0
  45. package/dist/assets/platform-CWTIp02q.js +4 -0
  46. package/dist/assets/projectMeta-DOe49dM_.js +3 -0
  47. package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
  48. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
  49. package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
  50. package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
  51. package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
  52. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
  53. package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
  54. package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
  55. package/dist/assets/src-BzX5UI-c.css +2 -0
  56. package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
  57. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
  58. package/dist/assets/textarea-3XR8sUfy.js +1 -0
  59. package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
  60. package/dist/assets/uk-CaX0iqkB.js +15 -0
  61. package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
  62. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
  63. package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
  64. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
  66. package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
  67. package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
  68. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  69. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  70. package/dist/assets/zh-CN-20MYEjha.js +15 -0
  71. package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
  72. package/dist/index.html +6 -6
  73. package/dist/mini-chat.html +6 -6
  74. package/dist/mobile.html +6 -6
  75. package/package.json +2 -2
  76. package/server/index.js +107 -0
  77. package/server/lib/assistants/DOCUMENTATION.md +18 -18
  78. package/server/lib/assistants/assign.js +2 -1
  79. package/server/lib/assistants/contact-store.js +37 -19
  80. package/server/lib/assistants/projection-history.test.js +207 -0
  81. package/server/lib/assistants/routes.js +5 -5
  82. package/server/lib/assistants/service.js +493 -123
  83. package/server/lib/assistants/service.test.js +203 -133
  84. package/server/lib/conversations/DOCUMENTATION.md +4 -4
  85. package/server/lib/conversations/service.js +86 -57
  86. package/server/lib/conversations/service.test.js +116 -100
  87. package/server/lib/event-stream/global-hub.js +2 -1
  88. package/server/lib/llm/DOCUMENTATION.md +71 -84
  89. package/server/lib/llm/catalog.js +111 -45
  90. package/server/lib/llm/catalog.test.js +121 -59
  91. package/server/lib/llm/completions.js +37 -12
  92. package/server/lib/llm/completions.test.js +45 -4
  93. package/server/lib/llm/generate.js +421 -450
  94. package/server/lib/llm/generate.test.js +553 -487
  95. package/server/lib/llm/routes.js +6 -4
  96. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  97. package/server/lib/message-queue/opencode-adapter.js +191 -104
  98. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  99. package/server/lib/message-queue/runtime.test.js +6 -6
  100. package/server/lib/message-queue/worker.js +7 -1
  101. package/server/lib/opencode/DOCUMENTATION.md +28 -5
  102. package/server/lib/opencode/config-entity-routes.js +84 -13
  103. package/server/lib/opencode/config-entity-routes.test.js +41 -10
  104. package/server/lib/opencode/ensure-cli.js +239 -0
  105. package/server/lib/opencode/ensure-cli.test.js +119 -0
  106. package/server/lib/opencode/env-runtime.js +193 -110
  107. package/server/lib/opencode/env-runtime.test.js +221 -17
  108. package/server/lib/opencode/feature-routes-runtime.js +37 -1
  109. package/server/lib/opencode/lifecycle.js +204 -70
  110. package/server/lib/opencode/lifecycle.test.js +376 -6
  111. package/server/lib/opencode/network-runtime.js +28 -16
  112. package/server/lib/opencode/network-runtime.test.js +68 -4
  113. package/server/lib/opencode/opencode2-pin.js +139 -0
  114. package/server/lib/opencode/opencode2-pin.test.js +87 -0
  115. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
  116. package/server/lib/opencode/plugin-compatibility.js +15 -0
  117. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  118. package/server/lib/opencode/plugins.js +4 -3
  119. package/server/lib/opencode/plugins.test.js +12 -0
  120. package/server/lib/opencode/proxy.js +153 -16
  121. package/server/lib/opencode/proxy.test.js +120 -0
  122. package/server/lib/opencode/routes.js +25 -32
  123. package/server/lib/opencode/routes.test.js +51 -52
  124. package/server/lib/opencode/server-utils-runtime.js +2 -0
  125. package/server/lib/opencode/skill-routes.js +6 -7
  126. package/server/lib/opencode/skill-routes.test.js +36 -5
  127. package/server/lib/opencode/v1-migration-gate.js +157 -0
  128. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  129. package/server/lib/opencode/v2-client.js +49 -0
  130. package/server/lib/opencode/watcher.js +2 -1
  131. package/server/lib/opencode/watcher.test.js +1 -1
  132. package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
  133. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  134. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  135. package/server/lib/scheduled-tasks/routes.js +27 -0
  136. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  137. package/server/lib/scheduled-tasks/runtime.js +331 -308
  138. package/server/lib/scheduled-tasks/runtime.test.js +530 -318
  139. package/server/lib/session-goal/DOCUMENTATION.md +65 -14
  140. package/server/lib/session-goal/capability.js +57 -0
  141. package/server/lib/session-goal/capability.test.js +33 -0
  142. package/server/lib/session-goal/routes.js +231 -2
  143. package/server/lib/session-goal/routes.test.js +211 -0
  144. package/server/lib/session-goal/runtime.js +46 -4
  145. package/server/lib/session-goal/runtime.test.js +62 -1
  146. package/server/lib/session-index/sync-runtime.js +20 -17
  147. package/server/lib/session-index/sync-runtime.test.js +9 -8
  148. package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
  149. package/server/lib/session-metadata/routes.js +94 -0
  150. package/server/lib/session-metadata/routes.test.js +111 -0
  151. package/server/lib/session-metadata/session-metadata-store.js +235 -0
  152. package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
  153. package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
  154. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  155. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  156. package/server/lib/session-turn-pages/routes.js +204 -71
  157. package/server/lib/session-turn-pages/routes.test.js +285 -0
  158. package/server/lib/session-turn-pages/service.js +6 -2
  159. package/server/lib/session-turn-pages/service.test.js +34 -0
  160. package/server/lib/small-model/DOCUMENTATION.md +5 -4
  161. package/server/lib/small-model/catalog.js +56 -6
  162. package/server/lib/small-model/catalog.test.js +47 -0
  163. package/server/lib/small-model/index.js +3 -3
  164. package/server/lib/small-model/opencode-session.js +50 -206
  165. package/server/lib/small-model/opencode-session.test.js +58 -88
  166. package/server/opencode-proxy.test.js +61 -12
  167. package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
  168. package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
  169. package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
  170. package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
  171. package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
  172. package/dist/assets/es-rqMiRP7-.js +0 -15
  173. package/dist/assets/gitApi-CcIuekQ3.js +0 -7
  174. package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
  175. package/dist/assets/ja-Bgr8-dOg.js +0 -15
  176. package/dist/assets/ko-B_Gxh-0l.js +0 -15
  177. package/dist/assets/main-CFLmpNC5.js +0 -169
  178. package/dist/assets/main-Ckj_IpUy.js +0 -2
  179. package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
  180. package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
  181. package/dist/assets/multirun-BGUKIJys.js +0 -4
  182. package/dist/assets/pl-DW2XJGC4.js +0 -15
  183. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  184. package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
  185. package/dist/assets/pt-BR-CIsanf4T.js +0 -15
  186. package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
  187. package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
  188. package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
  189. package/dist/assets/src-rS3zCcsC.css +0 -2
  190. package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
  191. package/dist/assets/uk-BPmPEUYJ.js +0 -15
  192. package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
  193. package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
  194. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  195. package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
@@ -1,3 +1,8 @@
1
+ import {
2
+ getSessionGoalCapability,
3
+ isSessionGoalSupported,
4
+ sessionGoalUnavailableMessage,
5
+ } from '../session-goal/capability.js';
1
6
  import { MANAGED_SCHEDULED_TASK_TOOL_PATH } from './managed-tool-contract.js';
2
7
 
3
8
  const MAX_ID_LENGTH = 512;
@@ -263,6 +268,17 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
263
268
  : new Set(['taskId', 'name', 'enabled', 'schedule', 'execution']), 'input');
264
269
  validateTaskInputFields(input);
265
270
 
271
+ const refuseGoalExecution = (execution) => {
272
+ if (execution?.goalEnabled !== true || isSessionGoalSupported()) {
273
+ return null;
274
+ }
275
+ const capability = getSessionGoalCapability();
276
+ const error = new Error(sessionGoalUnavailableMessage(capability));
277
+ error.statusCode = 501;
278
+ error.capability = capability;
279
+ return error;
280
+ };
281
+
266
282
  if (operation === 'update') {
267
283
  const patch = {
268
284
  ...(input.name === undefined ? {} : { name: input.name }),
@@ -270,6 +286,8 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
270
286
  ...(input.schedule === undefined ? {} : { schedule: input.schedule }),
271
287
  ...(input.execution === undefined ? {} : { execution: input.execution }),
272
288
  };
289
+ const goalRefusal = refuseGoalExecution(patch.execution);
290
+ if (goalRefusal) throw goalRefusal;
273
291
  const patched = await projectConfigRuntime.patchScheduledTask(project.id, taskID, patch);
274
292
  if (!patched?.task) return res.status(404).json({ error: 'Schedule not found' });
275
293
  const schedulerSynced = await syncAfterMutation(project.id, operation);
@@ -285,6 +303,8 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
285
303
  if (operation === 'create' && (!execution.providerID || !execution.modelID)) {
286
304
  throw new Error('execution.providerID and execution.modelID are required from the user message or input');
287
305
  }
306
+ const goalRefusal = refuseGoalExecution(execution);
307
+ if (goalRefusal) throw goalRefusal;
288
308
  const taskInput = {
289
309
  ...(input.name === undefined ? {} : { name: input.name }),
290
310
  ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
@@ -296,7 +316,17 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
296
316
  return res.status(201).json({ projectId: project.id, created: upserted.created, task: upserted.task, tasks: upserted.tasks, schedulerSynced });
297
317
  } catch (error) {
298
318
  const statusCode = error?.statusCode || (isValidationError(error?.message || '') ? 400 : 500);
299
- if (statusCode >= 500) logger.error?.('[ScheduledTaskTool] request failed', { operation: req.body?.operation, statusCode });
319
+ if (statusCode >= 500 && statusCode !== 501) {
320
+ logger.error?.('[ScheduledTaskTool] request failed', { operation: req.body?.operation, statusCode });
321
+ }
322
+ if (statusCode === 501) {
323
+ const capability = error?.capability || getSessionGoalCapability();
324
+ return res.status(501).json({
325
+ error: error?.message || sessionGoalUnavailableMessage(capability),
326
+ reason: capability.reason,
327
+ capability,
328
+ });
329
+ }
300
330
  return res.status(statusCode).json({ error: statusCode >= 500 ? 'Failed to manage schedule' : (error?.message || 'Invalid schedule request') });
301
331
  }
302
332
  });
@@ -265,4 +265,27 @@ describe('managed scheduled task tool route', () => {
265
265
  expect(oversized.status).toBe(413);
266
266
  } finally { fixture.restore(); }
267
267
  });
268
+
269
+ it('allows create/update with goalEnabled when Host goal state is supported', async () => {
270
+ const fixture = createApp();
271
+ try {
272
+ const created = await request(fixture.app).post(MANAGED_SCHEDULED_TASK_TOOL_PATH).send(body('create', {
273
+ name: 'Goal task',
274
+ schedule: { kind: 'daily', time: '10:00', timezone: 'UTC' },
275
+ execution: { prompt: 'review', goalEnabled: true },
276
+ }));
277
+ expect(created.status).toBe(201);
278
+ expect(fixture.projectConfigRuntime.upsertScheduledTask).toHaveBeenCalled();
279
+ expect(fixture.projectConfigRuntime.upsertScheduledTask.mock.calls[0][1]).toMatchObject({
280
+ execution: expect.objectContaining({ goalEnabled: true }),
281
+ });
282
+
283
+ const updated = await request(fixture.app).post(MANAGED_SCHEDULED_TASK_TOOL_PATH).send(body('update', {
284
+ taskId: 'task_1',
285
+ execution: { goalEnabled: true },
286
+ }));
287
+ expect(updated.status).toBe(200);
288
+ expect(fixture.projectConfigRuntime.patchScheduledTask).toHaveBeenCalled();
289
+ } finally { fixture.restore(); }
290
+ });
268
291
  });
@@ -1,3 +1,9 @@
1
+ import {
2
+ getSessionGoalCapability,
3
+ isSessionGoalSupported,
4
+ sessionGoalUnavailableMessage,
5
+ } from '../session-goal/capability.js';
6
+
1
7
  const asNonEmptyString = (value) => {
2
8
  if (typeof value !== 'string') {
3
9
  return null;
@@ -6,6 +12,22 @@ const asNonEmptyString = (value) => {
6
12
  return trimmed.length > 0 ? trimmed : null;
7
13
  };
8
14
 
15
+ /** Reject create/edit that enables goal when Host goal state is unavailable. */
16
+ const rejectUnsupportedGoalExecution = (taskInput) => {
17
+ if (taskInput?.execution?.goalEnabled !== true) {
18
+ return null;
19
+ }
20
+ if (isSessionGoalSupported()) {
21
+ return null;
22
+ }
23
+ const capability = getSessionGoalCapability();
24
+ return {
25
+ error: sessionGoalUnavailableMessage(capability),
26
+ reason: capability.reason,
27
+ capability,
28
+ };
29
+ };
30
+
9
31
  const parseProjectID = (req) => asNonEmptyString(req?.params?.projectId);
10
32
  const parseTaskID = (req) => asNonEmptyString(req?.params?.taskId);
11
33
 
@@ -182,6 +204,11 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
182
204
  return res.status(404).json({ error: 'Project not found' });
183
205
  }
184
206
 
207
+ const goalRefusal = rejectUnsupportedGoalExecution(taskInput);
208
+ if (goalRefusal) {
209
+ return res.status(501).json(goalRefusal);
210
+ }
211
+
185
212
  const upserted = await projectConfigRuntime.upsertScheduledTask(projectID, taskInput);
186
213
  const schedulerSynced = await syncAfterMutation(projectID);
187
214
 
@@ -359,4 +359,98 @@ describe('scheduled task mutation routes', () => {
359
359
  await Promise.resolve();
360
360
  expect(scheduledTasksRuntime.syncProject).toHaveBeenCalledTimes(4);
361
361
  });
362
+
363
+ it('allows create/edit with goalEnabled when Host goal state is supported', async () => {
364
+ const savedTask = {
365
+ id: 'goal-task',
366
+ name: 'Goal task',
367
+ execution: { prompt: 'do the work', goalEnabled: true },
368
+ };
369
+ const projectConfigRuntime = {
370
+ upsertScheduledTask: vi.fn(async () => ({ task: savedTask, tasks: [savedTask], created: true })),
371
+ deleteScheduledTask: vi.fn(),
372
+ listScheduledTasks: vi.fn(),
373
+ };
374
+ const routes = registerMutationRoutes({
375
+ projectConfigRuntime,
376
+ scheduledTasksRuntime: { syncProject: vi.fn(async () => {}) },
377
+ scheduleSyncRetry: vi.fn(),
378
+ });
379
+
380
+ const created = createResponse();
381
+ await routes.put({
382
+ params: { projectId: 'project-a' },
383
+ body: {
384
+ task: {
385
+ name: 'Goal task',
386
+ execution: {
387
+ prompt: 'do the work',
388
+ providerID: 'openai',
389
+ modelID: 'gpt',
390
+ goalEnabled: true,
391
+ },
392
+ },
393
+ },
394
+ }, created);
395
+
396
+ expect(created.statusCode).toBe(200);
397
+ expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(1);
398
+ expect(projectConfigRuntime.upsertScheduledTask.mock.calls[0][1]).toMatchObject({
399
+ execution: expect.objectContaining({ goalEnabled: true }),
400
+ });
401
+
402
+ const updatedTask = { ...savedTask, id: 'existing', name: 'Existing' };
403
+ projectConfigRuntime.upsertScheduledTask.mockResolvedValueOnce({
404
+ task: updatedTask,
405
+ tasks: [updatedTask],
406
+ created: false,
407
+ });
408
+ const updated = createResponse();
409
+ await routes.put({
410
+ params: { projectId: 'project-a' },
411
+ body: {
412
+ task: {
413
+ id: 'existing',
414
+ name: 'Existing',
415
+ execution: {
416
+ prompt: 'do the work',
417
+ providerID: 'openai',
418
+ modelID: 'gpt',
419
+ goalEnabled: true,
420
+ },
421
+ },
422
+ },
423
+ }, updated);
424
+
425
+ expect(updated.statusCode).toBe(200);
426
+ expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(2);
427
+ });
428
+
429
+ it('allows ordinary non-goal task create when goal is unsupported', async () => {
430
+ const savedTask = { id: 'plain', name: 'Plain' };
431
+ const projectConfigRuntime = {
432
+ upsertScheduledTask: vi.fn(async () => ({ task: savedTask, tasks: [savedTask], created: true })),
433
+ deleteScheduledTask: vi.fn(),
434
+ listScheduledTasks: vi.fn(),
435
+ };
436
+ const routes = registerMutationRoutes({
437
+ projectConfigRuntime,
438
+ scheduledTasksRuntime: { syncProject: vi.fn(async () => {}) },
439
+ scheduleSyncRetry: vi.fn(),
440
+ });
441
+
442
+ const response = createResponse();
443
+ await routes.put({
444
+ params: { projectId: 'project-a' },
445
+ body: {
446
+ task: {
447
+ name: 'Plain',
448
+ execution: { prompt: 'hello', providerID: 'openai', modelID: 'gpt' },
449
+ },
450
+ },
451
+ }, response);
452
+
453
+ expect(response.statusCode).toBe(200);
454
+ expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(1);
455
+ });
362
456
  });