diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index 6ce02cbb32..c37a02ae35 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -2671,3 +2671,95 @@ describe("OrchestratorMcpService thread lookup", () => { }), ); }); + +describe("scratch #26119 queued-only task_cancel", () => { + it.effect("cancels a held queued-only task and only its own queued run", () => + Effect.gen(function* () { + const parentThreadId = ThreadId.make("thread:s-parent"); + const childThreadId = ThreadId.make("thread:s-child"); + const taskId = NodeId.make("node:s-task"); + const ownRun = RunId.make("run:s-own-held"); + const otherRun = RunId.make("run:s-other-held"); + const dispatched = yield* Ref.make>([]); + const parentProjection = { + thread: { id: parentThreadId }, + runs: [], + contextTransfers: [], + subagents: [ + { + id: taskId, + threadId: parentThreadId, + runId: null, + origin: "app_owned", + childThreadId, + driver: "codex", + model: "gpt-5.6-terra", + status: "idle", + result: null, + completionDelivery: { state: "pending" }, + }, + ], + } as unknown as OrchestrationV2ThreadProjection; + const childProjection = { + thread: { id: childThreadId, title: "Child" }, + runs: [ + { id: RunId.make("run:s-first"), ordinal: 1, status: "interrupted" }, + { id: ownRun, ordinal: 2, status: "queued", queueHeld: true, delegatedTaskId: taskId, startedAt: null }, + { id: otherRun, ordinal: 3, status: "queued", queueHeld: true, startedAt: null }, + ], + contextTransfers: [], + messages: [], + subagents: [], + providerThreads: [], + runtimeRequests: [], + turnItems: [], + } as unknown as OrchestrationV2ThreadProjection; + const layerDependencies = Layer.mergeAll( + NodeServices.layer, + Layer.mock(ThreadManagementService.ThreadManagementService)({ + getThreadRecords: (threadId) => + Effect.succeed(threadId === parentThreadId ? parentProjection : childProjection), + getThreadShell: (threadId) => Effect.succeed(liveThreadShell(threadId)), + delegatedTaskResultPending: () => Effect.succeed(false), + stopDelegatedTasks: () => Effect.void, + dispatch: (command) => + Ref.update(dispatched, (commands) => [...commands, command]).pipe( + Effect.as({} as never), + ), + }), + ServerSettings.layerTest(), + Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }), + Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({ + list: () => Effect.succeed([]), + }), + Layer.mock(ProjectService.ProjectService)({}), + Layer.mock(SecretRequests.SecretRequests)({}), + Layer.mock(ScheduledTaskService.ScheduledTaskService)({}), + ); + const scope: McpInvocationScope = { + environmentId: EnvironmentId.make("environment:s"), + requestNamespace: "provider-session:s", + thread: { + threadId: parentThreadId, + providerSessionId: "provider-session:s", + providerInstanceId: ProviderInstanceId.make("codex"), + }, + client: undefined, + capabilities: new Set(["orchestration"]), + issuedAt: 1, + }; + yield* Effect.gen(function* () { + const service = yield* OrchestratorMcpService.OrchestratorMcpService; + const task = yield* service.taskStatus(scope, taskId); + console.log("SCRATCH task before cancel:", JSON.stringify({ status: task.status, workState: task.workState })); + const result = yield* service.cancelTask(scope, { taskId, clientRequestId: "s-cancel" }); + console.log("SCRATCH cancel result:", JSON.stringify(result)); + const commands = yield* Ref.get(dispatched); + console.log( + "SCRATCH dispatched:", + JSON.stringify(commands.map((c) => ({ type: c.type, ...("runId" in c ? { runId: c.runId } : {}) }))), + ); + }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies)))); + }), + ); +});