@bongos/core 1.19.817 → 1.19.819

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
+ // tests/routine_capture_kind.mjs — a routine that files its findings must name a
2
+ // kind the server accepts (task 1003694).
3
+ //
4
+ // THE FAILURE THIS PINS. .claude/scheduled-tasks/dep-audit-nightly/SKILL.md filed
5
+ // with `--kind security`. That is not one of the kinds — captureIdea throws BAD_KIND
6
+ // and the route 400s — so the capture step of that nightly routine had NEVER filed
7
+ // anything.
8
+ //
9
+ // It is quiet in the worst way. The routine still runs, still does the audit, and
10
+ // only the step that turns a finding into a durable row fails. There is no
11
+ // accumulating queue whose emptiness would look wrong, so nothing ever looked wrong.
12
+ // A scheduled routine cannot notice its own silence, which is exactly why this is a
13
+ // test and not a runtime check.
14
+ //
15
+ // Run: node tests/routine_capture_kind.mjs
16
+
17
+ import { test } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { createRequire } from 'node:module';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const { VALID_KINDS } = require('../modules/ideas/inbox.js');
26
+
27
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+
29
+ // Every SKILL.md that could carry a capture command: the scheduled routines and the
30
+ // skills. A routine is the dangerous case (nobody reads its output), but a skill with
31
+ // a bad kind wastes a builder's time the same way.
32
+ function skillFiles() {
33
+ const out = [];
34
+ const roots = [
35
+ path.join(REPO_ROOT, '.claude', 'scheduled-tasks'),
36
+ path.join(REPO_ROOT, '.claude', 'skills'),
37
+ ];
38
+ for (const root of roots) {
39
+ if (!existsSync(root)) continue;
40
+ for (const d of readdirSync(root, { withFileTypes: true })) {
41
+ if (!d.isDirectory()) continue;
42
+ const p = path.join(root, d.name, 'SKILL.md');
43
+ if (existsSync(p)) out.push(p);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ // `--kind foo` — a LITERAL kind, on a CAPTURE command only.
50
+ //
51
+ // Both narrowings are load-bearing, and I got each wrong first:
52
+ //
53
+ // - CAPTURE ONLY. `--kind` is not unique to capture.js. recall.js takes its own
54
+ // `--kind doc|db`, and a matcher that reads every `--kind` in the tree reports
55
+ // three recall invocations as broken. A guard whose first run is mostly noise
56
+ // is a guard people learn to suppress.
57
+ // - LITERAL ONLY. A placeholder (`--kind <bug|cleanup>`, `--kind cleanup|refactor`)
58
+ // documents a CHOICE rather than passing a value; overnight-code-review and
59
+ // repo-simplify-sweep both use that form deliberately.
60
+ const CAPTURE_LINE_RE = /capture\.js/;
61
+ const KIND_RE = /--kind\s+([a-z][a-z0-9-]*)(?=[\s`'"]|$)/g;
62
+
63
+ // A capture command can wrap across lines with a trailing backslash, so the unit of
64
+ // inspection is the command, not the line: a run of lines joined while the previous
65
+ // one ends in `\`.
66
+ function captureCommands(text) {
67
+ const lines = String(text || '').split('\n');
68
+ const out = [];
69
+ let buf = null;
70
+ for (const line of lines) {
71
+ const continues = /\\\s*$/.test(line);
72
+ if (buf !== null) {
73
+ buf += ' ' + line;
74
+ if (!continues) { out.push(buf); buf = null; }
75
+ } else if (CAPTURE_LINE_RE.test(line)) {
76
+ if (continues) buf = line;
77
+ else out.push(line);
78
+ }
79
+ }
80
+ if (buf !== null) out.push(buf);
81
+ return out;
82
+ }
83
+
84
+ function literalKinds(text) {
85
+ const out = [];
86
+ for (const cmd of captureCommands(text)) {
87
+ for (const m of cmd.matchAll(KIND_RE)) {
88
+ const after = cmd.slice(m.index + m[0].length, m.index + m[0].length + 1);
89
+ if (after === '|') continue; // `--kind cleanup|refactor` — a menu, not a value
90
+ out.push(m[1]);
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+
96
+ test('every literal --kind in a routine or skill is a kind the server accepts', () => {
97
+ const files = skillFiles();
98
+ // The scan must not pass by finding nothing: these files exist, and at least one of
99
+ // them documents a capture command.
100
+ assert.ok(files.length > 0, 'found no SKILL.md files at all — the scan is broken, not clean');
101
+
102
+ const bad = [];
103
+ let seen = 0;
104
+ for (const f of files) {
105
+ for (const kind of literalKinds(readFileSync(f, 'utf8'))) {
106
+ seen += 1;
107
+ if (!VALID_KINDS.includes(kind)) bad.push(`${path.relative(REPO_ROOT, f)}: --kind ${kind}`);
108
+ }
109
+ }
110
+ assert.ok(seen > 0, 'no literal --kind found anywhere — the pattern has drifted, and this test would pass an empty repo');
111
+ assert.deepEqual(bad, [],
112
+ `these file a kind the server rejects with a 400 — for a SCHEDULED routine that means its findings are silently never filed:\n ${bad.join('\n ')}\n`
113
+ + `Valid kinds: ${VALID_KINDS.join(', ')}`);
114
+ });
115
+
116
+ test('the matcher reads a literal kind on a capture command', () => {
117
+ assert.deepEqual(literalKinds('node scripts/gds/capture.js "t" --kind infra --body-file x'), ['infra']);
118
+ assert.deepEqual(literalKinds('node scripts/gds/capture.js "t" --kind security'), ['security'],
119
+ 'the real bug is still caught');
120
+ });
121
+
122
+ test('a DIFFERENT tool\'s --kind is not this guard\'s business', () => {
123
+ // recall.js takes --kind doc|db. Reading every --kind in the tree reported three
124
+ // recall invocations as broken on the first run of this guard.
125
+ assert.deepEqual(literalKinds('node scripts/gds/recall.js nightly backup --kind doc'), []);
126
+ assert.deepEqual(literalKinds('add `--kind doc` for ADRs/recipes'), []);
127
+ });
128
+
129
+ test('a documented menu is not a value', () => {
130
+ assert.deepEqual(literalKinds('node scripts/gds/capture.js --kind <bug|cleanup|refactor>'), []);
131
+ assert.deepEqual(literalKinds('node scripts/gds/capture.js --kind cleanup|refactor \\'), []);
132
+ });
133
+
134
+ test('a capture command wrapped over lines is still read', () => {
135
+ const text = ['node scripts/gds/capture.js "t" \\', ' --kind security \\', ' --body-file /tmp/x'].join('\n');
136
+ assert.deepEqual(literalKinds(text), ['security'],
137
+ 'dep-audit-nightly writes exactly this shape — a line-at-a-time matcher misses it');
138
+ });
139
+
140
+ test('dep-audit-nightly specifically now files with an accepted kind (task 1003694)', () => {
141
+ const p = path.join(REPO_ROOT, '.claude', 'scheduled-tasks', 'dep-audit-nightly', 'SKILL.md');
142
+ const src = readFileSync(p, 'utf8');
143
+ assert.ok(!/--kind\s+security\b/.test(src), 'security is not a kind and never was');
144
+ const kinds = literalKinds(src);
145
+ assert.ok(kinds.length > 0, 'the routine must still file something');
146
+ for (const k of kinds) assert.ok(VALID_KINDS.includes(k), `${k} is not a valid kind`);
147
+ });
@@ -140,6 +140,51 @@ test('the live tree passes the declare-check', () => {
140
140
  assert.equal(r.hardFail, false, `unexpected:\n ${r.violations.join('\n ')}`);
141
141
  });
142
142
 
143
+ // ---- CRLF (task 1003754) --------------------------------------------------
144
+ //
145
+ // Every regex in skill-preflight is anchored on \n, starting with the frontmatter
146
+ // fence /^---\n/. SKILL.md files are CRLF in a Windows checkout — .gitattributes
147
+ // pins *.js, *.json, *.sh and *.sql, but Markdown falls through `* text=auto` — so
148
+ // the frontmatter never matched and parseRequires reported `declared: false` for
149
+ // EVERY skill.
150
+ //
151
+ // The visible symptom was one red test below. The real damage was quieter: fitness
152
+ // check G03 compares a skill's fenced commands against its declaration, and with
153
+ // nothing parsed as declared it had nothing to compare — '0 declare a machine
154
+ // contract'. The guard against a skill's ops commands outrunning its declaration was
155
+ // INERT on Windows, which is a false green, not a false red.
156
+
157
+ test('parseRequires reads a CRLF frontmatter identically to an LF one (task 1003754)', () => {
158
+ const lf = '---\nname: x\nrequires: [gh, main-checkout]\n---\nbody\n';
159
+ const crlf = lf.replace(/\n/g, '\r\n');
160
+ assert.deepEqual(sp.parseRequires(crlf), sp.parseRequires(lf),
161
+ 'a checkout\'s line endings must not change what a skill is understood to require');
162
+ assert.deepEqual(sp.parseRequires(crlf).requires, ['gh', 'main-checkout']);
163
+ assert.equal(sp.parseRequires(crlf).declared, true);
164
+ });
165
+
166
+ test('the dash-list form survives CRLF too', () => {
167
+ const crlf = '---\r\nname: x\r\nrequires:\r\n - gh\r\n - droplet-ssh\r\n---\r\nbody\r\n';
168
+ assert.deepEqual(sp.parseRequires(crlf).requires, ['gh', 'droplet-ssh']);
169
+ });
170
+
171
+ test('inferRequired reads CRLF fenced commands (task 1003754)', () => {
172
+ // The half that would silently stop CATCHING an undeclared ops command.
173
+ const crlf = '---\r\nname: x\r\n---\r\n```bash\r\ngit push origin main\r\n```\r\n';
174
+ assert.ok(sp.inferRequired(crlf).includes('push-credential'),
175
+ 'a main-push in a CRLF skill must still imply a push credential, or G03 cannot catch it');
176
+ });
177
+
178
+ test('the live corpus is NOT empty — G03 has something to compare (task 1003754)', () => {
179
+ // The check\'s own note read "0 declare a machine contract" on Windows. A guard
180
+ // with an empty corpus passes whatever the skills do.
181
+ const r = sp.checkOpsSkillsDeclareMachine();
182
+ const n = Number(/(\d+) declare a machine contract/.exec(r.note || '')?.[1] ?? 0);
183
+ assert.ok(n > 0,
184
+ `no skill parsed as declaring a machine contract, so check G03 has nothing to compare and `
185
+ + `cannot fail. Note was: ${r.note}`);
186
+ });
187
+
143
188
  test('merge-mode really declares the machine it needs', () => {
144
189
  // The skill at the centre of the incident.
145
190
  const r = sp.preflightSkill('merge-mode', caps);