@danielblomma/cortex-mcp 2.0.6 → 2.0.8

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,179 @@
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
+ loadSyncedWorkflows,
9
+ syncedWorkflowsCachePath,
10
+ } from "../dist/core/workflow/synced-registry.js";
11
+ import { runWorkflowStart } from "../dist/core/workflow/mcp-tools.js";
12
+ import { SECURE_BUILD_WORKFLOW } from "../dist/core/workflow/default-workflows.js";
13
+
14
+ function makeWorkspace() {
15
+ return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-synced-registry-"));
16
+ }
17
+
18
+ const TINY_WORKFLOW = {
19
+ id: "tiny",
20
+ description: "Two-stage tests workflow",
21
+ version: 1,
22
+ stages: [
23
+ {
24
+ name: "plan",
25
+ artifact: "plan.md",
26
+ reads: [],
27
+ required_fields: [],
28
+ capability: "planner",
29
+ description: "Produce a plan.",
30
+ },
31
+ {
32
+ name: "review",
33
+ artifact: "review.md",
34
+ reads: ["plan"],
35
+ required_fields: ["approved"],
36
+ capability: "reviewer",
37
+ description: "Review the plan.",
38
+ },
39
+ ],
40
+ };
41
+
42
+ function writeCache(dir, payload) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ fs.writeFileSync(
45
+ syncedWorkflowsCachePath(dir),
46
+ JSON.stringify(payload, null, 2),
47
+ "utf8",
48
+ );
49
+ }
50
+
51
+ test("syncedWorkflowsCachePath: defaults to ~/.cortex/workflows.local.json", () => {
52
+ const expected = path.join(os.homedir(), ".cortex", "workflows.local.json");
53
+ assert.equal(syncedWorkflowsCachePath(), expected);
54
+ });
55
+
56
+ test("loadSyncedWorkflows: returns {} when cache is missing", () => {
57
+ const dir = makeWorkspace();
58
+ assert.deepEqual(loadSyncedWorkflows(dir), {});
59
+ });
60
+
61
+ test("loadSyncedWorkflows: returns {} when cache is unreadable JSON", () => {
62
+ const dir = makeWorkspace();
63
+ fs.writeFileSync(syncedWorkflowsCachePath(dir), "not valid json", "utf8");
64
+ assert.deepEqual(loadSyncedWorkflows(dir), {});
65
+ });
66
+
67
+ test("loadSyncedWorkflows: returns {} when 'workflows' key is missing", () => {
68
+ const dir = makeWorkspace();
69
+ writeCache(dir, { last_synced_at: "x" });
70
+ assert.deepEqual(loadSyncedWorkflows(dir), {});
71
+ });
72
+
73
+ test("loadSyncedWorkflows: drops entries whose definition fails schema", () => {
74
+ const dir = makeWorkspace();
75
+ writeCache(dir, {
76
+ workflows: {
77
+ "valid-one": {
78
+ workflow_id: "valid-one",
79
+ version: 1,
80
+ updated_at: "2026-05-06T12:00:00.000Z",
81
+ definition: TINY_WORKFLOW,
82
+ },
83
+ "broken-one": {
84
+ workflow_id: "broken-one",
85
+ version: 1,
86
+ updated_at: "2026-05-06T12:00:00.000Z",
87
+ definition: { id: "broken-one" /* missing stages */ },
88
+ },
89
+ },
90
+ });
91
+ const loaded = loadSyncedWorkflows(dir);
92
+ assert.deepEqual(Object.keys(loaded).sort(), ["valid-one"]);
93
+ });
94
+
95
+ test("loadSyncedWorkflows: returns valid workflow definitions keyed by workflow_id", () => {
96
+ const dir = makeWorkspace();
97
+ writeCache(dir, {
98
+ workflows: {
99
+ tiny: {
100
+ workflow_id: "tiny",
101
+ version: 1,
102
+ updated_at: "2026-05-06T12:00:00.000Z",
103
+ definition: TINY_WORKFLOW,
104
+ },
105
+ },
106
+ });
107
+ const loaded = loadSyncedWorkflows(dir);
108
+ assert.equal(loaded.tiny.id, "tiny");
109
+ assert.equal(loaded.tiny.stages.length, 2);
110
+ });
111
+
112
+ test("resolveWorkflow integration: synced workflow takes precedence over bundled default", () => {
113
+ // We can't easily intercept loadSyncedWorkflows() from inside
114
+ // mcp-tools.ts (it reads from a fixed home-dir path). Instead, exercise
115
+ // the explicit-registry path which mirrors what the merge would do
116
+ // and confirm the contract: passing a registry that includes the same
117
+ // id as a bundled default uses the registry version.
118
+ const cwd = makeWorkspace();
119
+ process.env.HOME = cwd; // sandbox the cache lookup
120
+ try {
121
+ const overridden = {
122
+ ...SECURE_BUILD_WORKFLOW,
123
+ description: "Org-overridden secure-build",
124
+ };
125
+ const registry = { "secure-build": overridden };
126
+ const result = runWorkflowStart(
127
+ {
128
+ task_id: "task-1",
129
+ task_description: "test",
130
+ workflow_id: "secure-build",
131
+ },
132
+ { cwd, workflows: registry },
133
+ );
134
+ assert.equal(result.state.workflow_id, "secure-build");
135
+ // The envelope renders the workflow description verbatim — confirms
136
+ // we got the org-overridden version and not the bundled one.
137
+ assert.match(result.envelope.prompt, /Org-overridden secure-build/);
138
+ } finally {
139
+ delete process.env.HOME;
140
+ }
141
+ });
142
+
143
+ test("resolveWorkflow integration: synced cache adds new workflow_ids beyond defaults", () => {
144
+ const cwd = makeWorkspace();
145
+ // Build a cache under a tmp HOME and point HOME at it, so that the
146
+ // home-dir-based loader picks it up.
147
+ const fakeHome = makeWorkspace();
148
+ process.env.HOME = fakeHome;
149
+ try {
150
+ fs.mkdirSync(path.join(fakeHome, ".cortex"), { recursive: true });
151
+ fs.writeFileSync(
152
+ path.join(fakeHome, ".cortex", "workflows.local.json"),
153
+ JSON.stringify({
154
+ workflows: {
155
+ tiny: {
156
+ workflow_id: "tiny",
157
+ version: 1,
158
+ updated_at: "2026-05-06T12:00:00.000Z",
159
+ definition: TINY_WORKFLOW,
160
+ },
161
+ },
162
+ }),
163
+ "utf8",
164
+ );
165
+
166
+ const result = runWorkflowStart(
167
+ {
168
+ task_id: "task-2",
169
+ task_description: "Use synced workflow",
170
+ workflow_id: "tiny",
171
+ },
172
+ { cwd },
173
+ );
174
+ assert.equal(result.state.workflow_id, "tiny");
175
+ assert.equal(result.state.current_stage, "plan");
176
+ } finally {
177
+ delete process.env.HOME;
178
+ }
179
+ });
@@ -0,0 +1,272 @@
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
+ createRun,
9
+ advanceStage,
10
+ } from "../dist/core/workflow/run-lifecycle.js";
11
+ import { composeStageEnvelope } from "../dist/core/workflow/envelope.js";
12
+ import {
13
+ runWorkflowAdvance,
14
+ runWorkflowStart,
15
+ } from "../dist/core/workflow/mcp-tools.js";
16
+
17
+ function makeWorkspace() {
18
+ return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-validators-"));
19
+ }
20
+
21
+ const WORKFLOW = {
22
+ id: "validated",
23
+ description: "One stage that requires two validators",
24
+ version: 1,
25
+ stages: [
26
+ {
27
+ name: "build",
28
+ artifact: "changes.md",
29
+ reads: [],
30
+ required_fields: [],
31
+ validators: [
32
+ { id: "tests-pass", description: "Test suite must pass" },
33
+ { id: "build-passes", description: "Build must succeed" },
34
+ ],
35
+ capability: "builder",
36
+ description: "Implement the change.",
37
+ },
38
+ ],
39
+ };
40
+
41
+ const REGISTRY = { validated: WORKFLOW };
42
+
43
+ test("advanceStage: blocks when validators_passed misses required ids", () => {
44
+ const cwd = makeWorkspace();
45
+ createRun({
46
+ cwd,
47
+ taskId: "task-1",
48
+ workflow: WORKFLOW,
49
+ taskDescription: "Test",
50
+ });
51
+
52
+ assert.throws(
53
+ () =>
54
+ advanceStage({
55
+ cwd,
56
+ taskId: "task-1",
57
+ workflow: WORKFLOW,
58
+ stageName: "build",
59
+ artifactName: "changes.md",
60
+ frontmatter: { stage: "build", status: "complete", references: [] },
61
+ body: "# Changes",
62
+ validatorsPassed: ["tests-pass"], // missing build-passes
63
+ }),
64
+ /Missing: build-passes/,
65
+ );
66
+ });
67
+
68
+ test("advanceStage: allows when validators_passed covers required ids", () => {
69
+ const cwd = makeWorkspace();
70
+ createRun({
71
+ cwd,
72
+ taskId: "task-1",
73
+ workflow: WORKFLOW,
74
+ taskDescription: "Test",
75
+ });
76
+
77
+ const next = advanceStage({
78
+ cwd,
79
+ taskId: "task-1",
80
+ workflow: WORKFLOW,
81
+ stageName: "build",
82
+ artifactName: "changes.md",
83
+ frontmatter: { stage: "build", status: "complete", references: [] },
84
+ body: "# Changes",
85
+ validatorsPassed: ["tests-pass", "build-passes"],
86
+ });
87
+ assert.equal(next.outcome, "complete");
88
+ assert.deepEqual(next.stages[0].validators_passed, ["tests-pass", "build-passes"]);
89
+ });
90
+
91
+ test("advanceStage: override.skipped_validators bypasses missing-validator block", () => {
92
+ const cwd = makeWorkspace();
93
+ createRun({
94
+ cwd,
95
+ taskId: "task-1",
96
+ workflow: WORKFLOW,
97
+ taskDescription: "Test",
98
+ });
99
+
100
+ const next = advanceStage({
101
+ cwd,
102
+ taskId: "task-1",
103
+ workflow: WORKFLOW,
104
+ stageName: "build",
105
+ artifactName: "changes.md",
106
+ frontmatter: { stage: "build", status: "complete", references: [] },
107
+ body: "# Changes",
108
+ validatorsPassed: ["tests-pass"],
109
+ override: {
110
+ reason: "Build infra is down on CI; skipping per ops-incident-2026-05-06",
111
+ skipped_validators: ["build-passes"],
112
+ },
113
+ });
114
+
115
+ assert.equal(next.outcome, "complete");
116
+ assert.equal(next.stages[0].override?.reason.includes("Build infra is down"), true);
117
+ assert.deepEqual(next.stages[0].override?.skipped_validators, ["build-passes"]);
118
+ });
119
+
120
+ test("advanceStage: blocked status is exempt from validator coverage check", () => {
121
+ const cwd = makeWorkspace();
122
+ createRun({
123
+ cwd,
124
+ taskId: "task-1",
125
+ workflow: WORKFLOW,
126
+ taskDescription: "Test",
127
+ });
128
+
129
+ const next = advanceStage({
130
+ cwd,
131
+ taskId: "task-1",
132
+ workflow: WORKFLOW,
133
+ stageName: "build",
134
+ artifactName: "changes.md",
135
+ frontmatter: { stage: "build", status: "blocked", references: [] },
136
+ body: "# Plan blocked\n\nMissing context.",
137
+ status: "blocked",
138
+ validatorsPassed: [], // exempt
139
+ });
140
+ assert.equal(next.outcome, "blocked");
141
+ });
142
+
143
+ test("advanceStage: override is stamped into the artifact frontmatter", () => {
144
+ const cwd = makeWorkspace();
145
+ createRun({
146
+ cwd,
147
+ taskId: "task-1",
148
+ workflow: WORKFLOW,
149
+ taskDescription: "Test",
150
+ });
151
+
152
+ advanceStage({
153
+ cwd,
154
+ taskId: "task-1",
155
+ workflow: WORKFLOW,
156
+ stageName: "build",
157
+ artifactName: "changes.md",
158
+ frontmatter: { stage: "build", status: "complete", references: [] },
159
+ body: "# Changes",
160
+ validatorsPassed: ["tests-pass"],
161
+ override: {
162
+ reason: "Hot fix, will follow up",
163
+ skipped_validators: ["build-passes"],
164
+ },
165
+ });
166
+
167
+ const text = fs.readFileSync(
168
+ path.join(cwd, ".agents", "task-1", "changes.md"),
169
+ "utf8",
170
+ );
171
+ assert.match(text, /override:/);
172
+ assert.match(text, /reason: Hot fix/);
173
+ assert.match(text, /- build-passes/);
174
+ });
175
+
176
+ test("composeStageEnvelope: renders VALIDATORS section when stage declares them", () => {
177
+ const cwd = makeWorkspace();
178
+ createRun({
179
+ cwd,
180
+ taskId: "task-1",
181
+ workflow: WORKFLOW,
182
+ taskDescription: "Test",
183
+ });
184
+
185
+ const env = composeStageEnvelope({
186
+ cwd,
187
+ taskId: "task-1",
188
+ workflow: WORKFLOW,
189
+ });
190
+
191
+ assert.match(env.prompt, /# VALIDATORS/);
192
+ assert.match(env.prompt, /`tests-pass` — Test suite must pass/);
193
+ assert.match(env.prompt, /`build-passes` — Build must succeed/);
194
+ assert.match(env.prompt, /`validators_passed: \[<id1>, <id2>, \.\.\.\]`/);
195
+ assert.deepEqual(env.validators.map((v) => v.id), ["tests-pass", "build-passes"]);
196
+ });
197
+
198
+ test("composeStageEnvelope: VALIDATORS section is empty when stage has none", () => {
199
+ const cwd = makeWorkspace();
200
+ const noValidators = {
201
+ ...WORKFLOW,
202
+ stages: [{ ...WORKFLOW.stages[0], validators: [] }],
203
+ };
204
+ createRun({
205
+ cwd,
206
+ taskId: "task-1",
207
+ workflow: noValidators,
208
+ taskDescription: "Test",
209
+ });
210
+
211
+ const env = composeStageEnvelope({
212
+ cwd,
213
+ taskId: "task-1",
214
+ workflow: noValidators,
215
+ });
216
+
217
+ assert.match(env.prompt, /No validators required for this stage/);
218
+ assert.equal(env.validators.length, 0);
219
+ });
220
+
221
+ test("runWorkflowAdvance MCP runner: forwards validators_passed + override", () => {
222
+ const cwd = makeWorkspace();
223
+ runWorkflowStart(
224
+ { task_id: "task-1", task_description: "x", workflow_id: "validated" },
225
+ { cwd, workflows: REGISTRY },
226
+ );
227
+
228
+ const result = runWorkflowAdvance(
229
+ {
230
+ task_id: "task-1",
231
+ stage: "build",
232
+ frontmatter: {},
233
+ body: "# Changes",
234
+ validators_passed: ["tests-pass"],
235
+ override: {
236
+ reason: "CI builder offline; manual verification done",
237
+ skipped_validators: ["build-passes"],
238
+ skipped_requirements: [],
239
+ },
240
+ },
241
+ { cwd, workflows: REGISTRY },
242
+ );
243
+
244
+ assert.equal(result.state.outcome, "complete");
245
+ assert.equal(
246
+ result.state.stages[0].override?.reason.includes("CI builder offline"),
247
+ true,
248
+ );
249
+ });
250
+
251
+ test("runWorkflowAdvance MCP runner: rejects missing validators without override", () => {
252
+ const cwd = makeWorkspace();
253
+ runWorkflowStart(
254
+ { task_id: "task-1", task_description: "x", workflow_id: "validated" },
255
+ { cwd, workflows: REGISTRY },
256
+ );
257
+
258
+ assert.throws(
259
+ () =>
260
+ runWorkflowAdvance(
261
+ {
262
+ task_id: "task-1",
263
+ stage: "build",
264
+ frontmatter: {},
265
+ body: "# Changes",
266
+ validators_passed: [],
267
+ },
268
+ { cwd, workflows: REGISTRY },
269
+ ),
270
+ /Missing: tests-pass, build-passes/,
271
+ );
272
+ });