1import { describe, expect, it } from "@effect/vitest";
2import * as Schema from "effect/Schema";
3
4import {
5 OrchestratorMcpCreateThreadsInput,
6 OrchestratorMcpDelegateTaskInput,
7 OrchestratorMcpDelegateTaskResult,
8 OrchestratorMcpThreadInterruptInput,
9 OrchestratorMcpThreadListInput,
10 OrchestratorMcpThreadReadInput,
11 OrchestratorMcpThreadSendInput,
12 OrchestratorMcpThreadWaitInput,
13} from "./orchestratorMcp.ts";
14
15const decodeCreateThreadsInput = Schema.decodeUnknownSync(OrchestratorMcpCreateThreadsInput);
16const decodeDelegateTaskInput = Schema.decodeUnknownSync(OrchestratorMcpDelegateTaskInput);
17const decodeDelegateTaskResult = Schema.decodeUnknownSync(OrchestratorMcpDelegateTaskResult);
18const decodeThreadInterruptInput = Schema.decodeUnknownSync(OrchestratorMcpThreadInterruptInput);
19const decodeThreadListInput = Schema.decodeUnknownSync(OrchestratorMcpThreadListInput);
20const decodeThreadReadInput = Schema.decodeUnknownSync(OrchestratorMcpThreadReadInput);
21const decodeThreadSendInput = Schema.decodeUnknownSync(OrchestratorMcpThreadSendInput);
22const decodeThreadWaitInput = Schema.decodeUnknownSync(OrchestratorMcpThreadWaitInput);
23
24describe("orchestrator MCP contracts", () => {
25 it("accepts explicit worktree isolation and keeps shared delegation as the default", () => {
26 expect(decodeDelegateTaskInput({ task: "Review changes" }).isolation).toBeUndefined();
27 expect(
28 decodeDelegateTaskInput({ task: "Review changes", isolation: "worktree" }).isolation,
29 ).toBe("worktree");
30 expect(() => decodeDelegateTaskInput({ task: "Review changes", isolation: "copy" })).toThrow();
31 });
32 it("decodes cross-provider delegated task requests and durable results", () => {
33 const request = decodeDelegateTaskInput({
34 task: "Inspect the workspace and report the result.",
35 target: {
36 providerInstanceId: "claudeAgent",
37 model: "claude-sonnet-4-6",
38 },
39 mode: "wait",
40 timeoutMs: 5_000,
41 clientRequestId: "delegate-1",
42 runtimeMode: "inherit",
43 interactionMode: "inherit",
44 });
45 const result = decodeDelegateTaskResult({
46 taskId: "node-task-1",
47 childThreadId: "thread-child-1",
48 childRunId: "run-child-1",
49 childNodeId: "node-task-1",
50 status: "completed",
51 workState: "result_available",
52 hasPendingChildRuns: false,
53 providerInstanceId: "claudeAgent",
54 model: "claude-sonnet-4-6",
55 summary: "Workspace inspected.",
56 resultContextTransferId: "context-transfer-result-1",
57 latestTerminalRunId: "run-child-1",
58 latestTerminalStatus: "completed",
59 latestTerminalSummary: "Workspace inspected.",
60 latestTerminalResultContextTransferId: "context-transfer-result-1",
61 hasPendingQuestion: false,
62 waitTimedOut: false,
63 });
64
65 expect(request.target?.providerInstanceId).toBe("claudeAgent");
66 expect(result.status).toBe("completed");
67 expect(result.summary).toBe("Workspace inspected.");
68 });
69
70 it("decodes target model options in canonical and shorthand shapes", () => {
71 const canonical = decodeDelegateTaskInput({
72 task: "Say hello.",
73 target: {
74 providerInstanceId: "codex",
75 model: "gpt-5.6-luna",
76 options: [{ id: "reasoning", value: "low" }],
77 },
78 });
79 const shorthand = decodeDelegateTaskInput({
80 task: "Say hello.",
81 target: {
82 providerInstanceId: "codex",
83 model: "gpt-5.6-luna",
84 options: { reasoning: "low", fastMode: true },
85 },
86 });
87
88 expect(canonical.target?.options).toEqual([{ id: "reasoning", value: "low" }]);
89 expect(shorthand.target?.options).toEqual([
90 { id: "reasoning", value: "low" },
91 { id: "fastMode", value: true },
92 ]);
93 });
94
95 it("rejects target model options that are not strings or booleans", () => {
96 expect(() =>
97 decodeDelegateTaskInput({
98 task: "Say hello.",
99 target: {
100 providerInstanceId: "codex",
101 model: "gpt-5.6-luna",
102 // Must fail loudly instead of being dropped like legacy persistence.
103 options: { reasoning: 3 },
104 },
105 }),
106 ).toThrow();
107 expect(() =>
108 decodeDelegateTaskInput({
109 task: "Say hello.",
110 target: {
111 providerInstanceId: "codex",
112 model: "gpt-5.6-luna",
113 options: [{ id: "reasoning", value: null }],
114 },
115 }),
116 ).toThrow();
117 });
118
119 it("decodes mixed prompted and empty thread batches", () => {
120 const request = decodeCreateThreadsInput({
121 clientRequestId: "threads-1",
122 threads: [
123 { title: "Inherited empty thread" },
124 {
125 prompt: "Review the API.",
126 target: { driverKind: "claudeAgent" },
127 runtimeMode: "approval-required",
128 },
129 ],
130 });
131
132 expect(request.threads).toHaveLength(2);
133 expect(request.threads[0]?.prompt).toBeUndefined();
134 expect(request.threads[1]?.target?.driverKind).toBe("claudeAgent");
135 });
136
137 it("decodes project-scoped thread orchestration requests", () => {
138 expect(
139 decodeThreadListInput({
140 statuses: ["running", "completed"],
141 includeSubagents: false,
142 limit: 25,
143 }).statuses,
144 ).toEqual(["running", "completed"]);
145 expect(
146 decodeThreadReadInput({
147 threadId: "thread-loop-1",
148 view: "activity",
149 afterPosition: 10,
150 }).afterPosition,
151 ).toBe(10);
152 expect(
153 decodeThreadSendInput({
154 threadId: "thread-loop-1",
155 message: "Continue with the next iteration.",
156 mode: "steer",
157 clientRequestId: "send-loop-2",
158 }).mode,
159 ).toBe("steer");
160 expect(
161 decodeThreadWaitInput({
162 threadId: "thread-loop-1",
163 runId: "run-loop-2",
164 timeoutMs: 5_000,
165 }).runId,
166 ).toBe("run-loop-2");
167 expect(
168 decodeThreadInterruptInput({
169 threadId: "thread-loop-1",
170 reason: "Loop converged.",
171 }).reason,
172 ).toBe("Loop converged.");
173 });
174});