@runchr/gstack-antigravity 0.1.0 → 0.1.2
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.
Potentially problematic release.
This version of @runchr/gstack-antigravity might be problematic. Click here for more details.
- package/.agents/skills/gstack/.agents/skills/gstack/SKILL.md +651 -0
- package/.agents/skills/gstack/.agents/skills/gstack-autoplan/SKILL.md +678 -0
- package/.agents/skills/gstack/.agents/skills/gstack-benchmark/SKILL.md +482 -0
- package/.agents/skills/gstack/.agents/skills/gstack-browse/SKILL.md +511 -0
- package/.agents/skills/gstack/.agents/skills/gstack-canary/SKILL.md +486 -0
- package/.agents/skills/gstack/.agents/skills/gstack-careful/SKILL.md +50 -0
- package/.agents/skills/gstack/.agents/skills/gstack-cso/SKILL.md +607 -0
- package/.agents/skills/gstack/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
- package/.agents/skills/gstack/.agents/skills/gstack-design-review/SKILL.md +988 -0
- package/.agents/skills/gstack/.agents/skills/gstack-document-release/SKILL.md +604 -0
- package/.agents/skills/gstack/.agents/skills/gstack-freeze/SKILL.md +67 -0
- package/.agents/skills/gstack/.agents/skills/gstack-guard/SKILL.md +62 -0
- package/.agents/skills/gstack/.agents/skills/gstack-investigate/SKILL.md +415 -0
- package/.agents/skills/gstack/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
- package/.agents/skills/gstack/.agents/skills/gstack-office-hours/SKILL.md +986 -0
- package/.agents/skills/gstack/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
- package/.agents/skills/gstack/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
- package/.agents/skills/gstack/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
- package/.agents/skills/gstack/.agents/skills/gstack-qa/SKILL.md +1006 -0
- package/.agents/skills/gstack/.agents/skills/gstack-qa-only/SKILL.md +626 -0
- package/.agents/skills/gstack/.agents/skills/gstack-retro/SKILL.md +1065 -0
- package/.agents/skills/gstack/.agents/skills/gstack-review/SKILL.md +704 -0
- package/.agents/skills/gstack/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
- package/.agents/skills/gstack/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
- package/.agents/skills/gstack/.agents/skills/gstack-ship/SKILL.md +1312 -0
- package/.agents/skills/gstack/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
- package/.agents/skills/gstack/.agents/skills/gstack-upgrade/SKILL.md +220 -0
- package/.agents/skills/gstack/.env.example +5 -0
- package/.agents/skills/gstack/.github/workflows/skill-docs.yml +17 -0
- package/.agents/skills/gstack/AGENTS.md +49 -0
- package/.agents/skills/gstack/ARCHITECTURE.md +359 -0
- package/.agents/skills/gstack/BROWSER.md +271 -0
- package/.agents/skills/gstack/CHANGELOG.md +800 -0
- package/.agents/skills/gstack/CLAUDE.md +284 -0
- package/.agents/skills/gstack/CONTRIBUTING.md +370 -0
- package/.agents/skills/gstack/ETHOS.md +129 -0
- package/.agents/skills/gstack/LICENSE +21 -0
- package/.agents/skills/gstack/README.md +228 -0
- package/.agents/skills/gstack/SKILL.md +657 -0
- package/.agents/skills/gstack/SKILL.md.tmpl +281 -0
- package/.agents/skills/gstack/TODOS.md +564 -0
- package/.agents/skills/gstack/VERSION +1 -0
- package/.agents/skills/gstack/autoplan/SKILL.md +689 -0
- package/.agents/skills/gstack/autoplan/SKILL.md.tmpl +416 -0
- package/.agents/skills/gstack/benchmark/SKILL.md +489 -0
- package/.agents/skills/gstack/benchmark/SKILL.md.tmpl +233 -0
- package/.agents/skills/gstack/bin/dev-setup +68 -0
- package/.agents/skills/gstack/bin/dev-teardown +56 -0
- package/.agents/skills/gstack/bin/gstack-analytics +191 -0
- package/.agents/skills/gstack/bin/gstack-community-dashboard +113 -0
- package/.agents/skills/gstack/bin/gstack-config +38 -0
- package/.agents/skills/gstack/bin/gstack-diff-scope +71 -0
- package/.agents/skills/gstack/bin/gstack-global-discover.ts +591 -0
- package/.agents/skills/gstack/bin/gstack-repo-mode +93 -0
- package/.agents/skills/gstack/bin/gstack-review-log +9 -0
- package/.agents/skills/gstack/bin/gstack-review-read +12 -0
- package/.agents/skills/gstack/bin/gstack-slug +15 -0
- package/.agents/skills/gstack/bin/gstack-telemetry-log +158 -0
- package/.agents/skills/gstack/bin/gstack-telemetry-sync +127 -0
- package/.agents/skills/gstack/bin/gstack-update-check +196 -0
- package/.agents/skills/gstack/browse/SKILL.md +517 -0
- package/.agents/skills/gstack/browse/SKILL.md.tmpl +141 -0
- package/.agents/skills/gstack/browse/bin/find-browse +21 -0
- package/.agents/skills/gstack/browse/bin/remote-slug +14 -0
- package/.agents/skills/gstack/browse/scripts/build-node-server.sh +48 -0
- package/.agents/skills/gstack/browse/src/browser-manager.ts +634 -0
- package/.agents/skills/gstack/browse/src/buffers.ts +137 -0
- package/.agents/skills/gstack/browse/src/bun-polyfill.cjs +109 -0
- package/.agents/skills/gstack/browse/src/cli.ts +420 -0
- package/.agents/skills/gstack/browse/src/commands.ts +111 -0
- package/.agents/skills/gstack/browse/src/config.ts +150 -0
- package/.agents/skills/gstack/browse/src/cookie-import-browser.ts +417 -0
- package/.agents/skills/gstack/browse/src/cookie-picker-routes.ts +207 -0
- package/.agents/skills/gstack/browse/src/cookie-picker-ui.ts +541 -0
- package/.agents/skills/gstack/browse/src/find-browse.ts +61 -0
- package/.agents/skills/gstack/browse/src/meta-commands.ts +269 -0
- package/.agents/skills/gstack/browse/src/platform.ts +17 -0
- package/.agents/skills/gstack/browse/src/read-commands.ts +335 -0
- package/.agents/skills/gstack/browse/src/server.ts +369 -0
- package/.agents/skills/gstack/browse/src/snapshot.ts +398 -0
- package/.agents/skills/gstack/browse/src/url-validation.ts +91 -0
- package/.agents/skills/gstack/browse/src/write-commands.ts +352 -0
- package/.agents/skills/gstack/browse/test/bun-polyfill.test.ts +72 -0
- package/.agents/skills/gstack/browse/test/commands.test.ts +1836 -0
- package/.agents/skills/gstack/browse/test/config.test.ts +250 -0
- package/.agents/skills/gstack/browse/test/cookie-import-browser.test.ts +397 -0
- package/.agents/skills/gstack/browse/test/cookie-picker-routes.test.ts +205 -0
- package/.agents/skills/gstack/browse/test/find-browse.test.ts +50 -0
- package/.agents/skills/gstack/browse/test/fixtures/basic.html +33 -0
- package/.agents/skills/gstack/browse/test/fixtures/cursor-interactive.html +22 -0
- package/.agents/skills/gstack/browse/test/fixtures/dialog.html +15 -0
- package/.agents/skills/gstack/browse/test/fixtures/empty.html +2 -0
- package/.agents/skills/gstack/browse/test/fixtures/forms.html +55 -0
- package/.agents/skills/gstack/browse/test/fixtures/qa-eval-checkout.html +108 -0
- package/.agents/skills/gstack/browse/test/fixtures/qa-eval-spa.html +98 -0
- package/.agents/skills/gstack/browse/test/fixtures/qa-eval.html +51 -0
- package/.agents/skills/gstack/browse/test/fixtures/responsive.html +49 -0
- package/.agents/skills/gstack/browse/test/fixtures/snapshot.html +55 -0
- package/.agents/skills/gstack/browse/test/fixtures/spa.html +24 -0
- package/.agents/skills/gstack/browse/test/fixtures/states.html +17 -0
- package/.agents/skills/gstack/browse/test/fixtures/upload.html +25 -0
- package/.agents/skills/gstack/browse/test/gstack-config.test.ts +125 -0
- package/.agents/skills/gstack/browse/test/gstack-update-check.test.ts +467 -0
- package/.agents/skills/gstack/browse/test/handoff.test.ts +235 -0
- package/.agents/skills/gstack/browse/test/path-validation.test.ts +63 -0
- package/.agents/skills/gstack/browse/test/platform.test.ts +37 -0
- package/.agents/skills/gstack/browse/test/snapshot.test.ts +467 -0
- package/.agents/skills/gstack/browse/test/test-server.ts +57 -0
- package/.agents/skills/gstack/browse/test/url-validation.test.ts +72 -0
- package/.agents/skills/gstack/canary/SKILL.md +493 -0
- package/.agents/skills/gstack/canary/SKILL.md.tmpl +220 -0
- package/.agents/skills/gstack/careful/SKILL.md +59 -0
- package/.agents/skills/gstack/careful/SKILL.md.tmpl +57 -0
- package/.agents/skills/gstack/careful/bin/check-careful.sh +112 -0
- package/.agents/skills/gstack/codex/SKILL.md +677 -0
- package/.agents/skills/gstack/codex/SKILL.md.tmpl +356 -0
- package/.agents/skills/gstack/conductor.json +6 -0
- package/.agents/skills/gstack/cso/SKILL.md +615 -0
- package/.agents/skills/gstack/cso/SKILL.md.tmpl +376 -0
- package/.agents/skills/gstack/design-consultation/SKILL.md +625 -0
- package/.agents/skills/gstack/design-consultation/SKILL.md.tmpl +369 -0
- package/.agents/skills/gstack/design-review/SKILL.md +998 -0
- package/.agents/skills/gstack/design-review/SKILL.md.tmpl +262 -0
- package/.agents/skills/gstack/docs/images/github-2013.png +0 -0
- package/.agents/skills/gstack/docs/images/github-2026.png +0 -0
- package/.agents/skills/gstack/docs/skills.md +877 -0
- package/.agents/skills/gstack/document-release/SKILL.md +613 -0
- package/.agents/skills/gstack/document-release/SKILL.md.tmpl +357 -0
- package/.agents/skills/gstack/freeze/SKILL.md +82 -0
- package/.agents/skills/gstack/freeze/SKILL.md.tmpl +80 -0
- package/.agents/skills/gstack/freeze/bin/check-freeze.sh +68 -0
- package/.agents/skills/gstack/gstack-upgrade/SKILL.md +226 -0
- package/.agents/skills/gstack/gstack-upgrade/SKILL.md.tmpl +224 -0
- package/.agents/skills/gstack/guard/SKILL.md +82 -0
- package/.agents/skills/gstack/guard/SKILL.md.tmpl +80 -0
- package/.agents/skills/gstack/investigate/SKILL.md +435 -0
- package/.agents/skills/gstack/investigate/SKILL.md.tmpl +196 -0
- package/.agents/skills/gstack/land-and-deploy/SKILL.md +880 -0
- package/.agents/skills/gstack/land-and-deploy/SKILL.md.tmpl +575 -0
- package/.agents/skills/gstack/office-hours/SKILL.md +996 -0
- package/.agents/skills/gstack/office-hours/SKILL.md.tmpl +624 -0
- package/.agents/skills/gstack/package.json +55 -0
- package/.agents/skills/gstack/plan-ceo-review/SKILL.md +1277 -0
- package/.agents/skills/gstack/plan-ceo-review/SKILL.md.tmpl +838 -0
- package/.agents/skills/gstack/plan-design-review/SKILL.md +676 -0
- package/.agents/skills/gstack/plan-design-review/SKILL.md.tmpl +314 -0
- package/.agents/skills/gstack/plan-eng-review/SKILL.md +836 -0
- package/.agents/skills/gstack/plan-eng-review/SKILL.md.tmpl +279 -0
- package/.agents/skills/gstack/qa/SKILL.md +1016 -0
- package/.agents/skills/gstack/qa/SKILL.md.tmpl +316 -0
- package/.agents/skills/gstack/qa/references/issue-taxonomy.md +85 -0
- package/.agents/skills/gstack/qa/templates/qa-report-template.md +126 -0
- package/.agents/skills/gstack/qa-only/SKILL.md +633 -0
- package/.agents/skills/gstack/qa-only/SKILL.md.tmpl +101 -0
- package/.agents/skills/gstack/retro/SKILL.md +1072 -0
- package/.agents/skills/gstack/retro/SKILL.md.tmpl +833 -0
- package/.agents/skills/gstack/review/SKILL.md +849 -0
- package/.agents/skills/gstack/review/SKILL.md.tmpl +259 -0
- package/.agents/skills/gstack/review/TODOS-format.md +62 -0
- package/.agents/skills/gstack/review/checklist.md +190 -0
- package/.agents/skills/gstack/review/design-checklist.md +132 -0
- package/.agents/skills/gstack/review/greptile-triage.md +220 -0
- package/.agents/skills/gstack/scripts/analytics.ts +190 -0
- package/.agents/skills/gstack/scripts/dev-skill.ts +82 -0
- package/.agents/skills/gstack/scripts/eval-compare.ts +96 -0
- package/.agents/skills/gstack/scripts/eval-list.ts +116 -0
- package/.agents/skills/gstack/scripts/eval-select.ts +86 -0
- package/.agents/skills/gstack/scripts/eval-summary.ts +187 -0
- package/.agents/skills/gstack/scripts/eval-watch.ts +172 -0
- package/.agents/skills/gstack/scripts/gen-skill-docs.ts +2414 -0
- package/.agents/skills/gstack/scripts/skill-check.ts +167 -0
- package/.agents/skills/gstack/setup +269 -0
- package/.agents/skills/gstack/setup-browser-cookies/SKILL.md +330 -0
- package/.agents/skills/gstack/setup-browser-cookies/SKILL.md.tmpl +74 -0
- package/.agents/skills/gstack/setup-deploy/SKILL.md +459 -0
- package/.agents/skills/gstack/setup-deploy/SKILL.md.tmpl +220 -0
- package/.agents/skills/gstack/ship/SKILL.md +1457 -0
- package/.agents/skills/gstack/ship/SKILL.md.tmpl +528 -0
- package/.agents/skills/gstack/supabase/config.sh +10 -0
- package/.agents/skills/gstack/supabase/functions/community-pulse/index.ts +59 -0
- package/.agents/skills/gstack/supabase/functions/telemetry-ingest/index.ts +135 -0
- package/.agents/skills/gstack/supabase/functions/update-check/index.ts +37 -0
- package/.agents/skills/gstack/supabase/migrations/001_telemetry.sql +89 -0
- package/.agents/skills/gstack/test/analytics.test.ts +277 -0
- package/.agents/skills/gstack/test/codex-e2e.test.ts +197 -0
- package/.agents/skills/gstack/test/fixtures/coverage-audit-fixture.ts +76 -0
- package/.agents/skills/gstack/test/fixtures/eval-baselines.json +7 -0
- package/.agents/skills/gstack/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
- package/.agents/skills/gstack/test/fixtures/qa-eval-ground-truth.json +43 -0
- package/.agents/skills/gstack/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.css +86 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.html +41 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-enum-diff.rb +30 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-enum.rb +27 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-vuln.rb +14 -0
- package/.agents/skills/gstack/test/gemini-e2e.test.ts +173 -0
- package/.agents/skills/gstack/test/gen-skill-docs.test.ts +1049 -0
- package/.agents/skills/gstack/test/global-discover.test.ts +187 -0
- package/.agents/skills/gstack/test/helpers/codex-session-runner.ts +282 -0
- package/.agents/skills/gstack/test/helpers/e2e-helpers.ts +239 -0
- package/.agents/skills/gstack/test/helpers/eval-store.test.ts +548 -0
- package/.agents/skills/gstack/test/helpers/eval-store.ts +689 -0
- package/.agents/skills/gstack/test/helpers/gemini-session-runner.test.ts +104 -0
- package/.agents/skills/gstack/test/helpers/gemini-session-runner.ts +201 -0
- package/.agents/skills/gstack/test/helpers/llm-judge.ts +130 -0
- package/.agents/skills/gstack/test/helpers/observability.test.ts +283 -0
- package/.agents/skills/gstack/test/helpers/session-runner.test.ts +96 -0
- package/.agents/skills/gstack/test/helpers/session-runner.ts +357 -0
- package/.agents/skills/gstack/test/helpers/skill-parser.ts +206 -0
- package/.agents/skills/gstack/test/helpers/touchfiles.ts +260 -0
- package/.agents/skills/gstack/test/hook-scripts.test.ts +373 -0
- package/.agents/skills/gstack/test/skill-e2e-browse.test.ts +293 -0
- package/.agents/skills/gstack/test/skill-e2e-deploy.test.ts +279 -0
- package/.agents/skills/gstack/test/skill-e2e-design.test.ts +614 -0
- package/.agents/skills/gstack/test/skill-e2e-plan.test.ts +538 -0
- package/.agents/skills/gstack/test/skill-e2e-qa-bugs.test.ts +194 -0
- package/.agents/skills/gstack/test/skill-e2e-qa-workflow.test.ts +412 -0
- package/.agents/skills/gstack/test/skill-e2e-review.test.ts +535 -0
- package/.agents/skills/gstack/test/skill-e2e-workflow.test.ts +586 -0
- package/.agents/skills/gstack/test/skill-e2e.test.ts +3325 -0
- package/.agents/skills/gstack/test/skill-llm-eval.test.ts +787 -0
- package/.agents/skills/gstack/test/skill-parser.test.ts +179 -0
- package/.agents/skills/gstack/test/skill-routing-e2e.test.ts +605 -0
- package/.agents/skills/gstack/test/skill-validation.test.ts +1520 -0
- package/.agents/skills/gstack/test/telemetry.test.ts +278 -0
- package/.agents/skills/gstack/test/touchfiles.test.ts +262 -0
- package/.agents/skills/gstack/unfreeze/SKILL.md +40 -0
- package/.agents/skills/gstack/unfreeze/SKILL.md.tmpl +38 -0
- package/README.md +12 -7
- package/README_KO.md +12 -6
- package/package.json +3 -2
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { COMMAND_DESCRIPTIONS } from '../browse/src/commands';
|
|
3
|
+
import { SNAPSHOT_FLAGS } from '../browse/src/snapshot';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(import.meta.dir, '..');
|
|
8
|
+
|
|
9
|
+
// Dynamic template discovery — matches the generator's findTemplates() behavior.
|
|
10
|
+
// New skills automatically get test coverage without updating a static list.
|
|
11
|
+
const ALL_SKILLS = (() => {
|
|
12
|
+
const skills: Array<{ dir: string; name: string }> = [];
|
|
13
|
+
if (fs.existsSync(path.join(ROOT, 'SKILL.md.tmpl'))) {
|
|
14
|
+
skills.push({ dir: '.', name: 'root gstack' });
|
|
15
|
+
}
|
|
16
|
+
for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) {
|
|
17
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
18
|
+
if (fs.existsSync(path.join(ROOT, entry.name, 'SKILL.md.tmpl'))) {
|
|
19
|
+
skills.push({ dir: entry.name, name: entry.name });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return skills;
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
describe('gen-skill-docs', () => {
|
|
26
|
+
test('generated SKILL.md contains all command categories', () => {
|
|
27
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
28
|
+
const categories = new Set(Object.values(COMMAND_DESCRIPTIONS).map(d => d.category));
|
|
29
|
+
for (const cat of categories) {
|
|
30
|
+
expect(content).toContain(`### ${cat}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('generated SKILL.md contains all commands', () => {
|
|
35
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
36
|
+
for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
|
|
37
|
+
const display = meta.usage || cmd;
|
|
38
|
+
expect(content).toContain(display);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('command table is sorted alphabetically within categories', () => {
|
|
43
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
44
|
+
// Extract command names from the Navigation section as a test
|
|
45
|
+
const navSection = content.match(/### Navigation\n\|.*\n\|.*\n([\s\S]*?)(?=\n###|\n## )/);
|
|
46
|
+
expect(navSection).not.toBeNull();
|
|
47
|
+
const rows = navSection![1].trim().split('\n');
|
|
48
|
+
const commands = rows.map(r => {
|
|
49
|
+
const match = r.match(/\| `(\w+)/);
|
|
50
|
+
return match ? match[1] : '';
|
|
51
|
+
}).filter(Boolean);
|
|
52
|
+
const sorted = [...commands].sort();
|
|
53
|
+
expect(commands).toEqual(sorted);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('generated header is present in SKILL.md', () => {
|
|
57
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
58
|
+
expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl');
|
|
59
|
+
expect(content).toContain('Regenerate: bun run gen:skill-docs');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('generated header is present in browse/SKILL.md', () => {
|
|
63
|
+
const content = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md'), 'utf-8');
|
|
64
|
+
expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('snapshot flags section contains all flags', () => {
|
|
68
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
69
|
+
for (const flag of SNAPSHOT_FLAGS) {
|
|
70
|
+
expect(content).toContain(flag.short);
|
|
71
|
+
expect(content).toContain(flag.description);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('every skill has a SKILL.md.tmpl template', () => {
|
|
76
|
+
for (const skill of ALL_SKILLS) {
|
|
77
|
+
const tmplPath = path.join(ROOT, skill.dir, 'SKILL.md.tmpl');
|
|
78
|
+
expect(fs.existsSync(tmplPath)).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('every skill has a generated SKILL.md with auto-generated header', () => {
|
|
83
|
+
for (const skill of ALL_SKILLS) {
|
|
84
|
+
const mdPath = path.join(ROOT, skill.dir, 'SKILL.md');
|
|
85
|
+
expect(fs.existsSync(mdPath)).toBe(true);
|
|
86
|
+
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
87
|
+
expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl');
|
|
88
|
+
expect(content).toContain('Regenerate: bun run gen:skill-docs');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('every generated SKILL.md has valid YAML frontmatter', () => {
|
|
93
|
+
for (const skill of ALL_SKILLS) {
|
|
94
|
+
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
|
95
|
+
expect(content.startsWith('---\n')).toBe(true);
|
|
96
|
+
expect(content).toContain('name:');
|
|
97
|
+
expect(content).toContain('description:');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('generated files are fresh (match --dry-run)', () => {
|
|
102
|
+
const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--dry-run'], {
|
|
103
|
+
cwd: ROOT,
|
|
104
|
+
stdout: 'pipe',
|
|
105
|
+
stderr: 'pipe',
|
|
106
|
+
});
|
|
107
|
+
expect(result.exitCode).toBe(0);
|
|
108
|
+
const output = result.stdout.toString();
|
|
109
|
+
// Every skill should be FRESH
|
|
110
|
+
for (const skill of ALL_SKILLS) {
|
|
111
|
+
const file = skill.dir === '.' ? 'SKILL.md' : `${skill.dir}/SKILL.md`;
|
|
112
|
+
expect(output).toContain(`FRESH: ${file}`);
|
|
113
|
+
}
|
|
114
|
+
expect(output).not.toContain('STALE');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('no generated SKILL.md contains unresolved placeholders', () => {
|
|
118
|
+
for (const skill of ALL_SKILLS) {
|
|
119
|
+
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
|
120
|
+
const unresolved = content.match(/\{\{[A-Z_]+\}\}/g);
|
|
121
|
+
expect(unresolved).toBeNull();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('templates contain placeholders', () => {
|
|
126
|
+
const rootTmpl = fs.readFileSync(path.join(ROOT, 'SKILL.md.tmpl'), 'utf-8');
|
|
127
|
+
expect(rootTmpl).toContain('{{COMMAND_REFERENCE}}');
|
|
128
|
+
expect(rootTmpl).toContain('{{SNAPSHOT_FLAGS}}');
|
|
129
|
+
expect(rootTmpl).toContain('{{PREAMBLE}}');
|
|
130
|
+
|
|
131
|
+
const browseTmpl = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md.tmpl'), 'utf-8');
|
|
132
|
+
expect(browseTmpl).toContain('{{COMMAND_REFERENCE}}');
|
|
133
|
+
expect(browseTmpl).toContain('{{SNAPSHOT_FLAGS}}');
|
|
134
|
+
expect(browseTmpl).toContain('{{PREAMBLE}}');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('generated SKILL.md contains contributor mode check', () => {
|
|
138
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
139
|
+
expect(content).toContain('Contributor Mode');
|
|
140
|
+
expect(content).toContain('gstack_contributor');
|
|
141
|
+
expect(content).toContain('contributor-logs');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('generated SKILL.md contains session awareness', () => {
|
|
145
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
146
|
+
expect(content).toContain('_SESSIONS');
|
|
147
|
+
expect(content).toContain('RECOMMENDATION');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('generated SKILL.md contains branch detection', () => {
|
|
151
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
152
|
+
expect(content).toContain('_BRANCH');
|
|
153
|
+
expect(content).toContain('git branch --show-current');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('generated SKILL.md contains ELI16 simplification rules', () => {
|
|
157
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
158
|
+
expect(content).toContain('No raw function names');
|
|
159
|
+
expect(content).toContain('plain English');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('generated SKILL.md contains telemetry line', () => {
|
|
163
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
164
|
+
expect(content).toContain('skill-usage.jsonl');
|
|
165
|
+
expect(content).toContain('~/.gstack/analytics');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('preamble-using skills have correct skill name in telemetry', () => {
|
|
169
|
+
const PREAMBLE_SKILLS = [
|
|
170
|
+
{ dir: '.', name: 'gstack' },
|
|
171
|
+
{ dir: 'ship', name: 'ship' },
|
|
172
|
+
{ dir: 'review', name: 'review' },
|
|
173
|
+
{ dir: 'qa', name: 'qa' },
|
|
174
|
+
{ dir: 'retro', name: 'retro' },
|
|
175
|
+
];
|
|
176
|
+
for (const skill of PREAMBLE_SKILLS) {
|
|
177
|
+
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
|
178
|
+
expect(content).toContain(`"skill":"${skill.name}"`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('qa and qa-only templates use QA_METHODOLOGY placeholder', () => {
|
|
183
|
+
const qaTmpl = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md.tmpl'), 'utf-8');
|
|
184
|
+
expect(qaTmpl).toContain('{{QA_METHODOLOGY}}');
|
|
185
|
+
|
|
186
|
+
const qaOnlyTmpl = fs.readFileSync(path.join(ROOT, 'qa-only', 'SKILL.md.tmpl'), 'utf-8');
|
|
187
|
+
expect(qaOnlyTmpl).toContain('{{QA_METHODOLOGY}}');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('QA_METHODOLOGY appears expanded in both qa and qa-only generated files', () => {
|
|
191
|
+
const qaContent = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
|
|
192
|
+
const qaOnlyContent = fs.readFileSync(path.join(ROOT, 'qa-only', 'SKILL.md'), 'utf-8');
|
|
193
|
+
|
|
194
|
+
// Both should contain the health score rubric
|
|
195
|
+
expect(qaContent).toContain('Health Score Rubric');
|
|
196
|
+
expect(qaOnlyContent).toContain('Health Score Rubric');
|
|
197
|
+
|
|
198
|
+
// Both should contain framework guidance
|
|
199
|
+
expect(qaContent).toContain('Framework-Specific Guidance');
|
|
200
|
+
expect(qaOnlyContent).toContain('Framework-Specific Guidance');
|
|
201
|
+
|
|
202
|
+
// Both should contain the important rules
|
|
203
|
+
expect(qaContent).toContain('Important Rules');
|
|
204
|
+
expect(qaOnlyContent).toContain('Important Rules');
|
|
205
|
+
|
|
206
|
+
// Both should contain the 6 phases
|
|
207
|
+
expect(qaContent).toContain('Phase 1');
|
|
208
|
+
expect(qaOnlyContent).toContain('Phase 1');
|
|
209
|
+
expect(qaContent).toContain('Phase 6');
|
|
210
|
+
expect(qaOnlyContent).toContain('Phase 6');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('qa-only has no-fix guardrails', () => {
|
|
214
|
+
const qaOnlyContent = fs.readFileSync(path.join(ROOT, 'qa-only', 'SKILL.md'), 'utf-8');
|
|
215
|
+
expect(qaOnlyContent).toContain('Never fix bugs');
|
|
216
|
+
expect(qaOnlyContent).toContain('NEVER fix anything');
|
|
217
|
+
// Should not have Edit, Glob, or Grep in allowed-tools
|
|
218
|
+
expect(qaOnlyContent).not.toMatch(/allowed-tools:[\s\S]*?Edit/);
|
|
219
|
+
expect(qaOnlyContent).not.toMatch(/allowed-tools:[\s\S]*?Glob/);
|
|
220
|
+
expect(qaOnlyContent).not.toMatch(/allowed-tools:[\s\S]*?Grep/);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('qa has fix-loop tools and phases', () => {
|
|
224
|
+
const qaContent = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
|
|
225
|
+
// Should have Edit, Glob, Grep in allowed-tools
|
|
226
|
+
expect(qaContent).toContain('Edit');
|
|
227
|
+
expect(qaContent).toContain('Glob');
|
|
228
|
+
expect(qaContent).toContain('Grep');
|
|
229
|
+
// Should have fix-loop phases
|
|
230
|
+
expect(qaContent).toContain('Phase 7');
|
|
231
|
+
expect(qaContent).toContain('Phase 8');
|
|
232
|
+
expect(qaContent).toContain('Fix Loop');
|
|
233
|
+
expect(qaContent).toContain('Triage');
|
|
234
|
+
expect(qaContent).toContain('WTF');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('BASE_BRANCH_DETECT resolver', () => {
|
|
239
|
+
// Find a generated SKILL.md that uses the placeholder (ship is guaranteed to)
|
|
240
|
+
const shipContent = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
|
241
|
+
|
|
242
|
+
test('resolver output contains PR base detection command', () => {
|
|
243
|
+
expect(shipContent).toContain('gh pr view --json baseRefName');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('resolver output contains repo default branch detection command', () => {
|
|
247
|
+
expect(shipContent).toContain('gh repo view --json defaultBranchRef');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('resolver output contains fallback to main', () => {
|
|
251
|
+
expect(shipContent).toMatch(/fall\s*back\s+to\s+`main`/i);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('resolver output uses "the base branch" phrasing', () => {
|
|
255
|
+
expect(shipContent).toContain('the base branch');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Quality evals — catch description regressions.
|
|
261
|
+
*
|
|
262
|
+
* These test that generated output is *useful for an AI agent*,
|
|
263
|
+
* not just structurally valid. Each test targets a specific
|
|
264
|
+
* regression we actually shipped and caught in review.
|
|
265
|
+
*/
|
|
266
|
+
describe('description quality evals', () => {
|
|
267
|
+
// Regression: snapshot flags lost value hints (-d <N>, -s <sel>, -o <path>)
|
|
268
|
+
test('snapshot flags with values include value hints in output', () => {
|
|
269
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
270
|
+
for (const flag of SNAPSHOT_FLAGS) {
|
|
271
|
+
if (flag.takesValue) {
|
|
272
|
+
expect(flag.valueHint).toBeDefined();
|
|
273
|
+
expect(content).toContain(`${flag.short} ${flag.valueHint}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Regression: "is" lost the valid states enum
|
|
279
|
+
test('is command lists valid state values', () => {
|
|
280
|
+
const desc = COMMAND_DESCRIPTIONS['is'].description;
|
|
281
|
+
for (const state of ['visible', 'hidden', 'enabled', 'disabled', 'checked', 'editable', 'focused']) {
|
|
282
|
+
expect(desc).toContain(state);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Regression: "press" lost common key examples
|
|
287
|
+
test('press command lists example keys', () => {
|
|
288
|
+
const desc = COMMAND_DESCRIPTIONS['press'].description;
|
|
289
|
+
expect(desc).toContain('Enter');
|
|
290
|
+
expect(desc).toContain('Tab');
|
|
291
|
+
expect(desc).toContain('Escape');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Regression: "console" lost --errors filter note
|
|
295
|
+
test('console command describes --errors behavior', () => {
|
|
296
|
+
const desc = COMMAND_DESCRIPTIONS['console'].description;
|
|
297
|
+
expect(desc).toContain('--errors');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Regression: snapshot -i lost "@e refs" context
|
|
301
|
+
test('snapshot -i mentions @e refs', () => {
|
|
302
|
+
const flag = SNAPSHOT_FLAGS.find(f => f.short === '-i')!;
|
|
303
|
+
expect(flag.description).toContain('@e');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Regression: snapshot -C lost "@c refs" context
|
|
307
|
+
test('snapshot -C mentions @c refs', () => {
|
|
308
|
+
const flag = SNAPSHOT_FLAGS.find(f => f.short === '-C')!;
|
|
309
|
+
expect(flag.description).toContain('@c');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Guard: every description must be at least 8 chars (catches empty or stub descriptions)
|
|
313
|
+
test('all command descriptions have meaningful length', () => {
|
|
314
|
+
for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
|
|
315
|
+
expect(meta.description.length).toBeGreaterThanOrEqual(8);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Guard: snapshot flag descriptions must be at least 10 chars
|
|
320
|
+
test('all snapshot flag descriptions have meaningful length', () => {
|
|
321
|
+
for (const flag of SNAPSHOT_FLAGS) {
|
|
322
|
+
expect(flag.description.length).toBeGreaterThanOrEqual(10);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Guard: descriptions must not contain pipe (breaks markdown table cells)
|
|
327
|
+
// Usage strings are backtick-wrapped in the table so pipes there are safe.
|
|
328
|
+
test('no command description contains pipe character', () => {
|
|
329
|
+
for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
|
|
330
|
+
expect(meta.description).not.toContain('|');
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Guard: generated output uses → not ->
|
|
335
|
+
test('generated SKILL.md uses unicode arrows', () => {
|
|
336
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
337
|
+
// Check the Tips section specifically (where we regressed -> from →)
|
|
338
|
+
const tipsSection = content.slice(content.indexOf('## Tips'));
|
|
339
|
+
expect(tipsSection).toContain('→');
|
|
340
|
+
expect(tipsSection).not.toContain('->');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('REVIEW_DASHBOARD resolver', () => {
|
|
345
|
+
const REVIEW_SKILLS = ['plan-ceo-review', 'plan-eng-review', 'plan-design-review'];
|
|
346
|
+
|
|
347
|
+
for (const skill of REVIEW_SKILLS) {
|
|
348
|
+
test(`review dashboard appears in ${skill} generated file`, () => {
|
|
349
|
+
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
|
350
|
+
expect(content).toContain('gstack-review');
|
|
351
|
+
expect(content).toContain('REVIEW READINESS DASHBOARD');
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
test('review dashboard appears in ship generated file', () => {
|
|
356
|
+
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
|
357
|
+
expect(content).toContain('reviews.jsonl');
|
|
358
|
+
expect(content).toContain('REVIEW READINESS DASHBOARD');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('resolver output contains key dashboard elements', () => {
|
|
362
|
+
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
|
363
|
+
expect(content).toContain('VERDICT');
|
|
364
|
+
expect(content).toContain('CLEARED');
|
|
365
|
+
expect(content).toContain('Eng Review');
|
|
366
|
+
expect(content).toContain('7 days');
|
|
367
|
+
expect(content).toContain('Design Review');
|
|
368
|
+
expect(content).toContain('skip_eng_review');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('dashboard bash block includes git HEAD for staleness detection', () => {
|
|
372
|
+
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
|
373
|
+
expect(content).toContain('git rev-parse --short HEAD');
|
|
374
|
+
expect(content).toContain('---HEAD---');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('dashboard includes staleness detection prose', () => {
|
|
378
|
+
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
|
379
|
+
expect(content).toContain('Staleness detection');
|
|
380
|
+
expect(content).toContain('commit');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
for (const skill of REVIEW_SKILLS) {
|
|
384
|
+
test(`${skill} contains review chaining section`, () => {
|
|
385
|
+
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
|
386
|
+
expect(content).toContain('Review Chaining');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test(`${skill} Review Log includes commit field`, () => {
|
|
390
|
+
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
|
391
|
+
expect(content).toContain('"commit"');
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
test('plan-ceo-review chaining mentions eng and design reviews', () => {
|
|
396
|
+
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
|
397
|
+
expect(content).toContain('/plan-eng-review');
|
|
398
|
+
expect(content).toContain('/plan-design-review');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('plan-eng-review chaining mentions design and ceo reviews', () => {
|
|
402
|
+
const content = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
|
|
403
|
+
expect(content).toContain('/plan-design-review');
|
|
404
|
+
expect(content).toContain('/plan-ceo-review');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('plan-design-review chaining mentions eng and ceo reviews', () => {
|
|
408
|
+
const content = fs.readFileSync(path.join(ROOT, 'plan-design-review', 'SKILL.md'), 'utf-8');
|
|
409
|
+
expect(content).toContain('/plan-eng-review');
|
|
410
|
+
expect(content).toContain('/plan-ceo-review');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('ship does NOT contain review chaining', () => {
|
|
414
|
+
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
|
415
|
+
expect(content).not.toContain('Review Chaining');
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ─── Test Coverage Audit Resolver Tests ─────────────────────
|
|
420
|
+
|
|
421
|
+
describe('TEST_COVERAGE_AUDIT placeholders', () => {
|
|
422
|
+
const planSkill = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
|
|
423
|
+
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
|
424
|
+
const reviewSkill = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
|
425
|
+
|
|
426
|
+
test('all three modes share codepath tracing methodology', () => {
|
|
427
|
+
const sharedPhrases = [
|
|
428
|
+
'Trace data flow',
|
|
429
|
+
'Diagram the execution',
|
|
430
|
+
'Quality scoring rubric',
|
|
431
|
+
'★★★',
|
|
432
|
+
'★★',
|
|
433
|
+
'GAP',
|
|
434
|
+
];
|
|
435
|
+
for (const phrase of sharedPhrases) {
|
|
436
|
+
expect(planSkill).toContain(phrase);
|
|
437
|
+
expect(shipSkill).toContain(phrase);
|
|
438
|
+
expect(reviewSkill).toContain(phrase);
|
|
439
|
+
}
|
|
440
|
+
// Plan mode traces the plan, not a git diff
|
|
441
|
+
expect(planSkill).toContain('Trace every codepath in the plan');
|
|
442
|
+
expect(planSkill).not.toContain('git diff origin');
|
|
443
|
+
// Ship and review modes trace the diff
|
|
444
|
+
expect(shipSkill).toContain('Trace every codepath changed');
|
|
445
|
+
expect(reviewSkill).toContain('Trace every codepath changed');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('all three modes include E2E decision matrix', () => {
|
|
449
|
+
for (const skill of [planSkill, shipSkill, reviewSkill]) {
|
|
450
|
+
expect(skill).toContain('E2E Test Decision Matrix');
|
|
451
|
+
expect(skill).toContain('→E2E');
|
|
452
|
+
expect(skill).toContain('→EVAL');
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('all three modes include regression rule', () => {
|
|
457
|
+
for (const skill of [planSkill, shipSkill, reviewSkill]) {
|
|
458
|
+
expect(skill).toContain('REGRESSION RULE');
|
|
459
|
+
expect(skill).toContain('IRON RULE');
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('all three modes include test framework detection', () => {
|
|
464
|
+
for (const skill of [planSkill, shipSkill, reviewSkill]) {
|
|
465
|
+
expect(skill).toContain('Test Framework Detection');
|
|
466
|
+
expect(skill).toContain('CLAUDE.md');
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('plan mode adds tests to plan + includes test plan artifact', () => {
|
|
471
|
+
expect(planSkill).toContain('Add missing tests to the plan');
|
|
472
|
+
expect(planSkill).toContain('eng-review-test-plan');
|
|
473
|
+
expect(planSkill).toContain('Test Plan Artifact');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('ship mode auto-generates tests + includes before/after count', () => {
|
|
477
|
+
expect(shipSkill).toContain('Generate tests for uncovered paths');
|
|
478
|
+
expect(shipSkill).toContain('Before/after test count');
|
|
479
|
+
expect(shipSkill).toContain('30 code paths max');
|
|
480
|
+
expect(shipSkill).toContain('ship-test-plan');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('review mode generates via Fix-First + gaps are INFORMATIONAL', () => {
|
|
484
|
+
expect(reviewSkill).toContain('Fix-First');
|
|
485
|
+
expect(reviewSkill).toContain('INFORMATIONAL');
|
|
486
|
+
expect(reviewSkill).toContain('Step 4.75');
|
|
487
|
+
expect(reviewSkill).toContain('subsumes the "Test Gaps" category');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('plan mode does NOT include ship-specific content', () => {
|
|
491
|
+
expect(planSkill).not.toContain('Before/after test count');
|
|
492
|
+
expect(planSkill).not.toContain('30 code paths max');
|
|
493
|
+
expect(planSkill).not.toContain('ship-test-plan');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('review mode does NOT include test plan artifact', () => {
|
|
497
|
+
expect(reviewSkill).not.toContain('Test Plan Artifact');
|
|
498
|
+
expect(reviewSkill).not.toContain('eng-review-test-plan');
|
|
499
|
+
expect(reviewSkill).not.toContain('ship-test-plan');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Regression guard: ship output contains key phrases from before the refactor
|
|
503
|
+
test('ship SKILL.md regression guard — key phrases preserved', () => {
|
|
504
|
+
const regressionPhrases = [
|
|
505
|
+
'100% coverage is the goal',
|
|
506
|
+
'ASCII coverage diagram',
|
|
507
|
+
'processPayment',
|
|
508
|
+
'refundPayment',
|
|
509
|
+
'billing.test.ts',
|
|
510
|
+
'checkout.e2e.ts',
|
|
511
|
+
'COVERAGE:',
|
|
512
|
+
'QUALITY:',
|
|
513
|
+
'GAPS:',
|
|
514
|
+
'Code paths:',
|
|
515
|
+
'User flows:',
|
|
516
|
+
];
|
|
517
|
+
for (const phrase of regressionPhrases) {
|
|
518
|
+
expect(shipSkill).toContain(phrase);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// --- {{TEST_FAILURE_TRIAGE}} resolver tests ---
|
|
524
|
+
|
|
525
|
+
describe('TEST_FAILURE_TRIAGE resolver', () => {
|
|
526
|
+
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
|
527
|
+
|
|
528
|
+
test('contains all 4 triage steps', () => {
|
|
529
|
+
expect(shipSkill).toContain('Step T1: Classify each failure');
|
|
530
|
+
expect(shipSkill).toContain('Step T2: Handle in-branch failures');
|
|
531
|
+
expect(shipSkill).toContain('Step T3: Handle pre-existing failures');
|
|
532
|
+
expect(shipSkill).toContain('Step T4: Execute the chosen action');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('T1 includes classification criteria (in-branch vs pre-existing)', () => {
|
|
536
|
+
expect(shipSkill).toContain('In-branch');
|
|
537
|
+
expect(shipSkill).toContain('Likely pre-existing');
|
|
538
|
+
expect(shipSkill).toContain('git diff origin/');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('T3 branches on REPO_MODE (solo vs collaborative)', () => {
|
|
542
|
+
expect(shipSkill).toContain('REPO_MODE');
|
|
543
|
+
expect(shipSkill).toContain('solo');
|
|
544
|
+
expect(shipSkill).toContain('collaborative');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('solo mode offers fix-now, TODO, and skip options', () => {
|
|
548
|
+
expect(shipSkill).toContain('Investigate and fix now');
|
|
549
|
+
expect(shipSkill).toContain('Add as P0 TODO');
|
|
550
|
+
expect(shipSkill).toContain('Skip');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('collaborative mode offers blame + assign option', () => {
|
|
554
|
+
expect(shipSkill).toContain('Blame + assign GitHub issue');
|
|
555
|
+
expect(shipSkill).toContain('gh issue create');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('defaults ambiguous failures to in-branch (safety)', () => {
|
|
559
|
+
expect(shipSkill).toContain('When ambiguous, default to in-branch');
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// --- {{PLAN_FILE_REVIEW_REPORT}} resolver tests ---
|
|
564
|
+
|
|
565
|
+
describe('PLAN_FILE_REVIEW_REPORT resolver', () => {
|
|
566
|
+
const REVIEW_SKILLS = ['plan-ceo-review', 'plan-eng-review', 'plan-design-review', 'codex'];
|
|
567
|
+
|
|
568
|
+
for (const skill of REVIEW_SKILLS) {
|
|
569
|
+
test(`plan file review report appears in ${skill} generated file`, () => {
|
|
570
|
+
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
|
571
|
+
expect(content).toContain('GSTACK REVIEW REPORT');
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
test('resolver output contains key report elements', () => {
|
|
576
|
+
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
|
577
|
+
expect(content).toContain('Trigger');
|
|
578
|
+
expect(content).toContain('Findings');
|
|
579
|
+
expect(content).toContain('VERDICT');
|
|
580
|
+
expect(content).toContain('/plan-ceo-review');
|
|
581
|
+
expect(content).toContain('/plan-eng-review');
|
|
582
|
+
expect(content).toContain('/plan-design-review');
|
|
583
|
+
expect(content).toContain('/codex review');
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// --- {{SPEC_REVIEW_LOOP}} resolver tests ---
|
|
588
|
+
|
|
589
|
+
describe('SPEC_REVIEW_LOOP resolver', () => {
|
|
590
|
+
const content = fs.readFileSync(path.join(ROOT, 'office-hours', 'SKILL.md'), 'utf-8');
|
|
591
|
+
|
|
592
|
+
test('contains all 5 review dimensions', () => {
|
|
593
|
+
for (const dim of ['Completeness', 'Consistency', 'Clarity', 'Scope', 'Feasibility']) {
|
|
594
|
+
expect(content).toContain(dim);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test('references Agent tool for subagent dispatch', () => {
|
|
599
|
+
expect(content).toMatch(/Agent.*tool/i);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('specifies max 3 iterations', () => {
|
|
603
|
+
expect(content).toMatch(/3.*iteration|maximum.*3/i);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test('includes quality score', () => {
|
|
607
|
+
expect(content).toContain('quality score');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('includes metrics path', () => {
|
|
611
|
+
expect(content).toContain('spec-review.jsonl');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('includes convergence guard', () => {
|
|
615
|
+
expect(content).toMatch(/[Cc]onvergence/);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('includes graceful failure handling', () => {
|
|
619
|
+
expect(content).toMatch(/skip.*review|unavailable/i);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// --- {{DESIGN_SKETCH}} resolver tests ---
|
|
624
|
+
|
|
625
|
+
describe('DESIGN_SKETCH resolver', () => {
|
|
626
|
+
const content = fs.readFileSync(path.join(ROOT, 'office-hours', 'SKILL.md'), 'utf-8');
|
|
627
|
+
|
|
628
|
+
test('references DESIGN.md for design system constraints', () => {
|
|
629
|
+
expect(content).toContain('DESIGN.md');
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test('contains wireframe or sketch terminology', () => {
|
|
633
|
+
expect(content).toMatch(/wireframe|sketch/i);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test('references browse binary for rendering', () => {
|
|
637
|
+
expect(content).toContain('$B goto');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('references screenshot capture', () => {
|
|
641
|
+
expect(content).toContain('$B screenshot');
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('specifies rough aesthetic', () => {
|
|
645
|
+
expect(content).toMatch(/[Rr]ough|hand-drawn/);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('includes skip conditions', () => {
|
|
649
|
+
expect(content).toMatch(/no UI component|skip/i);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// --- {{BENEFITS_FROM}} resolver tests ---
|
|
654
|
+
|
|
655
|
+
describe('BENEFITS_FROM resolver', () => {
|
|
656
|
+
const ceoContent = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
|
657
|
+
const engContent = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
|
|
658
|
+
|
|
659
|
+
test('plan-ceo-review contains prerequisite skill offer', () => {
|
|
660
|
+
expect(ceoContent).toContain('Prerequisite Skill Offer');
|
|
661
|
+
expect(ceoContent).toContain('/office-hours');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test('plan-eng-review contains prerequisite skill offer', () => {
|
|
665
|
+
expect(engContent).toContain('Prerequisite Skill Offer');
|
|
666
|
+
expect(engContent).toContain('/office-hours');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test('offer includes graceful decline', () => {
|
|
670
|
+
expect(ceoContent).toContain('No worries');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test('skills without benefits-from do NOT have prerequisite offer', () => {
|
|
674
|
+
const qaContent = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
|
|
675
|
+
expect(qaContent).not.toContain('Prerequisite Skill Offer');
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// ─── Codex Generation Tests ─────────────────────────────────
|
|
680
|
+
|
|
681
|
+
describe('Codex generation (--host codex)', () => {
|
|
682
|
+
const AGENTS_DIR = path.join(ROOT, '.agents', 'skills');
|
|
683
|
+
|
|
684
|
+
// Dynamic discovery of expected Codex skills: all templates except /codex
|
|
685
|
+
const CODEX_SKILLS = (() => {
|
|
686
|
+
const skills: Array<{ dir: string; codexName: string }> = [];
|
|
687
|
+
if (fs.existsSync(path.join(ROOT, 'SKILL.md.tmpl'))) {
|
|
688
|
+
skills.push({ dir: '.', codexName: 'gstack' });
|
|
689
|
+
}
|
|
690
|
+
for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) {
|
|
691
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
692
|
+
if (entry.name === 'codex') continue; // /codex is excluded from Codex output
|
|
693
|
+
if (!fs.existsSync(path.join(ROOT, entry.name, 'SKILL.md.tmpl'))) continue;
|
|
694
|
+
const codexName = entry.name.startsWith('gstack-') ? entry.name : `gstack-${entry.name}`;
|
|
695
|
+
skills.push({ dir: entry.name, codexName });
|
|
696
|
+
}
|
|
697
|
+
return skills;
|
|
698
|
+
})();
|
|
699
|
+
|
|
700
|
+
test('--host codex generates correct output paths', () => {
|
|
701
|
+
for (const skill of CODEX_SKILLS) {
|
|
702
|
+
const skillMd = path.join(AGENTS_DIR, skill.codexName, 'SKILL.md');
|
|
703
|
+
expect(fs.existsSync(skillMd)).toBe(true);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('codexSkillName mapping: root is gstack, others are gstack-{dir}', () => {
|
|
708
|
+
// Root → gstack
|
|
709
|
+
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack', 'SKILL.md'))).toBe(true);
|
|
710
|
+
// Subdirectories → gstack-{dir}
|
|
711
|
+
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'))).toBe(true);
|
|
712
|
+
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'))).toBe(true);
|
|
713
|
+
// gstack-upgrade doesn't double-prefix
|
|
714
|
+
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-upgrade', 'SKILL.md'))).toBe(true);
|
|
715
|
+
// No double-prefix: gstack-gstack-upgrade must NOT exist
|
|
716
|
+
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-gstack-upgrade', 'SKILL.md'))).toBe(false);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test('Codex frontmatter has ONLY name + description', () => {
|
|
720
|
+
for (const skill of CODEX_SKILLS) {
|
|
721
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
|
|
722
|
+
expect(content.startsWith('---\n')).toBe(true);
|
|
723
|
+
const fmEnd = content.indexOf('\n---', 4);
|
|
724
|
+
expect(fmEnd).toBeGreaterThan(0);
|
|
725
|
+
const frontmatter = content.slice(4, fmEnd);
|
|
726
|
+
// Must have name and description
|
|
727
|
+
expect(frontmatter).toContain('name:');
|
|
728
|
+
expect(frontmatter).toContain('description:');
|
|
729
|
+
// Must NOT have allowed-tools, version, or hooks
|
|
730
|
+
expect(frontmatter).not.toContain('allowed-tools:');
|
|
731
|
+
expect(frontmatter).not.toContain('version:');
|
|
732
|
+
expect(frontmatter).not.toContain('hooks:');
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test('no .claude/skills/ in Codex output', () => {
|
|
737
|
+
for (const skill of CODEX_SKILLS) {
|
|
738
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
|
|
739
|
+
expect(content).not.toContain('.claude/skills');
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test('no ~/.claude/ paths in Codex output', () => {
|
|
744
|
+
for (const skill of CODEX_SKILLS) {
|
|
745
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
|
|
746
|
+
expect(content).not.toContain('~/.claude/');
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test('/codex skill excluded from Codex output', () => {
|
|
751
|
+
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-codex', 'SKILL.md'))).toBe(false);
|
|
752
|
+
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-codex'))).toBe(false);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test('Codex review step stripped from Codex-host ship and review', () => {
|
|
756
|
+
const shipContent = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8');
|
|
757
|
+
expect(shipContent).not.toContain('codex review --base');
|
|
758
|
+
expect(shipContent).not.toContain('CODEX_REVIEWS');
|
|
759
|
+
|
|
760
|
+
const reviewContent = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8');
|
|
761
|
+
expect(reviewContent).not.toContain('codex review --base');
|
|
762
|
+
expect(reviewContent).not.toContain('CODEX_REVIEWS');
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test('--host codex --dry-run freshness', () => {
|
|
766
|
+
const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--dry-run'], {
|
|
767
|
+
cwd: ROOT,
|
|
768
|
+
stdout: 'pipe',
|
|
769
|
+
stderr: 'pipe',
|
|
770
|
+
});
|
|
771
|
+
expect(result.exitCode).toBe(0);
|
|
772
|
+
const output = result.stdout.toString();
|
|
773
|
+
// Every Codex skill should be FRESH
|
|
774
|
+
for (const skill of CODEX_SKILLS) {
|
|
775
|
+
expect(output).toContain(`FRESH: .agents/skills/${skill.codexName}/SKILL.md`);
|
|
776
|
+
}
|
|
777
|
+
expect(output).not.toContain('STALE');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test('--host agents alias produces same output as --host codex', () => {
|
|
781
|
+
const codexResult = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--dry-run'], {
|
|
782
|
+
cwd: ROOT,
|
|
783
|
+
stdout: 'pipe',
|
|
784
|
+
stderr: 'pipe',
|
|
785
|
+
});
|
|
786
|
+
const agentsResult = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'agents', '--dry-run'], {
|
|
787
|
+
cwd: ROOT,
|
|
788
|
+
stdout: 'pipe',
|
|
789
|
+
stderr: 'pipe',
|
|
790
|
+
});
|
|
791
|
+
expect(codexResult.exitCode).toBe(0);
|
|
792
|
+
expect(agentsResult.exitCode).toBe(0);
|
|
793
|
+
// Both should produce the same output (same FRESH lines)
|
|
794
|
+
expect(codexResult.stdout.toString()).toBe(agentsResult.stdout.toString());
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test('multiline descriptions preserved in Codex output', () => {
|
|
798
|
+
// office-hours has a multiline description — verify it survives the frontmatter transform
|
|
799
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-office-hours', 'SKILL.md'), 'utf-8');
|
|
800
|
+
const fmEnd = content.indexOf('\n---', 4);
|
|
801
|
+
const frontmatter = content.slice(4, fmEnd);
|
|
802
|
+
// Description should span multiple lines (block scalar)
|
|
803
|
+
const descLines = frontmatter.split('\n').filter(l => l.startsWith(' '));
|
|
804
|
+
expect(descLines.length).toBeGreaterThan(1);
|
|
805
|
+
// Verify key phrases survived
|
|
806
|
+
expect(frontmatter).toContain('YC Office Hours');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test('hook skills have safety prose and no hooks: in frontmatter', () => {
|
|
810
|
+
const HOOK_SKILLS = ['gstack-careful', 'gstack-freeze', 'gstack-guard'];
|
|
811
|
+
for (const skillName of HOOK_SKILLS) {
|
|
812
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, skillName, 'SKILL.md'), 'utf-8');
|
|
813
|
+
// Must have safety advisory prose
|
|
814
|
+
expect(content).toContain('Safety Advisory');
|
|
815
|
+
// Must NOT have hooks: in frontmatter
|
|
816
|
+
const fmEnd = content.indexOf('\n---', 4);
|
|
817
|
+
const frontmatter = content.slice(4, fmEnd);
|
|
818
|
+
expect(frontmatter).not.toContain('hooks:');
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test('all Codex SKILL.md files have auto-generated header', () => {
|
|
823
|
+
for (const skill of CODEX_SKILLS) {
|
|
824
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
|
|
825
|
+
expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl');
|
|
826
|
+
expect(content).toContain('Regenerate: bun run gen:skill-docs');
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test('Codex preamble uses codex paths', () => {
|
|
831
|
+
// Check a skill that has a preamble (review is a good candidate)
|
|
832
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8');
|
|
833
|
+
expect(content).toContain('~/.codex/skills/gstack');
|
|
834
|
+
expect(content).toContain('.agents/skills/gstack');
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// ─── Path rewriting regression tests ─────────────────────────
|
|
838
|
+
|
|
839
|
+
test('sidecar paths point to .agents/skills/gstack/review/ (not gstack-review/)', () => {
|
|
840
|
+
// Regression: gen-skill-docs rewrote .claude/skills/review → .agents/skills/gstack-review
|
|
841
|
+
// but setup puts sidecars under .agents/skills/gstack/review/. Must match setup layout.
|
|
842
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8');
|
|
843
|
+
// Correct: references to sidecar files use gstack/review/ path
|
|
844
|
+
expect(content).toContain('.agents/skills/gstack/review/checklist.md');
|
|
845
|
+
expect(content).toContain('.agents/skills/gstack/review/design-checklist.md');
|
|
846
|
+
// Wrong: must NOT reference gstack-review/checklist.md (file doesn't exist there)
|
|
847
|
+
expect(content).not.toContain('.agents/skills/gstack-review/checklist.md');
|
|
848
|
+
expect(content).not.toContain('.agents/skills/gstack-review/design-checklist.md');
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test('sidecar paths in ship skill point to gstack/review/ for pre-landing review', () => {
|
|
852
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8');
|
|
853
|
+
// Ship references the review checklist in its pre-landing review step
|
|
854
|
+
if (content.includes('checklist.md')) {
|
|
855
|
+
expect(content).toContain('.agents/skills/gstack/review/');
|
|
856
|
+
expect(content).not.toContain('.agents/skills/gstack-review/checklist');
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test('greptile-triage sidecar path is correct', () => {
|
|
861
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8');
|
|
862
|
+
if (content.includes('greptile-triage')) {
|
|
863
|
+
expect(content).toContain('.agents/skills/gstack/review/greptile-triage.md');
|
|
864
|
+
expect(content).not.toContain('.agents/skills/gstack-review/greptile-triage');
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test('all four path rewrite rules produce correct output', () => {
|
|
869
|
+
// Test each of the 4 path rewrite rules individually
|
|
870
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8');
|
|
871
|
+
|
|
872
|
+
// Rule 1: ~/.claude/skills/gstack → ~/.codex/skills/gstack
|
|
873
|
+
expect(content).not.toContain('~/.claude/skills/gstack');
|
|
874
|
+
expect(content).toContain('~/.codex/skills/gstack');
|
|
875
|
+
|
|
876
|
+
// Rule 2: .claude/skills/gstack → .agents/skills/gstack
|
|
877
|
+
expect(content).not.toContain('.claude/skills/gstack');
|
|
878
|
+
|
|
879
|
+
// Rule 3: .claude/skills/review → .agents/skills/gstack/review
|
|
880
|
+
expect(content).not.toContain('.claude/skills/review');
|
|
881
|
+
|
|
882
|
+
// Rule 4: .claude/skills → .agents/skills (catch-all)
|
|
883
|
+
expect(content).not.toContain('.claude/skills');
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test('path rewrite rules apply to all Codex skills with sidecar references', () => {
|
|
887
|
+
// Verify across ALL generated skills, not just review
|
|
888
|
+
for (const skill of CODEX_SKILLS) {
|
|
889
|
+
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
|
|
890
|
+
// No skill should reference Claude paths
|
|
891
|
+
expect(content).not.toContain('~/.claude/skills');
|
|
892
|
+
expect(content).not.toContain('.claude/skills');
|
|
893
|
+
// If a skill references checklist.md, it must use the correct sidecar path
|
|
894
|
+
if (content.includes('checklist.md') && !content.includes('design-checklist.md')) {
|
|
895
|
+
expect(content).not.toContain('gstack-review/checklist.md');
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// ─── Claude output regression guard ─────────────────────────
|
|
901
|
+
|
|
902
|
+
test('Claude output unchanged: review skill still uses .claude/skills/ paths', () => {
|
|
903
|
+
// Codex changes must NOT affect Claude output
|
|
904
|
+
const content = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
|
905
|
+
expect(content).toContain('.claude/skills/review/checklist.md');
|
|
906
|
+
expect(content).toContain('~/.claude/skills/gstack');
|
|
907
|
+
// Must NOT contain Codex paths
|
|
908
|
+
expect(content).not.toContain('.agents/skills');
|
|
909
|
+
expect(content).not.toContain('~/.codex/');
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test('Claude output unchanged: ship skill still uses .claude/skills/ paths', () => {
|
|
913
|
+
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
|
914
|
+
expect(content).toContain('~/.claude/skills/gstack');
|
|
915
|
+
expect(content).not.toContain('.agents/skills');
|
|
916
|
+
expect(content).not.toContain('~/.codex/');
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test('Claude output unchanged: all Claude skills have zero Codex paths', () => {
|
|
920
|
+
for (const skill of ALL_SKILLS) {
|
|
921
|
+
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
|
922
|
+
expect(content).not.toContain('~/.codex/');
|
|
923
|
+
expect(content).not.toContain('.agents/skills');
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// ─── Setup script validation ─────────────────────────────────
|
|
929
|
+
// These tests verify the setup script's install layout matches
|
|
930
|
+
// what the generator produces — catching the bug where setup
|
|
931
|
+
// installed Claude-format source dirs for Codex users.
|
|
932
|
+
|
|
933
|
+
describe('setup script validation', () => {
|
|
934
|
+
const setupContent = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8');
|
|
935
|
+
|
|
936
|
+
test('setup has separate link functions for Claude and Codex', () => {
|
|
937
|
+
expect(setupContent).toContain('link_claude_skill_dirs');
|
|
938
|
+
expect(setupContent).toContain('link_codex_skill_dirs');
|
|
939
|
+
// Old unified function must not exist
|
|
940
|
+
expect(setupContent).not.toMatch(/^link_skill_dirs\(\)/m);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test('Claude install uses link_claude_skill_dirs', () => {
|
|
944
|
+
// The Claude install section (section 4) should use the Claude function
|
|
945
|
+
const claudeSection = setupContent.slice(
|
|
946
|
+
setupContent.indexOf('# 4. Install for Claude'),
|
|
947
|
+
setupContent.indexOf('# 5. Install for Codex')
|
|
948
|
+
);
|
|
949
|
+
expect(claudeSection).toContain('link_claude_skill_dirs');
|
|
950
|
+
expect(claudeSection).not.toContain('link_codex_skill_dirs');
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
test('Codex install uses link_codex_skill_dirs', () => {
|
|
954
|
+
// The Codex install section (section 5) should use the Codex function
|
|
955
|
+
const codexSection = setupContent.slice(
|
|
956
|
+
setupContent.indexOf('# 5. Install for Codex'),
|
|
957
|
+
setupContent.indexOf('# 6. Create')
|
|
958
|
+
);
|
|
959
|
+
expect(codexSection).toContain('link_codex_skill_dirs');
|
|
960
|
+
expect(codexSection).not.toContain('link_claude_skill_dirs');
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test('link_codex_skill_dirs reads from .agents/skills/', () => {
|
|
964
|
+
// The Codex link function must reference .agents/skills for generated Codex skills
|
|
965
|
+
const fnStart = setupContent.indexOf('link_codex_skill_dirs()');
|
|
966
|
+
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
|
|
967
|
+
const fnBody = setupContent.slice(fnStart, fnEnd);
|
|
968
|
+
expect(fnBody).toContain('.agents/skills');
|
|
969
|
+
expect(fnBody).toContain('gstack*');
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
test('link_claude_skill_dirs creates relative symlinks', () => {
|
|
973
|
+
// Claude links should be relative: ln -snf "gstack/skill_name"
|
|
974
|
+
const fnStart = setupContent.indexOf('link_claude_skill_dirs()');
|
|
975
|
+
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
|
|
976
|
+
const fnBody = setupContent.slice(fnStart, fnEnd);
|
|
977
|
+
expect(fnBody).toContain('ln -snf "gstack/$skill_name"');
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
test('setup supports --host auto|claude|codex', () => {
|
|
981
|
+
expect(setupContent).toContain('--host');
|
|
982
|
+
expect(setupContent).toContain('claude|codex|auto');
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test('auto mode detects claude and codex binaries', () => {
|
|
986
|
+
expect(setupContent).toContain('command -v claude');
|
|
987
|
+
expect(setupContent).toContain('command -v codex');
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
test('create_agents_sidecar links runtime assets', () => {
|
|
991
|
+
// Sidecar must link bin, browse, review, qa
|
|
992
|
+
const fnStart = setupContent.indexOf('create_agents_sidecar()');
|
|
993
|
+
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('done', fnStart));
|
|
994
|
+
const fnBody = setupContent.slice(fnStart, fnEnd);
|
|
995
|
+
expect(fnBody).toContain('bin');
|
|
996
|
+
expect(fnBody).toContain('browse');
|
|
997
|
+
expect(fnBody).toContain('review');
|
|
998
|
+
expect(fnBody).toContain('qa');
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
describe('telemetry', () => {
|
|
1003
|
+
test('generated SKILL.md contains telemetry start block', () => {
|
|
1004
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
1005
|
+
expect(content).toContain('_TEL_START');
|
|
1006
|
+
expect(content).toContain('_SESSION_ID');
|
|
1007
|
+
expect(content).toContain('TELEMETRY:');
|
|
1008
|
+
expect(content).toContain('TEL_PROMPTED:');
|
|
1009
|
+
expect(content).toContain('gstack-config get telemetry');
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
test('generated SKILL.md contains telemetry opt-in prompt', () => {
|
|
1013
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
1014
|
+
expect(content).toContain('.telemetry-prompted');
|
|
1015
|
+
expect(content).toContain('Help gstack get better');
|
|
1016
|
+
expect(content).toContain('gstack-config set telemetry community');
|
|
1017
|
+
expect(content).toContain('gstack-config set telemetry anonymous');
|
|
1018
|
+
expect(content).toContain('gstack-config set telemetry off');
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test('generated SKILL.md contains telemetry epilogue', () => {
|
|
1022
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
1023
|
+
expect(content).toContain('Telemetry (run last)');
|
|
1024
|
+
expect(content).toContain('gstack-telemetry-log');
|
|
1025
|
+
expect(content).toContain('_TEL_END');
|
|
1026
|
+
expect(content).toContain('_TEL_DUR');
|
|
1027
|
+
expect(content).toContain('SKILL_NAME');
|
|
1028
|
+
expect(content).toContain('OUTCOME');
|
|
1029
|
+
expect(content).toContain('PLAN MODE EXCEPTION');
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test('generated SKILL.md contains pending marker handling', () => {
|
|
1033
|
+
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
|
1034
|
+
expect(content).toContain('.pending');
|
|
1035
|
+
expect(content).toContain('_pending_finalize');
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
test('telemetry blocks appear in all skill files that use PREAMBLE', () => {
|
|
1039
|
+
const skills = ['qa', 'ship', 'review', 'plan-ceo-review', 'plan-eng-review', 'retro'];
|
|
1040
|
+
for (const skill of skills) {
|
|
1041
|
+
const skillPath = path.join(ROOT, skill, 'SKILL.md');
|
|
1042
|
+
if (fs.existsSync(skillPath)) {
|
|
1043
|
+
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
1044
|
+
expect(content).toContain('_TEL_START');
|
|
1045
|
+
expect(content).toContain('Telemetry (run last)');
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
});
|