@danielblomma/cortex-mcp 2.0.5 → 2.0.7

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.
@@ -0,0 +1,247 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { composeStageEnvelope } from "../dist/core/workflow/envelope.js";
8
+ import { createRun, advanceStage } from "../dist/core/workflow/run-lifecycle.js";
9
+
10
+ function makeWorkspace() {
11
+ return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-envelope-"));
12
+ }
13
+
14
+ const TINY_WORKFLOW = {
15
+ id: "tiny",
16
+ description: "Two-stage workflow used for tests",
17
+ version: 1,
18
+ stages: [
19
+ {
20
+ name: "plan",
21
+ artifact: "plan.md",
22
+ reads: [],
23
+ required_fields: ["files_targeted"],
24
+ capability: "planner",
25
+ description: "Produce a step-by-step plan.",
26
+ },
27
+ {
28
+ name: "review",
29
+ artifact: "review.md",
30
+ reads: ["plan"],
31
+ required_fields: ["approved", "blocking_comments"],
32
+ capability: "reviewer",
33
+ description: "Review the plan for soundness.",
34
+ },
35
+ ],
36
+ };
37
+
38
+ function startRun(cwd, taskId) {
39
+ return createRun({
40
+ cwd,
41
+ taskId,
42
+ workflow: TINY_WORKFLOW,
43
+ taskDescription: "Wire up the new dashboard endpoint.",
44
+ });
45
+ }
46
+
47
+ function completePlanStage(cwd, taskId) {
48
+ return advanceStage({
49
+ cwd,
50
+ taskId,
51
+ workflow: TINY_WORKFLOW,
52
+ stageName: "plan",
53
+ artifactName: "plan.md",
54
+ frontmatter: {
55
+ stage: "plan",
56
+ status: "complete",
57
+ references: [],
58
+ files_targeted: ["src/foo.ts"],
59
+ },
60
+ body: "# Plan\n\n1. Add endpoint\n2. Wire route",
61
+ });
62
+ }
63
+
64
+ test("composeStageEnvelope: first stage has no handoffs and labels first-stage explicitly", () => {
65
+ const cwd = makeWorkspace();
66
+ const taskId = "envelope-first";
67
+ startRun(cwd, taskId);
68
+
69
+ const envelope = composeStageEnvelope({ cwd, taskId, workflow: TINY_WORKFLOW });
70
+
71
+ assert.equal(envelope.expectedArtifact, "plan.md");
72
+ assert.deepEqual(envelope.requiredFields, ["files_targeted"]);
73
+ assert.equal(envelope.capability, "planner");
74
+ assert.match(envelope.prompt, /# TASK/);
75
+ assert.match(envelope.prompt, /Wire up the new dashboard endpoint\./);
76
+ assert.match(envelope.prompt, /# STAGE: plan/);
77
+ assert.match(envelope.prompt, /Running under capability: `planner`/);
78
+ assert.match(envelope.prompt, /No prior-stage artifacts/);
79
+ assert.match(envelope.prompt, /# OUTPUT/);
80
+ assert.match(envelope.prompt, /`files_targeted`/);
81
+ });
82
+
83
+ test("composeStageEnvelope: mid-flow stage inlines prior artifact verbatim", () => {
84
+ const cwd = makeWorkspace();
85
+ const taskId = "envelope-mid";
86
+ startRun(cwd, taskId);
87
+ completePlanStage(cwd, taskId);
88
+
89
+ const envelope = composeStageEnvelope({ cwd, taskId, workflow: TINY_WORKFLOW });
90
+
91
+ assert.equal(envelope.expectedArtifact, "review.md");
92
+ assert.deepEqual(envelope.requiredFields, ["approved", "blocking_comments"]);
93
+ assert.match(envelope.prompt, /# STAGE: review/);
94
+ // Handoff section uses fenced markers and contains the planning frontmatter + body.
95
+ assert.match(envelope.prompt, /--- handoff:plan \(plan\.md\) ---/);
96
+ assert.match(envelope.prompt, /--- end handoff:plan ---/);
97
+ assert.match(envelope.prompt, /stage: plan/);
98
+ assert.match(envelope.prompt, /1\. Add endpoint/);
99
+ });
100
+
101
+ test("composeStageEnvelope: explicit stageName overrides current_stage for dry-run", () => {
102
+ const cwd = makeWorkspace();
103
+ const taskId = "envelope-explicit";
104
+ startRun(cwd, taskId);
105
+
106
+ const envelope = composeStageEnvelope({
107
+ cwd,
108
+ taskId,
109
+ workflow: TINY_WORKFLOW,
110
+ stageName: "plan",
111
+ });
112
+ assert.match(envelope.prompt, /# STAGE: plan/);
113
+ });
114
+
115
+ test("composeStageEnvelope: throws when run hasn't been created", () => {
116
+ const cwd = makeWorkspace();
117
+ assert.throws(
118
+ () => composeStageEnvelope({ cwd, taskId: "ghost", workflow: TINY_WORKFLOW }),
119
+ /No run state/,
120
+ );
121
+ });
122
+
123
+ test("composeStageEnvelope: throws when stage not in workflow", () => {
124
+ const cwd = makeWorkspace();
125
+ const taskId = "envelope-unknown";
126
+ startRun(cwd, taskId);
127
+
128
+ assert.throws(
129
+ () =>
130
+ composeStageEnvelope({
131
+ cwd,
132
+ taskId,
133
+ workflow: TINY_WORKFLOW,
134
+ stageName: "nonexistent",
135
+ }),
136
+ /not defined in workflow/,
137
+ );
138
+ });
139
+
140
+ test("composeStageEnvelope: throws when prior handoff artifact hasn't been produced", () => {
141
+ const cwd = makeWorkspace();
142
+ const taskId = "envelope-missing";
143
+ startRun(cwd, taskId);
144
+ // Skip the plan stage — go straight to asking for a review envelope. Plan
145
+ // is still pending so its artifact doesn't exist.
146
+ assert.throws(
147
+ () =>
148
+ composeStageEnvelope({
149
+ cwd,
150
+ taskId,
151
+ workflow: TINY_WORKFLOW,
152
+ stageName: "review",
153
+ }),
154
+ /requires artifact from plan, but it has not been produced yet/,
155
+ );
156
+ });
157
+
158
+ test("composeStageEnvelope: throws when stage declares reads from unknown stage", () => {
159
+ const cwd = makeWorkspace();
160
+ const taskId = "envelope-bad-reads";
161
+ const broken = {
162
+ ...TINY_WORKFLOW,
163
+ stages: [
164
+ TINY_WORKFLOW.stages[0],
165
+ { ...TINY_WORKFLOW.stages[1], reads: ["does-not-exist"] },
166
+ ],
167
+ };
168
+ createRun({
169
+ cwd,
170
+ taskId,
171
+ workflow: broken,
172
+ taskDescription: "Test bad reads",
173
+ });
174
+ advanceStage({
175
+ cwd,
176
+ taskId,
177
+ workflow: broken,
178
+ stageName: "plan",
179
+ artifactName: "plan.md",
180
+ frontmatter: { stage: "plan", status: "complete", references: [] },
181
+ body: "# Plan",
182
+ });
183
+
184
+ assert.throws(
185
+ () =>
186
+ composeStageEnvelope({
187
+ cwd,
188
+ taskId,
189
+ workflow: broken,
190
+ stageName: "review",
191
+ }),
192
+ /declares reads from unknown stage/,
193
+ );
194
+ });
195
+
196
+ test("composeStageEnvelope: surfaces lack of capability gracefully", () => {
197
+ const cwd = makeWorkspace();
198
+ const taskId = "envelope-no-capability";
199
+ const noCapability = {
200
+ ...TINY_WORKFLOW,
201
+ stages: [
202
+ { ...TINY_WORKFLOW.stages[0], capability: undefined },
203
+ TINY_WORKFLOW.stages[1],
204
+ ],
205
+ };
206
+ createRun({
207
+ cwd,
208
+ taskId,
209
+ workflow: noCapability,
210
+ taskDescription: "Test no capability",
211
+ });
212
+
213
+ const envelope = composeStageEnvelope({
214
+ cwd,
215
+ taskId,
216
+ workflow: noCapability,
217
+ });
218
+ assert.equal(envelope.capability, null);
219
+ assert.match(envelope.prompt, /No capability constraint declared/);
220
+ });
221
+
222
+ test("composeStageEnvelope: throws when run is already finished", () => {
223
+ const cwd = makeWorkspace();
224
+ const taskId = "envelope-finished";
225
+ startRun(cwd, taskId);
226
+ completePlanStage(cwd, taskId);
227
+ advanceStage({
228
+ cwd,
229
+ taskId,
230
+ workflow: TINY_WORKFLOW,
231
+ stageName: "review",
232
+ artifactName: "review.md",
233
+ frontmatter: {
234
+ stage: "review",
235
+ status: "complete",
236
+ references: ["plan.md"],
237
+ approved: true,
238
+ blocking_comments: 0,
239
+ },
240
+ body: "# Review\n\nLooks good.",
241
+ });
242
+
243
+ assert.throws(
244
+ () => composeStageEnvelope({ cwd, taskId, workflow: TINY_WORKFLOW }),
245
+ /not at any stage/,
246
+ );
247
+ });
@@ -0,0 +1,293 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ WorkflowStartInput,
9
+ WorkflowAdvanceInput,
10
+ WorkflowStatusInput,
11
+ WorkflowEnvelopeInput,
12
+ runWorkflowStart,
13
+ runWorkflowAdvance,
14
+ runWorkflowStatus,
15
+ runWorkflowEnvelope,
16
+ } from "../dist/core/workflow/mcp-tools.js";
17
+ import { SECURE_BUILD_WORKFLOW } from "../dist/core/workflow/default-workflows.js";
18
+
19
+ function makeWorkspace() {
20
+ return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-mcp-tools-"));
21
+ }
22
+
23
+ const TINY_WORKFLOW = {
24
+ id: "tiny",
25
+ description: "Two-stage tests workflow",
26
+ version: 1,
27
+ stages: [
28
+ {
29
+ name: "plan",
30
+ artifact: "plan.md",
31
+ reads: [],
32
+ required_fields: [],
33
+ capability: "planner",
34
+ description: "Produce a plan.",
35
+ },
36
+ {
37
+ name: "review",
38
+ artifact: "review.md",
39
+ reads: ["plan"],
40
+ required_fields: ["approved"],
41
+ capability: "reviewer",
42
+ description: "Review the plan.",
43
+ },
44
+ ],
45
+ };
46
+
47
+ const TINY_REGISTRY = { tiny: TINY_WORKFLOW };
48
+
49
+ test("input schemas: workflow_id defaults to secure-build", () => {
50
+ const parsed = WorkflowStartInput.parse({
51
+ task_id: "abc",
52
+ task_description: "y",
53
+ });
54
+ assert.equal(parsed.workflow_id, "secure-build");
55
+ });
56
+
57
+ test("input schemas: advance requires stage + body", () => {
58
+ assert.throws(() => WorkflowAdvanceInput.parse({ task_id: "abc" }));
59
+ assert.throws(() =>
60
+ WorkflowAdvanceInput.parse({ task_id: "abc", stage: "plan", body: "" }),
61
+ );
62
+ });
63
+
64
+ test("input schemas: status accepts only task_id", () => {
65
+ const parsed = WorkflowStatusInput.parse({ task_id: "abc" });
66
+ assert.equal(parsed.task_id, "abc");
67
+ });
68
+
69
+ test("input schemas: envelope stage is optional", () => {
70
+ const parsed = WorkflowEnvelopeInput.parse({ task_id: "abc" });
71
+ assert.equal(parsed.stage, undefined);
72
+ });
73
+
74
+ test("runWorkflowStart: creates run and returns the first envelope", () => {
75
+ const cwd = makeWorkspace();
76
+ const result = runWorkflowStart(
77
+ {
78
+ task_id: "task-1",
79
+ task_description: "Add login flow",
80
+ workflow_id: "tiny",
81
+ },
82
+ { cwd, workflows: TINY_REGISTRY },
83
+ );
84
+
85
+ assert.equal(result.state.task_id, "task-1");
86
+ assert.equal(result.state.current_stage, "plan");
87
+ assert.equal(result.state.outcome, "in_progress");
88
+ assert.equal(result.envelope.expectedArtifact, "plan.md");
89
+ assert.match(result.envelope.prompt, /# STAGE: plan/);
90
+
91
+ // state.json persisted under .agents/<task-id>/
92
+ const statePath = path.join(cwd, ".agents", "task-1", "state.json");
93
+ assert.ok(fs.existsSync(statePath));
94
+ });
95
+
96
+ test("runWorkflowStart: rejects unknown workflow_id", () => {
97
+ const cwd = makeWorkspace();
98
+ assert.throws(
99
+ () =>
100
+ runWorkflowStart(
101
+ { task_id: "task-1", task_description: "x", workflow_id: "nope" },
102
+ { cwd, workflows: TINY_REGISTRY },
103
+ ),
104
+ /Unknown workflow_id/,
105
+ );
106
+ });
107
+
108
+ test("runWorkflowStart: defaults to bundled DEFAULT_WORKFLOWS when no registry given", () => {
109
+ const cwd = makeWorkspace();
110
+ const result = runWorkflowStart(
111
+ {
112
+ task_id: "task-1",
113
+ task_description: "Use the default workflow",
114
+ workflow_id: SECURE_BUILD_WORKFLOW.id,
115
+ },
116
+ { cwd },
117
+ );
118
+ assert.equal(result.state.workflow_id, "secure-build");
119
+ assert.equal(result.envelope.expectedArtifact, "plan.md");
120
+ });
121
+
122
+ test("runWorkflowAdvance: writes artifact + state, returns next envelope while in_progress", () => {
123
+ const cwd = makeWorkspace();
124
+ runWorkflowStart(
125
+ { task_id: "task-2", task_description: "Multi-stage", workflow_id: "tiny" },
126
+ { cwd, workflows: TINY_REGISTRY },
127
+ );
128
+
129
+ const advance = runWorkflowAdvance(
130
+ {
131
+ task_id: "task-2",
132
+ stage: "plan",
133
+ frontmatter: {},
134
+ body: "# Plan\n\nDetailed plan body.",
135
+ },
136
+ { cwd, workflows: TINY_REGISTRY },
137
+ );
138
+
139
+ assert.equal(advance.state.current_stage, "review");
140
+ assert.equal(advance.state.outcome, "in_progress");
141
+ assert.equal(advance.state.stages[0].status, "complete");
142
+ assert.equal(advance.state.stages[0].artifact, "plan.md");
143
+ assert.ok(advance.next_envelope);
144
+ assert.equal(advance.next_envelope.expectedArtifact, "review.md");
145
+ // The handoff renders the plan artifact inline.
146
+ assert.match(advance.next_envelope.prompt, /--- handoff:plan \(plan\.md\) ---/);
147
+ });
148
+
149
+ test("runWorkflowAdvance: returns next_envelope=null when run completes", () => {
150
+ const cwd = makeWorkspace();
151
+ runWorkflowStart(
152
+ { task_id: "task-3", task_description: "x", workflow_id: "tiny" },
153
+ { cwd, workflows: TINY_REGISTRY },
154
+ );
155
+ runWorkflowAdvance(
156
+ { task_id: "task-3", stage: "plan", frontmatter: {}, body: "# Plan" },
157
+ { cwd, workflows: TINY_REGISTRY },
158
+ );
159
+
160
+ const finalAdvance = runWorkflowAdvance(
161
+ {
162
+ task_id: "task-3",
163
+ stage: "review",
164
+ frontmatter: { approved: true },
165
+ body: "# Review\n\nLooks good.",
166
+ outcome: { approved: true },
167
+ },
168
+ { cwd, workflows: TINY_REGISTRY },
169
+ );
170
+
171
+ assert.equal(finalAdvance.state.current_stage, null);
172
+ assert.equal(finalAdvance.state.outcome, "complete");
173
+ assert.equal(finalAdvance.next_envelope, null);
174
+ });
175
+
176
+ test("runWorkflowAdvance: blocked status halts the run and returns null envelope", () => {
177
+ const cwd = makeWorkspace();
178
+ runWorkflowStart(
179
+ { task_id: "task-4", task_description: "x", workflow_id: "tiny" },
180
+ { cwd, workflows: TINY_REGISTRY },
181
+ );
182
+
183
+ const result = runWorkflowAdvance(
184
+ {
185
+ task_id: "task-4",
186
+ stage: "plan",
187
+ frontmatter: {},
188
+ body: "# Plan blocked\n\nMissing context.",
189
+ status: "blocked",
190
+ },
191
+ { cwd, workflows: TINY_REGISTRY },
192
+ );
193
+
194
+ assert.equal(result.state.outcome, "blocked");
195
+ assert.equal(result.state.current_stage, null);
196
+ assert.equal(result.next_envelope, null);
197
+ });
198
+
199
+ test("runWorkflowAdvance: rejects when no run exists", () => {
200
+ const cwd = makeWorkspace();
201
+ assert.throws(
202
+ () =>
203
+ runWorkflowAdvance(
204
+ {
205
+ task_id: "ghost",
206
+ stage: "plan",
207
+ frontmatter: {},
208
+ body: "# Plan",
209
+ },
210
+ { cwd, workflows: TINY_REGISTRY },
211
+ ),
212
+ /No run state/,
213
+ );
214
+ });
215
+
216
+ test("runWorkflowAdvance: auto-derives references from stage.reads when caller omits them", () => {
217
+ const cwd = makeWorkspace();
218
+ runWorkflowStart(
219
+ { task_id: "task-5", task_description: "x", workflow_id: "tiny" },
220
+ { cwd, workflows: TINY_REGISTRY },
221
+ );
222
+ runWorkflowAdvance(
223
+ { task_id: "task-5", stage: "plan", frontmatter: {}, body: "# Plan" },
224
+ { cwd, workflows: TINY_REGISTRY },
225
+ );
226
+ runWorkflowAdvance(
227
+ {
228
+ task_id: "task-5",
229
+ stage: "review",
230
+ frontmatter: { approved: false },
231
+ body: "# Review",
232
+ },
233
+ { cwd, workflows: TINY_REGISTRY },
234
+ );
235
+
236
+ const reviewArtifact = path.join(cwd, ".agents", "task-5", "review.md");
237
+ const text = fs.readFileSync(reviewArtifact, "utf8");
238
+ assert.match(text, /references:/);
239
+ assert.match(text, /- plan\.md/);
240
+ });
241
+
242
+ test("runWorkflowStatus: returns state for an active run", () => {
243
+ const cwd = makeWorkspace();
244
+ runWorkflowStart(
245
+ { task_id: "task-6", task_description: "x", workflow_id: "tiny" },
246
+ { cwd, workflows: TINY_REGISTRY },
247
+ );
248
+
249
+ const result = runWorkflowStatus({ task_id: "task-6" }, { cwd, workflows: TINY_REGISTRY });
250
+ assert.equal(result.state?.task_id, "task-6");
251
+ assert.equal(result.state?.current_stage, "plan");
252
+ });
253
+
254
+ test("runWorkflowStatus: returns null state when nothing exists", () => {
255
+ const cwd = makeWorkspace();
256
+ const result = runWorkflowStatus({ task_id: "no-run" }, { cwd, workflows: TINY_REGISTRY });
257
+ assert.equal(result.state, null);
258
+ });
259
+
260
+ test("runWorkflowEnvelope: returns current envelope without mutating state", () => {
261
+ const cwd = makeWorkspace();
262
+ runWorkflowStart(
263
+ { task_id: "task-7", task_description: "x", workflow_id: "tiny" },
264
+ { cwd, workflows: TINY_REGISTRY },
265
+ );
266
+
267
+ const before = runWorkflowStatus({ task_id: "task-7" }, { cwd, workflows: TINY_REGISTRY });
268
+ const envelope = runWorkflowEnvelope(
269
+ { task_id: "task-7" },
270
+ { cwd, workflows: TINY_REGISTRY },
271
+ );
272
+ const after = runWorkflowStatus({ task_id: "task-7" }, { cwd, workflows: TINY_REGISTRY });
273
+
274
+ assert.equal(envelope.envelope.expectedArtifact, "plan.md");
275
+ assert.deepEqual(before.state, after.state);
276
+ });
277
+
278
+ test("runWorkflowEnvelope: explicit stage overrides current_stage for dry-run", () => {
279
+ const cwd = makeWorkspace();
280
+ runWorkflowStart(
281
+ { task_id: "task-8", task_description: "x", workflow_id: "tiny" },
282
+ { cwd, workflows: TINY_REGISTRY },
283
+ );
284
+
285
+ // Dry-run review even though we're at plan — should fail because review
286
+ // requires plan's artifact to exist.
287
+ assert.throws(() =>
288
+ runWorkflowEnvelope(
289
+ { task_id: "task-8", stage: "review" },
290
+ { cwd, workflows: TINY_REGISTRY },
291
+ ),
292
+ );
293
+ });