@danielblomma/cortex-mcp 2.0.5 → 2.0.6

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,283 @@
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
+ parseStageArtifact,
9
+ renderStageArtifact,
10
+ readRunState,
11
+ readStageArtifact,
12
+ } from "../dist/core/workflow/artifact-io.js";
13
+ import {
14
+ createRun,
15
+ advanceStage,
16
+ getRunState,
17
+ } from "../dist/core/workflow/run-lifecycle.js";
18
+ import {
19
+ workflowDefinitionSchema,
20
+ stageArtifactFrontmatterSchema,
21
+ runStateSchema,
22
+ } from "../dist/core/workflow/schemas.js";
23
+ import { SECURE_BUILD_WORKFLOW } from "../dist/core/workflow/default-workflows.js";
24
+
25
+ function makeWorkspace() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-workflow-"));
27
+ }
28
+
29
+ const TINY_WORKFLOW = {
30
+ id: "tiny",
31
+ description: "Two-stage workflow used for tests",
32
+ version: 1,
33
+ stages: [
34
+ {
35
+ name: "plan",
36
+ artifact: "plan.md",
37
+ reads: [],
38
+ required_fields: [],
39
+ description: "Produce a plan",
40
+ },
41
+ {
42
+ name: "review",
43
+ artifact: "review.md",
44
+ reads: ["plan"],
45
+ required_fields: ["approved"],
46
+ description: "Review the plan",
47
+ },
48
+ ],
49
+ };
50
+
51
+ test("schemas: SECURE_BUILD_WORKFLOW validates against workflowDefinitionSchema", () => {
52
+ const parsed = workflowDefinitionSchema.parse(SECURE_BUILD_WORKFLOW);
53
+ assert.equal(parsed.id, "secure-build");
54
+ assert.ok(parsed.stages.length > 0);
55
+ });
56
+
57
+ test("schemas: stage names must be slug-cased", () => {
58
+ assert.throws(() =>
59
+ workflowDefinitionSchema.parse({
60
+ ...TINY_WORKFLOW,
61
+ stages: [
62
+ { ...TINY_WORKFLOW.stages[0], name: "Bad Name" },
63
+ TINY_WORKFLOW.stages[1],
64
+ ],
65
+ }),
66
+ );
67
+ });
68
+
69
+ test("schemas: stage artifact frontmatter requires status + stage", () => {
70
+ assert.throws(() =>
71
+ stageArtifactFrontmatterSchema.parse({
72
+ stage: "plan",
73
+ // status missing
74
+ written_at: new Date().toISOString(),
75
+ }),
76
+ );
77
+ });
78
+
79
+ test("artifact-io: render + parse round-trips frontmatter", () => {
80
+ const fm = stageArtifactFrontmatterSchema.parse({
81
+ stage: "plan",
82
+ status: "complete",
83
+ references: [],
84
+ written_at: "2026-05-06T19:00:00.000Z",
85
+ });
86
+ const text = renderStageArtifact(fm, "# Plan\n\nDo the thing.");
87
+ const parsed = parseStageArtifact(text);
88
+ assert.equal(parsed.frontmatter.stage, "plan");
89
+ assert.equal(parsed.frontmatter.status, "complete");
90
+ assert.equal(parsed.body, "# Plan\n\nDo the thing.");
91
+ });
92
+
93
+ test("artifact-io: parseStageArtifact rejects missing frontmatter", () => {
94
+ assert.throws(() => parseStageArtifact("# No frontmatter here\n"));
95
+ });
96
+
97
+ test("artifact-io: parseStageArtifact rejects unterminated frontmatter", () => {
98
+ assert.throws(() =>
99
+ parseStageArtifact("---\nstage: plan\nstatus: complete\n# no close marker\n"),
100
+ );
101
+ });
102
+
103
+ test("artifact-io: parseStageArtifact preserves passthrough fields", () => {
104
+ const text = `---
105
+ stage: review
106
+ status: complete
107
+ references:
108
+ - plan.md
109
+ written_at: "2026-05-06T19:00:00.000Z"
110
+ approved: true
111
+ blocking_comments: 0
112
+ ---
113
+
114
+ # Review
115
+
116
+ Looks good.
117
+ `;
118
+ const parsed = parseStageArtifact(text);
119
+ assert.equal(parsed.frontmatter.stage, "review");
120
+ assert.deepEqual(parsed.frontmatter.references, ["plan.md"]);
121
+ assert.equal(parsed.frontmatter.approved, true);
122
+ assert.equal(parsed.frontmatter.blocking_comments, 0);
123
+ });
124
+
125
+ test("createRun: writes state.json with all stages pending and current_stage = first", () => {
126
+ const cwd = makeWorkspace();
127
+ const state = createRun({
128
+ cwd,
129
+ taskId: "2026-05-06-fixture",
130
+ workflow: TINY_WORKFLOW,
131
+ taskDescription: "Test run",
132
+ });
133
+
134
+ assert.equal(state.task_id, "2026-05-06-fixture");
135
+ assert.equal(state.current_stage, "plan");
136
+ assert.equal(state.outcome, "in_progress");
137
+ assert.deepEqual(
138
+ state.stages.map((s) => s.status),
139
+ ["pending", "pending"],
140
+ );
141
+
142
+ const persisted = readRunState(cwd, "2026-05-06-fixture");
143
+ assert.deepEqual(persisted, state);
144
+ });
145
+
146
+ test("advanceStage: writes artifact, updates state, advances current_stage", () => {
147
+ const cwd = makeWorkspace();
148
+ const taskId = "2026-05-06-advance";
149
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
150
+
151
+ const after = advanceStage({
152
+ cwd,
153
+ taskId,
154
+ workflow: TINY_WORKFLOW,
155
+ stageName: "plan",
156
+ artifactName: "plan.md",
157
+ frontmatter: { stage: "plan", status: "complete", references: [] },
158
+ body: "# Plan\n\n- step 1\n- step 2",
159
+ });
160
+
161
+ assert.equal(after.current_stage, "review");
162
+ assert.equal(after.outcome, "in_progress");
163
+ assert.equal(after.stages[0].status, "complete");
164
+ assert.equal(after.stages[0].artifact, "plan.md");
165
+ assert.equal(after.stages[1].status, "pending");
166
+
167
+ // Artifact lives on disk under .agents/<taskId>/
168
+ const artifactPath = path.join(cwd, ".agents", taskId, "plan.md");
169
+ assert.ok(fs.existsSync(artifactPath));
170
+ const parsed = readStageArtifact(cwd, taskId, "plan.md");
171
+ assert.equal(parsed.frontmatter.stage, "plan");
172
+ });
173
+
174
+ test("advanceStage: marks run complete after final stage", () => {
175
+ const cwd = makeWorkspace();
176
+ const taskId = "2026-05-06-final";
177
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
178
+
179
+ advanceStage({
180
+ cwd,
181
+ taskId,
182
+ workflow: TINY_WORKFLOW,
183
+ stageName: "plan",
184
+ artifactName: "plan.md",
185
+ frontmatter: { stage: "plan", status: "complete", references: [] },
186
+ body: "# Plan",
187
+ });
188
+
189
+ const after = advanceStage({
190
+ cwd,
191
+ taskId,
192
+ workflow: TINY_WORKFLOW,
193
+ stageName: "review",
194
+ artifactName: "review.md",
195
+ frontmatter: {
196
+ stage: "review",
197
+ status: "complete",
198
+ references: ["plan.md"],
199
+ approved: true,
200
+ },
201
+ body: "# Review\n\napproved",
202
+ outcome: { approved: true },
203
+ });
204
+
205
+ assert.equal(after.current_stage, null);
206
+ assert.equal(after.outcome, "complete");
207
+ assert.ok(after.completed_at);
208
+ assert.deepEqual(after.stages[1].outcome, { approved: true });
209
+ });
210
+
211
+ test("advanceStage: blocked status surfaces as run outcome", () => {
212
+ const cwd = makeWorkspace();
213
+ const taskId = "2026-05-06-blocked";
214
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
215
+
216
+ const after = advanceStage({
217
+ cwd,
218
+ taskId,
219
+ workflow: TINY_WORKFLOW,
220
+ stageName: "plan",
221
+ artifactName: "plan.md",
222
+ frontmatter: { stage: "plan", status: "blocked", references: [] },
223
+ body: "# Plan blocked",
224
+ status: "blocked",
225
+ });
226
+
227
+ assert.equal(after.outcome, "blocked");
228
+ assert.equal(after.current_stage, null);
229
+ });
230
+
231
+ test("advanceStage: refuses to advance the wrong stage", () => {
232
+ const cwd = makeWorkspace();
233
+ const taskId = "2026-05-06-wrong";
234
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
235
+
236
+ assert.throws(() =>
237
+ advanceStage({
238
+ cwd,
239
+ taskId,
240
+ workflow: TINY_WORKFLOW,
241
+ stageName: "review",
242
+ artifactName: "review.md",
243
+ frontmatter: { stage: "review", status: "complete", references: [] },
244
+ body: "# Out of order",
245
+ }),
246
+ );
247
+ });
248
+
249
+ test("getRunState: returns null for missing tasks", () => {
250
+ const cwd = makeWorkspace();
251
+ assert.equal(getRunState(cwd, "no-such-task"), null);
252
+ });
253
+
254
+ test("readRunState: validates persisted state against schema", () => {
255
+ const cwd = makeWorkspace();
256
+ const taskId = "2026-05-06-corrupt";
257
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
258
+
259
+ // Corrupt the file: drop required field.
260
+ const statePath = path.join(cwd, ".agents", taskId, "state.json");
261
+ const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
262
+ delete raw.workflow_id;
263
+ fs.writeFileSync(statePath, JSON.stringify(raw, null, 2));
264
+
265
+ assert.throws(() => readRunState(cwd, taskId));
266
+ });
267
+
268
+ test("runStateSchema: rejects unknown outcome", () => {
269
+ assert.throws(() =>
270
+ runStateSchema.parse({
271
+ schema_version: 1,
272
+ task_id: "x",
273
+ workflow_id: "tiny",
274
+ workflow_version: 1,
275
+ task_description: "y",
276
+ current_stage: null,
277
+ outcome: "totally-bogus",
278
+ started_at: new Date().toISOString(),
279
+ completed_at: null,
280
+ stages: [{ name: "plan", status: "complete" }],
281
+ }),
282
+ );
283
+ });