@onlooker-community/ecosystem 0.10.0 → 0.15.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.
- package/.claude-plugin/marketplace.json +39 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.github/copilot-instructions.md +46 -0
- package/.github/workflows/coverage.yml +78 -0
- package/.github/workflows/release.yml +24 -8
- package/.github/workflows/test.yml +3 -0
- package/.markdownlintignore +3 -0
- package/.release-please-manifest.json +5 -1
- package/CHANGELOG.md +44 -0
- package/README.md +58 -13
- package/config.json +6 -1
- package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
- package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
- package/docs/adr/003-ulid-over-uuid.md +40 -0
- package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
- package/docs/architecture.md +123 -0
- package/hooks/hooks.json +4 -0
- package/package.json +13 -7
- package/plugins/archivist/.claude-plugin/plugin.json +14 -0
- package/plugins/archivist/CHANGELOG.md +8 -0
- package/plugins/archivist/README.md +105 -0
- package/plugins/archivist/config.json +18 -0
- package/plugins/archivist/hooks/hooks.json +35 -0
- package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
- package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
- package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
- package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
- package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
- package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
- package/plugins/cartographer/.claude-plugin/plugin.json +14 -0
- package/plugins/cartographer/CHANGELOG.md +27 -0
- package/plugins/cartographer/README.md +113 -0
- package/plugins/cartographer/config.json +21 -0
- package/plugins/cartographer/docs/adr/001-background-audit-launch.md +28 -0
- package/plugins/cartographer/docs/adr/002-flock-pid-file-fallback.md +30 -0
- package/plugins/cartographer/docs/adr/003-at-least-once-event-delivery.md +32 -0
- package/plugins/cartographer/docs/adr/004-exclude-paths-replace-semantics.md +27 -0
- package/plugins/cartographer/hooks/hooks.json +44 -0
- package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +87 -0
- package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +89 -0
- package/plugins/cartographer/scripts/lib/cartographer-analyze.sh +286 -0
- package/plugins/cartographer/scripts/lib/cartographer-collect.sh +59 -0
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +105 -0
- package/plugins/cartographer/scripts/lib/cartographer-events.sh +82 -0
- package/plugins/cartographer/scripts/lib/cartographer-lock.sh +38 -0
- package/plugins/cartographer/scripts/lib/cartographer-project-key.sh +55 -0
- package/plugins/cartographer/scripts/lib/cartographer-ulid.sh +47 -0
- package/plugins/cartographer/scripts/run-audit.sh +309 -0
- package/plugins/cartographer/skills/cartographer/SKILL.md +154 -0
- package/plugins/echo/.claude-plugin/plugin.json +14 -0
- package/plugins/echo/CHANGELOG.md +24 -0
- package/plugins/echo/README.md +110 -0
- package/plugins/echo/config.json +15 -0
- package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
- package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
- package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
- package/plugins/echo/hooks/hooks.json +15 -0
- package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
- package/plugins/echo/scripts/lib/echo-config.sh +108 -0
- package/plugins/echo/scripts/lib/echo-events.sh +74 -0
- package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
- package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
- package/plugins/tribunal/CHANGELOG.md +10 -0
- package/plugins/tribunal/README.md +134 -0
- package/plugins/tribunal/agents/tribunal-actor.md +35 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
- package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
- package/plugins/tribunal/config.json +50 -0
- package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
- package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
- package/plugins/tribunal/hooks/hooks.json +15 -0
- package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
- package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
- package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
- package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
- package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
- package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
- package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
- package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
- package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
- package/release-please-config.json +59 -5
- package/scripts/coverage/bash-coverage.mjs +169 -0
- package/scripts/coverage/format-comment.mjs +120 -0
- package/scripts/coverage/run-coverage.mjs +151 -0
- package/scripts/hooks/agent-spawn-tracker.sh +4 -4
- package/scripts/hooks/prompt-rule-injector.sh +122 -0
- package/scripts/lib/portable-lock.sh +48 -0
- package/scripts/lib/prompt-rules.sh +207 -0
- package/scripts/lib/tool-history.sh +7 -8
- package/scripts/lib/validate-path.sh +4 -0
- package/scripts/lint/check-manifests.mjs +314 -0
- package/scripts/lint/check-references.mjs +311 -0
- package/skills/list-prompt-rules/SKILL.md +15 -0
- package/test/bats/archivist-config-files.bats +60 -0
- package/test/bats/archivist-config.bats +54 -0
- package/test/bats/archivist-inject.bats +73 -0
- package/test/bats/archivist-project-key.bats +75 -0
- package/test/bats/archivist-storage.bats +119 -0
- package/test/bats/archivist-ulid.bats +36 -0
- package/test/bats/cartographer-config.bats +107 -0
- package/test/bats/cartographer-lock.bats +77 -0
- package/test/bats/cartographer-ulid.bats +56 -0
- package/test/bats/config.bats +10 -10
- package/test/bats/echo-config.bats +90 -0
- package/test/bats/echo-events.bats +121 -0
- package/test/bats/echo-project-key.bats +115 -0
- package/test/bats/echo-stop-hook.bats +101 -0
- package/test/bats/echo-ulid.bats +38 -0
- package/test/bats/portable-lock.bats +62 -0
- package/test/bats/prompt-rules.bats +269 -0
- package/test/bats/tribunal-aggregate.bats +77 -0
- package/test/bats/tribunal-config.bats +86 -0
- package/test/bats/tribunal-events.bats +209 -0
- package/test/bats/tribunal-gate.bats +95 -0
- package/test/bats/tribunal-jury.bats +80 -0
- package/test/bats/tribunal-rubric.bats +119 -0
- package/test/bats/tribunal-stop-hook.bats +73 -0
- package/test/bats/tribunal-verdict.bats +71 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
- package/test/helpers/setup.bash +9 -0
- package/test/node/check-manifests.test.mjs +173 -0
- package/test/node/check-references.test.mjs +279 -0
- package/test/node/coverage.test.mjs +143 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Tests for scripts/lint/check-references.mjs. Each test stands up a
|
|
2
|
+
// scratch marketplace under BATS_TEST_TMPDIR-style isolation, runs the
|
|
3
|
+
// linter as a subprocess, and asserts on exit code + emitted output.
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { describe, it } from 'node:test';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const REPO_ROOT = resolve(HERE, '..', '..');
|
|
15
|
+
const LINTER = join(REPO_ROOT, 'scripts', 'lint', 'check-references.mjs');
|
|
16
|
+
|
|
17
|
+
function scaffold() {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), 'check-refs-'));
|
|
19
|
+
mkdirSync(join(root, '.claude-plugin'), { recursive: true });
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeJson(p, data) {
|
|
24
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
25
|
+
writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeFile(p, text) {
|
|
29
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
30
|
+
writeFileSync(p, text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function run(root, ...args) {
|
|
34
|
+
const r = spawnSync('node', [LINTER, '--root', root, ...args], { encoding: 'utf8' });
|
|
35
|
+
return { code: r.status, stdout: r.stdout, stderr: r.stderr };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeSkill(root, pluginDir, fileRelPath, frontmatter, body = '') {
|
|
39
|
+
const fmLines = ['---', ...Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`), '---', body];
|
|
40
|
+
writeFile(join(root, pluginDir, fileRelPath), fmLines.join('\n'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('check-references', () => {
|
|
44
|
+
it('passes on an empty marketplace', () => {
|
|
45
|
+
const root = scaffold();
|
|
46
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
47
|
+
name: 'tm',
|
|
48
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
49
|
+
});
|
|
50
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
51
|
+
name: 'ecosystem',
|
|
52
|
+
version: '0.0.1',
|
|
53
|
+
});
|
|
54
|
+
const r = run(root);
|
|
55
|
+
assert.equal(r.code, 0, r.stderr);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('passes when skills and commands resolve and have frontmatter', () => {
|
|
59
|
+
const root = scaffold();
|
|
60
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
61
|
+
name: 'tm',
|
|
62
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
63
|
+
});
|
|
64
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
65
|
+
name: 'ecosystem',
|
|
66
|
+
version: '0.0.1',
|
|
67
|
+
skills: ['./skills/think.md'],
|
|
68
|
+
commands: ['./commands/commit.md'],
|
|
69
|
+
});
|
|
70
|
+
writeSkill(root, '.', 'skills/think.md', { name: 'think', description: 'muse' });
|
|
71
|
+
writeSkill(root, '.', 'commands/commit.md', { name: 'commit', description: 'git commit' });
|
|
72
|
+
const r = run(root);
|
|
73
|
+
assert.equal(r.code, 0, r.stderr);
|
|
74
|
+
assert.match(r.stdout, /ok \(2 records/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('fails when a referenced path does not exist', () => {
|
|
78
|
+
const root = scaffold();
|
|
79
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
80
|
+
name: 'tm',
|
|
81
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
82
|
+
});
|
|
83
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
84
|
+
name: 'ecosystem',
|
|
85
|
+
version: '0.0.1',
|
|
86
|
+
skills: ['./skills/missing.md'],
|
|
87
|
+
});
|
|
88
|
+
const r = run(root);
|
|
89
|
+
assert.equal(r.code, 1);
|
|
90
|
+
assert.match(r.stderr, /points to a missing file/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('fails when a markdown file has no frontmatter', () => {
|
|
94
|
+
const root = scaffold();
|
|
95
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
96
|
+
name: 'tm',
|
|
97
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
98
|
+
});
|
|
99
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
100
|
+
name: 'ecosystem',
|
|
101
|
+
version: '0.0.1',
|
|
102
|
+
skills: ['./skills/bare.md'],
|
|
103
|
+
});
|
|
104
|
+
writeFile(join(root, 'skills/bare.md'), 'no frontmatter here\n');
|
|
105
|
+
const r = run(root);
|
|
106
|
+
assert.equal(r.code, 1);
|
|
107
|
+
assert.match(r.stderr, /missing YAML frontmatter/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('fails when required frontmatter fields are missing', () => {
|
|
111
|
+
const root = scaffold();
|
|
112
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
113
|
+
name: 'tm',
|
|
114
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
115
|
+
});
|
|
116
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
117
|
+
name: 'ecosystem',
|
|
118
|
+
version: '0.0.1',
|
|
119
|
+
skills: ['./skills/half.md'],
|
|
120
|
+
});
|
|
121
|
+
writeSkill(root, '.', 'skills/half.md', { name: 'half' });
|
|
122
|
+
const r = run(root);
|
|
123
|
+
assert.equal(r.code, 1);
|
|
124
|
+
assert.match(r.stderr, /missing required frontmatter field "description"/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('warns on body references to unknown slash commands', () => {
|
|
128
|
+
const root = scaffold();
|
|
129
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
130
|
+
name: 'tm',
|
|
131
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
132
|
+
});
|
|
133
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
134
|
+
name: 'ecosystem',
|
|
135
|
+
version: '0.0.1',
|
|
136
|
+
skills: ['./skills/refs.md'],
|
|
137
|
+
});
|
|
138
|
+
writeSkill(
|
|
139
|
+
root,
|
|
140
|
+
'.',
|
|
141
|
+
'skills/refs.md',
|
|
142
|
+
{ name: 'refs', description: 'x' },
|
|
143
|
+
'Use the /nonexistent-command to bootstrap.',
|
|
144
|
+
);
|
|
145
|
+
const r = run(root);
|
|
146
|
+
// Warnings alone do not fail by default.
|
|
147
|
+
assert.equal(r.code, 0);
|
|
148
|
+
assert.match(r.stderr, /unknown command "\/nonexistent-command"/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('does not warn on built-in slash commands', () => {
|
|
152
|
+
const root = scaffold();
|
|
153
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
154
|
+
name: 'tm',
|
|
155
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
156
|
+
});
|
|
157
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
158
|
+
name: 'ecosystem',
|
|
159
|
+
version: '0.0.1',
|
|
160
|
+
skills: ['./skills/refs.md'],
|
|
161
|
+
});
|
|
162
|
+
writeSkill(root, '.', 'skills/refs.md', { name: 'refs', description: 'x' }, 'Run /help and /clear to reset.');
|
|
163
|
+
const r = run(root);
|
|
164
|
+
assert.equal(r.code, 0);
|
|
165
|
+
assert.doesNotMatch(r.stderr, /unknown command/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('does not warn on commands declared elsewhere in the same marketplace', () => {
|
|
169
|
+
const root = scaffold();
|
|
170
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
171
|
+
name: 'tm',
|
|
172
|
+
plugins: [
|
|
173
|
+
{ name: 'ecosystem', source: './' },
|
|
174
|
+
{ name: 'archivist', source: './plugins/archivist' },
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
178
|
+
name: 'ecosystem',
|
|
179
|
+
version: '0.0.1',
|
|
180
|
+
commands: ['./commands/pin.md'],
|
|
181
|
+
skills: ['./skills/uses-pin.md'],
|
|
182
|
+
});
|
|
183
|
+
writeSkill(root, '.', 'commands/pin.md', { name: 'pin', description: 'pin a memory' });
|
|
184
|
+
writeSkill(
|
|
185
|
+
root,
|
|
186
|
+
'.',
|
|
187
|
+
'skills/uses-pin.md',
|
|
188
|
+
{ name: 'uses-pin', description: 'x' },
|
|
189
|
+
'Call /pin to mark an item as important.',
|
|
190
|
+
);
|
|
191
|
+
writeJson(join(root, 'plugins/archivist/.claude-plugin/plugin.json'), {
|
|
192
|
+
name: 'archivist',
|
|
193
|
+
version: '0.0.1',
|
|
194
|
+
});
|
|
195
|
+
const r = run(root);
|
|
196
|
+
assert.equal(r.code, 0, r.stderr);
|
|
197
|
+
assert.doesNotMatch(r.stderr, /unknown command/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('--strict turns warnings into errors', () => {
|
|
201
|
+
const root = scaffold();
|
|
202
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
203
|
+
name: 'tm',
|
|
204
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
205
|
+
});
|
|
206
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
207
|
+
name: 'ecosystem',
|
|
208
|
+
version: '0.0.1',
|
|
209
|
+
skills: ['./skills/refs.md'],
|
|
210
|
+
});
|
|
211
|
+
writeSkill(root, '.', 'skills/refs.md', { name: 'refs', description: 'x' }, 'Run /nothing-here.');
|
|
212
|
+
const r = run(root, '--strict');
|
|
213
|
+
assert.equal(r.code, 1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('--plugin filters to a single plugin', () => {
|
|
217
|
+
const root = scaffold();
|
|
218
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
219
|
+
name: 'tm',
|
|
220
|
+
plugins: [
|
|
221
|
+
{ name: 'ecosystem', source: './' },
|
|
222
|
+
{ name: 'archivist', source: './plugins/archivist' },
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
226
|
+
name: 'ecosystem',
|
|
227
|
+
version: '0.0.1',
|
|
228
|
+
skills: ['./skills/missing.md'],
|
|
229
|
+
});
|
|
230
|
+
writeJson(join(root, 'plugins/archivist/.claude-plugin/plugin.json'), {
|
|
231
|
+
name: 'archivist',
|
|
232
|
+
version: '0.0.1',
|
|
233
|
+
});
|
|
234
|
+
const r = run(root, '--plugin', 'archivist');
|
|
235
|
+
// ecosystem has a broken path, but we filtered it out, so this passes.
|
|
236
|
+
assert.equal(r.code, 0, r.stderr);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('treats a directory entry as a tree of markdown files', () => {
|
|
240
|
+
const root = scaffold();
|
|
241
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
242
|
+
name: 'tm',
|
|
243
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
244
|
+
});
|
|
245
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
246
|
+
name: 'ecosystem',
|
|
247
|
+
version: '0.0.1',
|
|
248
|
+
skills: ['./skills'],
|
|
249
|
+
});
|
|
250
|
+
writeSkill(root, '.', 'skills/a.md', { name: 'a', description: 'x' });
|
|
251
|
+
writeSkill(root, '.', 'skills/nested/b.md', { name: 'b', description: 'y' });
|
|
252
|
+
const r = run(root);
|
|
253
|
+
assert.equal(r.code, 0, r.stderr);
|
|
254
|
+
assert.match(r.stdout, /ok \(2 records/);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('ignores slash-command-like strings inside backtick spans', () => {
|
|
258
|
+
const root = scaffold();
|
|
259
|
+
writeJson(join(root, '.claude-plugin', 'marketplace.json'), {
|
|
260
|
+
name: 'tm',
|
|
261
|
+
plugins: [{ name: 'ecosystem', source: './' }],
|
|
262
|
+
});
|
|
263
|
+
writeJson(join(root, '.claude-plugin', 'plugin.json'), {
|
|
264
|
+
name: 'ecosystem',
|
|
265
|
+
version: '0.0.1',
|
|
266
|
+
skills: ['./skills/code.md'],
|
|
267
|
+
});
|
|
268
|
+
writeSkill(
|
|
269
|
+
root,
|
|
270
|
+
'.',
|
|
271
|
+
'skills/code.md',
|
|
272
|
+
{ name: 'code', description: 'x' },
|
|
273
|
+
'Inline `/should-not-warn` should be ignored.',
|
|
274
|
+
);
|
|
275
|
+
const r = run(root);
|
|
276
|
+
assert.equal(r.code, 0);
|
|
277
|
+
assert.doesNotMatch(r.stderr, /unknown command/);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const REPO_ROOT = resolve(HERE, '..', '..');
|
|
11
|
+
|
|
12
|
+
function runJson(script, root, ...args) {
|
|
13
|
+
const r = spawnSync('node', [join(REPO_ROOT, script), '--root', root, '--json', ...args], {
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
});
|
|
16
|
+
if (r.status !== 0) {
|
|
17
|
+
throw new Error(`${script} exited ${r.status}\nstdout:\n${r.stdout}\nstderr:\n${r.stderr}`);
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(r.stdout);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scaffoldMarketplace() {
|
|
23
|
+
const root = mkdtempSync(join(tmpdir(), 'coverage-'));
|
|
24
|
+
mkdirSync(join(root, '.claude-plugin'), { recursive: true });
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(root, '.claude-plugin', 'marketplace.json'),
|
|
27
|
+
JSON.stringify({ name: 't', plugins: [{ name: 's', source: './' }] }),
|
|
28
|
+
);
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeFile(path, content) {
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
34
|
+
writeFileSync(path, content);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('bash-coverage', () => {
|
|
38
|
+
it('returns 100% when every public function is referenced in tests', () => {
|
|
39
|
+
const root = scaffoldMarketplace();
|
|
40
|
+
writeFile(join(root, 'scripts/lib/foo.sh'), `#!/usr/bin/env bash\nfoo() { echo 1; }\nbar() { echo 2; }\n`);
|
|
41
|
+
writeFile(join(root, 'test/bats/foo.bats'), `@test "covers" { foo; bar; }\n`);
|
|
42
|
+
const report = runJson('scripts/coverage/bash-coverage.mjs', root);
|
|
43
|
+
assert.equal(report.overall.total, 2);
|
|
44
|
+
assert.equal(report.overall.tested, 2);
|
|
45
|
+
assert.equal(report.overall.ratio, 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('drops untested public functions into the untested list', () => {
|
|
49
|
+
const root = scaffoldMarketplace();
|
|
50
|
+
writeFile(join(root, 'scripts/lib/foo.sh'), `#!/usr/bin/env bash\nfoo() { :; }\nbar() { :; }\nbaz() { :; }\n`);
|
|
51
|
+
writeFile(join(root, 'test/bats/foo.bats'), `@test "covers" { foo; }\n`);
|
|
52
|
+
const report = runJson('scripts/coverage/bash-coverage.mjs', root);
|
|
53
|
+
assert.equal(report.overall.tested, 1);
|
|
54
|
+
assert.equal(report.overall.total, 3);
|
|
55
|
+
const untestedNames = report.untested.map((u) => u.name).sort();
|
|
56
|
+
assert.deepEqual(untestedNames, ['bar', 'baz']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('skips private (underscore-prefixed) helpers', () => {
|
|
60
|
+
const root = scaffoldMarketplace();
|
|
61
|
+
writeFile(join(root, 'scripts/lib/foo.sh'), `#!/usr/bin/env bash\nfoo() { :; }\n_internal() { :; }\n`);
|
|
62
|
+
writeFile(join(root, 'test/bats/foo.bats'), `@test "covers foo" { foo; }\n`);
|
|
63
|
+
const report = runJson('scripts/coverage/bash-coverage.mjs', root);
|
|
64
|
+
assert.equal(report.overall.total, 1, JSON.stringify(report));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not count indented function-looking lines as definitions', () => {
|
|
68
|
+
const root = scaffoldMarketplace();
|
|
69
|
+
writeFile(
|
|
70
|
+
join(root, 'scripts/lib/foo.sh'),
|
|
71
|
+
`#!/usr/bin/env bash\nfoo() {\n inner() { :; } # nested, should NOT count\n}\n`,
|
|
72
|
+
);
|
|
73
|
+
writeFile(join(root, 'test/bats/foo.bats'), `@test "covers" { foo; }\n`);
|
|
74
|
+
const report = runJson('scripts/coverage/bash-coverage.mjs', root);
|
|
75
|
+
assert.equal(report.overall.total, 1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('handles repos with no .sh files at all', () => {
|
|
79
|
+
const root = scaffoldMarketplace();
|
|
80
|
+
const report = runJson('scripts/coverage/bash-coverage.mjs', root);
|
|
81
|
+
assert.equal(report.overall.total, 0);
|
|
82
|
+
assert.equal(report.overall.ratio, 1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('format-comment', () => {
|
|
87
|
+
function render(nodeReport, bashReport, sha = 'abcdef0123456789') {
|
|
88
|
+
const tmp = mkdtempSync(join(tmpdir(), 'cov-fmt-'));
|
|
89
|
+
const nPath = join(tmp, 'node.json');
|
|
90
|
+
const bPath = join(tmp, 'bash.json');
|
|
91
|
+
writeFileSync(nPath, JSON.stringify(nodeReport));
|
|
92
|
+
writeFileSync(bPath, JSON.stringify(bashReport));
|
|
93
|
+
const r = spawnSync(
|
|
94
|
+
'node',
|
|
95
|
+
[join(REPO_ROOT, 'scripts', 'coverage', 'format-comment.mjs'), '--node', nPath, '--bash', bPath, '--sha', sha],
|
|
96
|
+
{ encoding: 'utf8' },
|
|
97
|
+
);
|
|
98
|
+
if (r.status !== 0) throw new Error(`format-comment failed: ${r.stderr}`);
|
|
99
|
+
return r.stdout;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
it('emits the sentinel comment so the upsert logic can find it', () => {
|
|
103
|
+
const out = render(
|
|
104
|
+
{ overall: null, files: [] },
|
|
105
|
+
{ overall: { total: 0, tested: 0, ratio: 1 }, files: [], untested: [] },
|
|
106
|
+
);
|
|
107
|
+
assert.ok(out.startsWith('<!-- onlooker-coverage-comment -->'));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('renders node coverage rows in the markdown table', () => {
|
|
111
|
+
const out = render(
|
|
112
|
+
{
|
|
113
|
+
overall: { file: 'all files', line: 75, branch: 55, funcs: 90, uncoveredLines: '' },
|
|
114
|
+
files: [{ file: 'foo.mjs', line: 75, branch: 50, funcs: 80, uncoveredLines: '12-14' }],
|
|
115
|
+
},
|
|
116
|
+
{ overall: { total: 1, tested: 1, ratio: 1 }, files: [], untested: [] },
|
|
117
|
+
);
|
|
118
|
+
assert.match(out, /\| `foo\.mjs` \| 75\.0% \| 50\.0% \| 80\.0% \|/);
|
|
119
|
+
// 75% lines → yellow (60–79 inclusive).
|
|
120
|
+
assert.match(out, /\*\*Overall:\*\* 🟡 75\.0% lines/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('marks bash overall red when below 50%', () => {
|
|
124
|
+
const out = render(
|
|
125
|
+
{ overall: null, files: [] },
|
|
126
|
+
{ overall: { total: 10, tested: 3, ratio: 0.3 }, files: [], untested: [] },
|
|
127
|
+
);
|
|
128
|
+
assert.match(out, /🔴 3\/10 public functions/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('lists untested functions inside a collapsed details block', () => {
|
|
132
|
+
const out = render(
|
|
133
|
+
{ overall: null, files: [] },
|
|
134
|
+
{
|
|
135
|
+
overall: { total: 2, tested: 1, ratio: 0.5 },
|
|
136
|
+
files: [{ file: 'a.sh', total: 2, tested: 1, ratio: 0.5, untested: ['bar'] }],
|
|
137
|
+
untested: [{ file: 'a.sh', name: 'bar' }],
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
assert.match(out, /<details><summary>Untested public functions<\/summary>/);
|
|
141
|
+
assert.match(out, /`a\.sh` — `bar`/);
|
|
142
|
+
});
|
|
143
|
+
});
|