@danielblomma/cortex-mcp 2.0.15 → 2.0.18
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.
- package/README.md +5 -3
- package/package.json +1 -1
- package/scaffold/mcp/package-lock.json +59 -60
- package/scaffold/mcp/package.json +2 -2
- package/scaffold/mcp/src/cli/stage.ts +15 -18
- package/scaffold/mcp/src/cli/telemetry-test.ts +2 -1
- package/scaffold/mcp/src/core/license.ts +3 -2
- package/scaffold/mcp/src/core/telemetry/collector.ts +5 -4
- package/scaffold/mcp/src/core/telemetry/state-dir.ts +40 -0
- package/scaffold/mcp/src/core/validators/builtins.ts +24 -4
- package/scaffold/mcp/src/core/workflow/index.ts +1 -0
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +15 -25
- package/scaffold/mcp/src/core/workflow/resolution.ts +113 -0
- package/scaffold/mcp/src/daemon/main.ts +4 -3
- package/scaffold/mcp/src/enterprise/index.ts +16 -1
- package/scaffold/mcp/src/enterprise/reviews/policy-selection.ts +65 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +55 -4
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +305 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +40 -47
- package/scaffold/mcp/src/enterprise/tools/harness.ts +2 -0
- package/scaffold/mcp/src/enterprise/workflow/state.ts +21 -0
- package/scaffold/mcp/tests/enterprise-review-trust.test.mjs +318 -0
- package/scaffold/mcp/tests/fixtures/org-skillz-contract.json +59 -0
- package/scaffold/mcp/tests/fixtures/review-trust-contract.json +111 -0
- package/scaffold/mcp/tests/review-policy-selection.test.mjs +85 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +167 -0
- package/scaffold/mcp/tests/review-validator-source.test.mjs +52 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +277 -0
- package/scaffold/mcp/tests/telemetry-collector.test.mjs +31 -1
- package/scaffold/mcp/tests/workflow-cli.test.mjs +102 -1
- package/scaffold/mcp/tests/workflow-mcp-tools.test.mjs +22 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +56 -0
- package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { partitionReviewPolicies } from "../dist/enterprise/reviews/policy-selection.js";
|
|
5
|
+
import { DEFERRED_CODE_REVIEW_REASON } from "../dist/enterprise/reviews/policy-selection.js";
|
|
6
|
+
import "../dist/core/validators/builtins.js";
|
|
7
|
+
|
|
8
|
+
test("partitionReviewPolicies: defers require-code-review with explicit evidence", () => {
|
|
9
|
+
const result = partitionReviewPolicies([
|
|
10
|
+
{
|
|
11
|
+
id: "require-code-review",
|
|
12
|
+
description: "Code review is required",
|
|
13
|
+
priority: 10,
|
|
14
|
+
scope: "global",
|
|
15
|
+
enforce: true,
|
|
16
|
+
source: "org",
|
|
17
|
+
kind: "predefined",
|
|
18
|
+
type: null,
|
|
19
|
+
config: null,
|
|
20
|
+
severity: "block",
|
|
21
|
+
status: "active",
|
|
22
|
+
title: "Require code review",
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(result.enforced, []);
|
|
27
|
+
assert.deepEqual(result.skipped, [
|
|
28
|
+
{
|
|
29
|
+
policy_id: "require-code-review",
|
|
30
|
+
kind: "predefined",
|
|
31
|
+
type: null,
|
|
32
|
+
reason: DEFERRED_CODE_REVIEW_REASON,
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("partitionReviewPolicies: keeps executable validators enforced and skips missing evaluators", () => {
|
|
38
|
+
const result = partitionReviewPolicies([
|
|
39
|
+
{
|
|
40
|
+
id: "no-secrets-in-code",
|
|
41
|
+
description: "Block secrets",
|
|
42
|
+
priority: 10,
|
|
43
|
+
scope: "global",
|
|
44
|
+
enforce: true,
|
|
45
|
+
source: "org",
|
|
46
|
+
kind: "predefined",
|
|
47
|
+
type: null,
|
|
48
|
+
config: null,
|
|
49
|
+
severity: "block",
|
|
50
|
+
status: "active",
|
|
51
|
+
title: "No secrets",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "custom.missing",
|
|
55
|
+
description: "Missing evaluator",
|
|
56
|
+
priority: 20,
|
|
57
|
+
scope: "global",
|
|
58
|
+
enforce: true,
|
|
59
|
+
source: "org",
|
|
60
|
+
kind: "custom",
|
|
61
|
+
type: "regex-custom",
|
|
62
|
+
config: { pattern: "secret" },
|
|
63
|
+
severity: "warning",
|
|
64
|
+
status: "active",
|
|
65
|
+
title: "Missing custom evaluator",
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(result.enforced, [
|
|
70
|
+
{
|
|
71
|
+
id: "no-secrets-in-code",
|
|
72
|
+
type: null,
|
|
73
|
+
config: null,
|
|
74
|
+
severity: "block",
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
assert.deepEqual(result.skipped, [
|
|
78
|
+
{
|
|
79
|
+
policy_id: "custom.missing",
|
|
80
|
+
kind: "custom",
|
|
81
|
+
type: "regex-custom",
|
|
82
|
+
reason: 'No evaluator registered for type "regex-custom"',
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { SECURE_BUILD_WORKFLOW } from "../dist/core/workflow/default-workflows.js";
|
|
9
|
+
import {
|
|
10
|
+
loadReviewTrustState,
|
|
11
|
+
recordQueuedReviewTrustState,
|
|
12
|
+
recordReviewDeliveryStatus,
|
|
13
|
+
} from "../dist/enterprise/reviews/trust-state.js";
|
|
14
|
+
import { syncedWorkflowsCachePath } from "../dist/core/workflow/synced-registry.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const reviewFixture = JSON.parse(
|
|
18
|
+
fs.readFileSync(
|
|
19
|
+
path.resolve(__dirname, "./fixtures/review-trust-contract.json"),
|
|
20
|
+
"utf8",
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
function makeProject() {
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-review-contract-"));
|
|
26
|
+
const contextDir = path.join(root, ".context");
|
|
27
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
28
|
+
const taskDir = path.join(root, ".agents", "task-1");
|
|
29
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
path.join(taskDir, "state.json"),
|
|
32
|
+
JSON.stringify(
|
|
33
|
+
{
|
|
34
|
+
workflow_id: "secure-build",
|
|
35
|
+
outcome: "in_progress",
|
|
36
|
+
},
|
|
37
|
+
null,
|
|
38
|
+
2,
|
|
39
|
+
),
|
|
40
|
+
"utf8",
|
|
41
|
+
);
|
|
42
|
+
return { root, contextDir };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function enableSyncedSecureBuild(homeDir) {
|
|
46
|
+
const syncedDir = path.join(homeDir, ".cortex");
|
|
47
|
+
fs.mkdirSync(syncedDir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
syncedWorkflowsCachePath(syncedDir),
|
|
50
|
+
JSON.stringify(
|
|
51
|
+
{
|
|
52
|
+
workflows: {
|
|
53
|
+
"secure-build": {
|
|
54
|
+
workflow_id: "secure-build",
|
|
55
|
+
version: 1,
|
|
56
|
+
updated_at: "2026-05-19T09:30:00.000Z",
|
|
57
|
+
definition: SECURE_BUILD_WORKFLOW,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
null,
|
|
62
|
+
2,
|
|
63
|
+
),
|
|
64
|
+
"utf8",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeContractShape(state) {
|
|
69
|
+
return {
|
|
70
|
+
workflow: {
|
|
71
|
+
id: state.workflow.id,
|
|
72
|
+
source: state.workflow.source,
|
|
73
|
+
},
|
|
74
|
+
last_review_at: state.last_review_at,
|
|
75
|
+
skipped_policies: state.skipped_policies,
|
|
76
|
+
delivery: {
|
|
77
|
+
status: state.delivery.status,
|
|
78
|
+
review_count: state.delivery.review_count,
|
|
79
|
+
last_accepted_at: state.delivery.last_accepted_at,
|
|
80
|
+
last_attempted_at: state.delivery.last_attempted_at,
|
|
81
|
+
last_error: state.delivery.last_error,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test("review trust contract: accepted payload matches the shared cross-repo fixture", () => {
|
|
87
|
+
const { root, contextDir } = makeProject();
|
|
88
|
+
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-review-home-"));
|
|
89
|
+
process.env.HOME = fakeHome;
|
|
90
|
+
enableSyncedSecureBuild(fakeHome);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const accepted = reviewFixture.accepted.workflow_snapshot.snapshot.review_trust;
|
|
94
|
+
recordQueuedReviewTrustState(
|
|
95
|
+
{
|
|
96
|
+
contextDir,
|
|
97
|
+
projectRoot: root,
|
|
98
|
+
repo: reviewFixture.accepted.workflow_snapshot.repo,
|
|
99
|
+
instance_id: reviewFixture.accepted.workflow_snapshot.instanceId,
|
|
100
|
+
session_id: reviewFixture.accepted.workflow_snapshot.sessionId,
|
|
101
|
+
task_id: "task-1",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
reviewedAt: accepted.last_review_at,
|
|
105
|
+
summary: {
|
|
106
|
+
total: accepted.delivery.review_count,
|
|
107
|
+
passed: 2,
|
|
108
|
+
failed: 0,
|
|
109
|
+
warnings: 0,
|
|
110
|
+
skipped: accepted.skipped_policies.length,
|
|
111
|
+
},
|
|
112
|
+
skippedPolicies: accepted.skipped_policies,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
recordReviewDeliveryStatus(contextDir, "accepted", {
|
|
116
|
+
reviewCount: accepted.delivery.review_count,
|
|
117
|
+
attemptedAt: accepted.delivery.last_accepted_at,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const state = loadReviewTrustState(contextDir);
|
|
121
|
+
assert.deepEqual(normalizeContractShape(state), accepted);
|
|
122
|
+
} finally {
|
|
123
|
+
delete process.env.HOME;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("review trust contract: failed payload matches the shared cross-repo fixture", () => {
|
|
128
|
+
const { root, contextDir } = makeProject();
|
|
129
|
+
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-review-home-"));
|
|
130
|
+
process.env.HOME = fakeHome;
|
|
131
|
+
enableSyncedSecureBuild(fakeHome);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const failed = reviewFixture.failed.workflow_snapshot.snapshot.review_trust;
|
|
135
|
+
recordQueuedReviewTrustState(
|
|
136
|
+
{
|
|
137
|
+
contextDir,
|
|
138
|
+
projectRoot: root,
|
|
139
|
+
repo: reviewFixture.failed.workflow_snapshot.repo,
|
|
140
|
+
instance_id: reviewFixture.failed.workflow_snapshot.instanceId,
|
|
141
|
+
session_id: reviewFixture.failed.workflow_snapshot.sessionId,
|
|
142
|
+
task_id: "task-1",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
reviewedAt: failed.last_review_at,
|
|
146
|
+
summary: {
|
|
147
|
+
total: failed.delivery.review_count,
|
|
148
|
+
passed: 1,
|
|
149
|
+
failed: 1,
|
|
150
|
+
warnings: 0,
|
|
151
|
+
skipped: 0,
|
|
152
|
+
},
|
|
153
|
+
skippedPolicies: [],
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
recordReviewDeliveryStatus(contextDir, "failed", {
|
|
157
|
+
reviewCount: failed.delivery.review_count,
|
|
158
|
+
attemptedAt: failed.delivery.last_attempted_at,
|
|
159
|
+
error: failed.delivery.last_error,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const state = loadReviewTrustState(contextDir);
|
|
163
|
+
assert.deepEqual(normalizeContractShape(state), failed);
|
|
164
|
+
} finally {
|
|
165
|
+
delete process.env.HOME;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { runValidators } from "../dist/core/validators/engine.js";
|
|
8
|
+
import "../dist/core/validators/builtins.js";
|
|
9
|
+
|
|
10
|
+
function makeProject() {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-review-validator-"));
|
|
12
|
+
const contextDir = path.join(root, ".context");
|
|
13
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
14
|
+
return { root, contextDir };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("require-code-review reports source none when no evidence exists", async () => {
|
|
18
|
+
const { root, contextDir } = makeProject();
|
|
19
|
+
const result = await runValidators(
|
|
20
|
+
new Set(["require-code-review"]),
|
|
21
|
+
{ projectRoot: root, contextDir, changedFiles: [] },
|
|
22
|
+
{},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
assert.equal(result.results[0].pass, false);
|
|
26
|
+
assert.match(result.results[0].detail ?? "", /Source: none/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("require-code-review reports workflow state as the evidence source", async () => {
|
|
30
|
+
const { root, contextDir } = makeProject();
|
|
31
|
+
fs.mkdirSync(path.join(contextDir, "workflow"), { recursive: true });
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(contextDir, "workflow", "state.json"),
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
last_review: {
|
|
36
|
+
status: "passed",
|
|
37
|
+
reviewed_at: "2026-05-17T10:00:00.000Z",
|
|
38
|
+
reviewed_files: [],
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
"utf8",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const result = await runValidators(
|
|
45
|
+
new Set(["require-code-review"]),
|
|
46
|
+
{ projectRoot: root, contextDir, changedFiles: [] },
|
|
47
|
+
{},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
assert.equal(result.results[0].pass, true);
|
|
51
|
+
assert.match(result.results[0].detail ?? "", /Source: workflow state/);
|
|
52
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
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
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
runSkillSyncForCli,
|
|
10
|
+
runSkillSyncOnce,
|
|
11
|
+
} from "../dist/daemon/skill-sync-checker.js";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const skillFixture = JSON.parse(
|
|
15
|
+
fs.readFileSync(
|
|
16
|
+
path.resolve(__dirname, "./fixtures/org-skillz-contract.json"),
|
|
17
|
+
"utf8",
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function makeWorkspace() {
|
|
22
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-sync-"));
|
|
23
|
+
const contextDir = path.join(cwd, ".context");
|
|
24
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
path.join(contextDir, "enterprise.yml"),
|
|
27
|
+
[
|
|
28
|
+
"enterprise:",
|
|
29
|
+
" api_key: ent_test_12345678",
|
|
30
|
+
" endpoint: https://example.com",
|
|
31
|
+
"",
|
|
32
|
+
].join("\n"),
|
|
33
|
+
"utf8",
|
|
34
|
+
);
|
|
35
|
+
return { cwd, contextDir };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function skillPath(homeDir, cli, name) {
|
|
39
|
+
const root = cli === "codex"
|
|
40
|
+
? path.join(homeDir, ".codex", "skills")
|
|
41
|
+
: path.join(homeDir, ".claude", "skills");
|
|
42
|
+
return path.join(root, name, "SKILL.md");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function daemonStatePath(homeDir) {
|
|
46
|
+
return path.join(homeDir, ".cortex", "skills.local.json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function daemonNotificationPath(homeDir) {
|
|
50
|
+
return path.join(homeDir, ".cortex", ".skills-update-applied.json");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function latestHostAuditFile(cwd) {
|
|
54
|
+
const auditDir = path.join(cwd, ".context", "audit");
|
|
55
|
+
const files = fs.existsSync(auditDir)
|
|
56
|
+
? fs.readdirSync(auditDir).filter((file) => file.startsWith("host-events-"))
|
|
57
|
+
: [];
|
|
58
|
+
assert.ok(files.length > 0, "expected at least one host audit file");
|
|
59
|
+
files.sort();
|
|
60
|
+
return path.join(auditDir, files[files.length - 1]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createFetchStub(state) {
|
|
64
|
+
return async (input) => {
|
|
65
|
+
const url = input instanceof URL
|
|
66
|
+
? input
|
|
67
|
+
: new URL(typeof input === "string" ? input : input.url);
|
|
68
|
+
if (url.pathname === "/api/v1/govern/skills/manifest") {
|
|
69
|
+
const cli = url.searchParams.get("cli");
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
status: 200,
|
|
73
|
+
statusText: "OK",
|
|
74
|
+
json: async () => ({ skills: state.manifests[cli] ?? [] }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (url.pathname.startsWith("/api/v1/govern/skills/")) {
|
|
79
|
+
const name = decodeURIComponent(url.pathname.split("/").pop() ?? "");
|
|
80
|
+
if (!(name in state.bodies)) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
status: 404,
|
|
84
|
+
statusText: "Not Found",
|
|
85
|
+
text: async () => "",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
status: 200,
|
|
91
|
+
statusText: "OK",
|
|
92
|
+
text: async () => state.bodies[name],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new Error(`Unexpected fetch URL: ${url.toString()}`);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test.afterEach(() => {
|
|
101
|
+
delete process.env.HOME;
|
|
102
|
+
globalThis.fetch = undefined;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("runSkillSyncOnce: syncs global and CLI-scoped skills into the correct local roots", async () => {
|
|
106
|
+
const { cwd } = makeWorkspace();
|
|
107
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
108
|
+
process.env.HOME = homeDir;
|
|
109
|
+
|
|
110
|
+
const state = {
|
|
111
|
+
manifests: {
|
|
112
|
+
claude: [
|
|
113
|
+
{
|
|
114
|
+
name: "global-skill",
|
|
115
|
+
scope: "global",
|
|
116
|
+
updated_at: "2026-05-19T08:00:00.000Z",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
codex: [
|
|
120
|
+
{
|
|
121
|
+
name: "global-skill",
|
|
122
|
+
scope: "global",
|
|
123
|
+
updated_at: "2026-05-19T08:00:00.000Z",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "codex-only",
|
|
127
|
+
scope: "cli:codex",
|
|
128
|
+
updated_at: "2026-05-19T08:00:00.000Z",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
bodies: {
|
|
133
|
+
"global-skill": "---\nname: global-skill\ndescription: \"Global\"\n---\n\n# Global\n",
|
|
134
|
+
"codex-only": "---\nname: codex-only\ndescription: \"Codex\"\n---\n\n# Codex\n",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
globalThis.fetch = createFetchStub(state);
|
|
139
|
+
|
|
140
|
+
const outcomes = await runSkillSyncOnce(cwd, ["claude", "codex"]);
|
|
141
|
+
assert.equal(outcomes[0].kind, "synced");
|
|
142
|
+
assert.equal(outcomes[1].kind, "synced");
|
|
143
|
+
|
|
144
|
+
assert.ok(fs.existsSync(skillPath(homeDir, "claude", "global-skill")));
|
|
145
|
+
assert.ok(!fs.existsSync(skillPath(homeDir, "claude", "codex-only")));
|
|
146
|
+
assert.ok(fs.existsSync(skillPath(homeDir, "codex", "global-skill")));
|
|
147
|
+
assert.ok(fs.existsSync(skillPath(homeDir, "codex", "codex-only")));
|
|
148
|
+
|
|
149
|
+
const parsedState = JSON.parse(fs.readFileSync(daemonStatePath(homeDir), "utf8"));
|
|
150
|
+
assert.deepEqual(Object.keys(parsedState.skills).sort(), [
|
|
151
|
+
"claude:global-skill",
|
|
152
|
+
"codex:codex-only",
|
|
153
|
+
"codex:global-skill",
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
const notification = JSON.parse(
|
|
157
|
+
fs.readFileSync(daemonNotificationPath(homeDir), "utf8"),
|
|
158
|
+
);
|
|
159
|
+
assert.equal(notification.cli, "codex");
|
|
160
|
+
|
|
161
|
+
const auditLines = fs
|
|
162
|
+
.readFileSync(latestHostAuditFile(cwd), "utf8")
|
|
163
|
+
.trim()
|
|
164
|
+
.split("\n")
|
|
165
|
+
.map((line) => JSON.parse(line));
|
|
166
|
+
assert.deepEqual(
|
|
167
|
+
auditLines.map((event) => event.event_type),
|
|
168
|
+
["skills_synced", "skills_synced"],
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("runSkillSyncForCli: rewrites a changed skill and reports an unchanged rerun", async () => {
|
|
173
|
+
const { cwd } = makeWorkspace();
|
|
174
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
175
|
+
process.env.HOME = homeDir;
|
|
176
|
+
|
|
177
|
+
const state = {
|
|
178
|
+
manifests: {
|
|
179
|
+
codex: [
|
|
180
|
+
{
|
|
181
|
+
name: "global-skill",
|
|
182
|
+
scope: "global",
|
|
183
|
+
updated_at: "2026-05-19T08:00:00.000Z",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
bodies: {
|
|
188
|
+
"global-skill": "---\nname: global-skill\ndescription: \"V1\"\n---\n\n# One\n",
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
globalThis.fetch = createFetchStub(state);
|
|
192
|
+
|
|
193
|
+
const first = await runSkillSyncForCli(cwd, "codex");
|
|
194
|
+
assert.equal(first.kind, "synced");
|
|
195
|
+
assert.deepEqual(first.added, ["global-skill"]);
|
|
196
|
+
|
|
197
|
+
state.manifests.codex[0].updated_at = "2026-05-19T09:00:00.000Z";
|
|
198
|
+
state.bodies["global-skill"] =
|
|
199
|
+
"---\nname: global-skill\ndescription: \"V2\"\n---\n\n# Two\n";
|
|
200
|
+
|
|
201
|
+
const second = await runSkillSyncForCli(cwd, "codex");
|
|
202
|
+
assert.equal(second.kind, "synced");
|
|
203
|
+
assert.deepEqual(second.changed, ["global-skill"]);
|
|
204
|
+
assert.match(
|
|
205
|
+
fs.readFileSync(skillPath(homeDir, "codex", "global-skill"), "utf8"),
|
|
206
|
+
/# Two/,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const third = await runSkillSyncForCli(cwd, "codex");
|
|
210
|
+
assert.deepEqual(third, {
|
|
211
|
+
kind: "unchanged",
|
|
212
|
+
cli: "codex",
|
|
213
|
+
count: 1,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("runSkillSyncForCli: removes skills that disappear from the manifest", async () => {
|
|
218
|
+
const { cwd } = makeWorkspace();
|
|
219
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
220
|
+
process.env.HOME = homeDir;
|
|
221
|
+
|
|
222
|
+
const state = {
|
|
223
|
+
manifests: {
|
|
224
|
+
claude: [
|
|
225
|
+
{
|
|
226
|
+
name: "global-skill",
|
|
227
|
+
scope: "global",
|
|
228
|
+
updated_at: "2026-05-19T08:00:00.000Z",
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
bodies: {
|
|
233
|
+
"global-skill": "---\nname: global-skill\ndescription: \"Global\"\n---\n\n# Global\n",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
globalThis.fetch = createFetchStub(state);
|
|
237
|
+
|
|
238
|
+
const first = await runSkillSyncForCli(cwd, "claude");
|
|
239
|
+
assert.equal(first.kind, "synced");
|
|
240
|
+
assert.ok(fs.existsSync(skillPath(homeDir, "claude", "global-skill")));
|
|
241
|
+
|
|
242
|
+
state.manifests.claude = [];
|
|
243
|
+
|
|
244
|
+
const second = await runSkillSyncForCli(cwd, "claude");
|
|
245
|
+
assert.equal(second.kind, "synced");
|
|
246
|
+
assert.deepEqual(second.removed, ["global-skill"]);
|
|
247
|
+
assert.ok(!fs.existsSync(skillPath(homeDir, "claude", "global-skill")));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("runSkillSyncOnce: consumes the shared org-skillz contract fixture end-to-end", async () => {
|
|
251
|
+
const { cwd } = makeWorkspace();
|
|
252
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
253
|
+
process.env.HOME = homeDir;
|
|
254
|
+
|
|
255
|
+
globalThis.fetch = createFetchStub({
|
|
256
|
+
manifests: skillFixture.manifest,
|
|
257
|
+
bodies: skillFixture.markdown,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const outcomes = await runSkillSyncOnce(cwd, ["claude", "codex"]);
|
|
261
|
+
assert.equal(outcomes[0].kind, "synced");
|
|
262
|
+
assert.equal(outcomes[1].kind, "synced");
|
|
263
|
+
|
|
264
|
+
assert.equal(
|
|
265
|
+
fs.readFileSync(skillPath(homeDir, "codex", "alpha-codex"), "utf8"),
|
|
266
|
+
skillFixture.markdown["alpha-codex"],
|
|
267
|
+
);
|
|
268
|
+
assert.equal(
|
|
269
|
+
fs.readFileSync(skillPath(homeDir, "codex", "global-guard"), "utf8"),
|
|
270
|
+
skillFixture.markdown["global-guard"],
|
|
271
|
+
);
|
|
272
|
+
assert.equal(
|
|
273
|
+
fs.readFileSync(skillPath(homeDir, "claude", "claude-playbook"), "utf8"),
|
|
274
|
+
skillFixture.markdown["claude-playbook"],
|
|
275
|
+
);
|
|
276
|
+
assert.ok(!fs.existsSync(skillPath(homeDir, "claude", "alpha-codex")));
|
|
277
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdtempSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
@@ -28,3 +28,33 @@ test("TelemetryCollector counts context.impact as an impact analysis", () => {
|
|
|
28
28
|
assert.equal(metrics.impact_analyses, 1);
|
|
29
29
|
assert.equal(metrics.tool_metrics["context.impact"].calls, 1);
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test("TelemetryCollector falls back when .context/telemetry is not writable", () => {
|
|
33
|
+
const contextDir = createContextDir("cortex-telemetry-fallback-");
|
|
34
|
+
const primaryTelemetryDir = path.join(contextDir, "telemetry");
|
|
35
|
+
const fallbackMetricsPath = path.join(
|
|
36
|
+
contextDir,
|
|
37
|
+
"cache",
|
|
38
|
+
"telemetry",
|
|
39
|
+
"metrics.json",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
mkdirSync(primaryTelemetryDir, { recursive: true });
|
|
43
|
+
chmodSync(primaryTelemetryDir, 0o555);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const collector = new TelemetryCollector(contextDir, "test-version");
|
|
47
|
+
collector.recordEvent({
|
|
48
|
+
tool: "context.search",
|
|
49
|
+
phase: "success",
|
|
50
|
+
result_count: 1,
|
|
51
|
+
estimated_tokens_saved: 100,
|
|
52
|
+
duration_ms: 10,
|
|
53
|
+
});
|
|
54
|
+
collector.flush();
|
|
55
|
+
|
|
56
|
+
assert.equal(existsSync(fallbackMetricsPath), true);
|
|
57
|
+
} finally {
|
|
58
|
+
chmodSync(primaryTelemetryDir, 0o755);
|
|
59
|
+
}
|
|
60
|
+
});
|