@jaybeeuu/agent-cortex 2.0.0 → 2.1.0

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,147 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ EPIC_GATE_LABEL,
6
+ FEATURE_GATE_LABEL,
7
+ HITL_GATE_LABELS,
8
+ fetchGateState,
9
+ resolveGateState,
10
+ runCli,
11
+ } from '../skills/workflow/ralph/scripts/pending-gates.mjs';
12
+
13
+ function createBead({ id, title = `${id} title` }) {
14
+ return { id, title };
15
+ }
16
+
17
+ /**
18
+ * Fake `bd` for the query-contract tests: answers the three queries ralph makes
19
+ * and refuses anything else, so an unexpected query fails loudly.
20
+ */
21
+ function makeExec({ ready = [], open = [], epics = [] } = {}) {
22
+ const calls = [];
23
+
24
+ const exec = async (args) => {
25
+ calls.push(args);
26
+ if (args[0] === 'ready') return JSON.stringify(ready);
27
+ if (args[0] === 'list' && args.includes(EPIC_GATE_LABEL)) return JSON.stringify(epics);
28
+ if (args[0] === 'list') return JSON.stringify(open);
29
+ throw new Error(`unexpected bd call: ${args.join(' ')}`);
30
+ };
31
+
32
+ return { exec, calls };
33
+ }
34
+
35
+ function capture() {
36
+ let output = '';
37
+ return { write: (chunk) => { output += chunk; }, read: () => JSON.parse(output) };
38
+ }
39
+
40
+ describe('resolveGateState', () => {
41
+ it('leaves dispatch unblocked while every gate is still parked behind its own pipeline', () => {
42
+ const state = resolveGateState({
43
+ readyGates: [],
44
+ openGates: [
45
+ createBead({ id: 'proj-1.5' }),
46
+ createBead({ id: 'proj-2.5' }),
47
+ createBead({ id: 'proj-3.5' }),
48
+ createBead({ id: 'proj-4.5' }),
49
+ ],
50
+ epicsAwaitingMerge: [],
51
+ });
52
+
53
+ assert.deepEqual(state.pendingHumanAction, []);
54
+ assert.deepEqual(state.parked, [
55
+ { id: 'proj-1.5', title: 'proj-1.5 title' },
56
+ { id: 'proj-2.5', title: 'proj-2.5 title' },
57
+ { id: 'proj-3.5', title: 'proj-3.5 title' },
58
+ { id: 'proj-4.5', title: 'proj-4.5 title' },
59
+ ]);
60
+ });
61
+
62
+ it('reports an unblocked gate as pending human action', () => {
63
+ const state = resolveGateState({
64
+ readyGates: [createBead({ id: 'proj-1.5', title: 'PR Review and Merge' })],
65
+ openGates: [createBead({ id: 'proj-1.5', title: 'PR Review and Merge' })],
66
+ epicsAwaitingMerge: [],
67
+ });
68
+
69
+ assert.deepEqual(state.pendingHumanAction, [
70
+ { id: 'proj-1.5', title: 'PR Review and Merge', kind: 'feature-pr' },
71
+ ]);
72
+ assert.deepEqual(state.parked, []);
73
+ });
74
+
75
+ it('separates an unblocked gate from gates that are still parked', () => {
76
+ const state = resolveGateState({
77
+ readyGates: [createBead({ id: 'proj-1.5' })],
78
+ openGates: [createBead({ id: 'proj-1.5' }), createBead({ id: 'proj-2.5' })],
79
+ epicsAwaitingMerge: [],
80
+ });
81
+
82
+ assert.deepEqual(state.pendingHumanAction.map((gate) => gate.id), ['proj-1.5']);
83
+ assert.deepEqual(state.parked.map((gate) => gate.id), ['proj-2.5']);
84
+ });
85
+
86
+ it('reports an epic awaiting merge as pending human action alongside feature gates', () => {
87
+ const state = resolveGateState({
88
+ readyGates: [],
89
+ openGates: [],
90
+ epicsAwaitingMerge: [createBead({ id: 'epic-9', title: 'Merge epic into main' })],
91
+ });
92
+
93
+ assert.deepEqual(state.pendingHumanAction, [
94
+ { id: 'epic-9', title: 'Merge epic into main', kind: 'epic-pr' },
95
+ ]);
96
+ assert.deepEqual(state.parked, []);
97
+ });
98
+ });
99
+
100
+ describe('fetchGateState', () => {
101
+ it('asks bd for unblocked gates, all gates, and epics awaiting merge on either HITL axis', async () => {
102
+ const { exec, calls } = makeExec();
103
+
104
+ await fetchGateState({ exec });
105
+
106
+ assert.deepEqual(calls, [
107
+ ['ready', '--json', '-l', FEATURE_GATE_LABEL, '--label-any', HITL_GATE_LABELS.join(',')],
108
+ ['list', '--json', '--limit', '0', '-l', FEATURE_GATE_LABEL, '--label-any', HITL_GATE_LABELS.join(',')],
109
+ ['list', '--json', '--limit', '0', '-l', EPIC_GATE_LABEL],
110
+ ]);
111
+ });
112
+
113
+ it('covers both the current and the legacy HITL label axis', () => {
114
+ assert.deepEqual(HITL_GATE_LABELS, ['execution:hitl', 'implementation-type:hitl']);
115
+ });
116
+
117
+ it('treats a gate that only the list query returns as parked', async () => {
118
+ const { exec } = makeExec({ ready: [], open: [createBead({ id: 'proj-1.5' })] });
119
+
120
+ const state = await fetchGateState({ exec });
121
+
122
+ assert.deepEqual(state.pendingHumanAction, []);
123
+ assert.deepEqual(state.parked, [{ id: 'proj-1.5', title: 'proj-1.5 title' }]);
124
+ });
125
+ });
126
+
127
+ describe('pending-gates CLI', () => {
128
+ it('prints the resolved gate state as JSON', async () => {
129
+ const { exec } = makeExec({
130
+ ready: [createBead({ id: 'proj-1.5' })],
131
+ open: [createBead({ id: 'proj-1.5' }), createBead({ id: 'proj-2.5' })],
132
+ epics: [createBead({ id: 'epic-9' })],
133
+ });
134
+ const out = capture();
135
+
136
+ const code = await runCli({ argv: [], exec, write: out.write });
137
+
138
+ assert.equal(code, 0);
139
+ assert.deepEqual(out.read(), {
140
+ pendingHumanAction: [
141
+ { id: 'proj-1.5', title: 'proj-1.5 title', kind: 'feature-pr' },
142
+ { id: 'epic-9', title: 'epic-9 title', kind: 'epic-pr' },
143
+ ],
144
+ parked: [{ id: 'proj-2.5', title: 'proj-2.5 title' }],
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,137 @@
1
+ // Behaviour tests for scripts/lib/skill-frontmatter.mjs — the SKILL.md
2
+ // frontmatter check that `pnpm lint` runs. Each case proves one failure mode
3
+ // the check must catch: unparseable YAML, a missing name/description field, and
4
+ // a description longer than the limit.
5
+
6
+ import { describe, it } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import {
13
+ MAX_DESCRIPTION_LENGTH,
14
+ checkSkills,
15
+ validateSkillFrontmatter,
16
+ } from "../scripts/lib/skill-frontmatter.mjs";
17
+
18
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
19
+
20
+ /** Wrap frontmatter lines in a SKILL.md-shaped document. */
21
+ function skillDocument(frontmatter) {
22
+ return `---\n${frontmatter}\n---\n\n# Skill body\n`;
23
+ }
24
+
25
+ /** Throwaway directory to lay out a fake skills tree in. */
26
+ async function makeFixtureRoot() {
27
+ return await mkdtemp(join(tmpdir(), "agent-cortex-skill-frontmatter-"));
28
+ }
29
+
30
+ /** Write a SKILL.md at `<root>/<relativeSkillDir>/SKILL.md`. */
31
+ async function writeSkill(root, relativeSkillDir, content) {
32
+ const dir = join(root, relativeSkillDir);
33
+ await mkdir(dir, { recursive: true });
34
+ await writeFile(join(dir, "SKILL.md"), content);
35
+ }
36
+
37
+ describe("validateSkillFrontmatter", () => {
38
+ it("accepts frontmatter with a name and a description", () => {
39
+ const errors = validateSkillFrontmatter(
40
+ skillDocument("name: example-skill\ndescription: Does a thing. Use when a thing needs doing."),
41
+ );
42
+
43
+ assert.deepEqual(errors, []);
44
+ });
45
+
46
+ it("reports unparseable YAML", () => {
47
+ const errors = validateSkillFrontmatter(
48
+ skillDocument("name: example-skill\ndescription: Use when the plan: breaks"),
49
+ );
50
+
51
+ assert.equal(errors.length, 1);
52
+ assert.match(errors[0], /not valid YAML/);
53
+ });
54
+
55
+ it("reports a missing name", () => {
56
+ const errors = validateSkillFrontmatter(skillDocument("description: Does a thing."));
57
+
58
+ assert.deepEqual(errors, ["frontmatter is missing a non-empty name"]);
59
+ });
60
+
61
+ it("reports a missing description", () => {
62
+ const errors = validateSkillFrontmatter(skillDocument("name: example-skill"));
63
+
64
+ assert.deepEqual(errors, ["frontmatter is missing a non-empty description"]);
65
+ });
66
+
67
+ it("reports a description over the length limit", () => {
68
+ const errors = validateSkillFrontmatter(
69
+ skillDocument(`name: example-skill\ndescription: ${"a".repeat(MAX_DESCRIPTION_LENGTH + 1)}`),
70
+ );
71
+
72
+ assert.equal(errors.length, 1);
73
+ assert.match(errors[0], /description exceeds 1024 characters \(1025\)/);
74
+ });
75
+
76
+ it("accepts a description exactly at the length limit", () => {
77
+ const errors = validateSkillFrontmatter(
78
+ skillDocument(`name: example-skill\ndescription: ${"a".repeat(MAX_DESCRIPTION_LENGTH)}`),
79
+ );
80
+
81
+ assert.deepEqual(errors, []);
82
+ });
83
+
84
+ it("reports a file with no frontmatter block", () => {
85
+ const errors = validateSkillFrontmatter("# Skill body\n\nNo frontmatter here.\n");
86
+
87
+ assert.deepEqual(errors, ["frontmatter block is missing (expected '---' delimiters)"]);
88
+ });
89
+ });
90
+
91
+ describe("checkSkills", () => {
92
+ it("reports each failing SKILL.md and counts every file it checked", async () => {
93
+ const root = await makeFixtureRoot();
94
+ try {
95
+ await writeSkill(
96
+ root,
97
+ "skills/good",
98
+ skillDocument("name: good-skill\ndescription: Does a thing. Use when testing the checker."),
99
+ );
100
+ await writeSkill(
101
+ root,
102
+ "skills/broken",
103
+ skillDocument("name: broken-skill\ndescription: Use when the plan: breaks"),
104
+ );
105
+
106
+ const result = await checkSkills({ root });
107
+
108
+ assert.equal(result.checked, 2);
109
+ assert.equal(result.failures.length, 1);
110
+ assert.equal(result.failures[0].file, join("skills", "broken", "SKILL.md"));
111
+ } finally {
112
+ await rm(root, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ it("ignores markdown files that are not SKILL.md", async () => {
117
+ const root = await makeFixtureRoot();
118
+ try {
119
+ await writeSkill(root, "skills/good", skillDocument("name: good-skill\ndescription: Does a thing."));
120
+ await writeFile(join(root, "skills", "good", "REFERENCE.md"), "---\nnot: a skill\n---\n");
121
+
122
+ const result = await checkSkills({ root });
123
+
124
+ assert.equal(result.checked, 1);
125
+ assert.deepEqual(result.failures, []);
126
+ } finally {
127
+ await rm(root, { recursive: true, force: true });
128
+ }
129
+ });
130
+
131
+ it("passes on this repository's own skills tree", async () => {
132
+ const result = await checkSkills({ root: REPO_ROOT });
133
+
134
+ assert.deepEqual(result.failures, []);
135
+ assert.ok(result.checked > 0, "expected the check to find SKILL.md files");
136
+ });
137
+ });