@marcomigozzi/trailhead 0.14.0 → 0.16.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/README.it.md +158 -34
- package/README.md +147 -33
- package/bin/auto-intro-docs.test.js +72 -0
- package/bin/auto-merge-docs.test.js +33 -0
- package/bin/auto-merge-ticket-docs.test.js +32 -0
- package/bin/auto-review-rounds-docs.test.js +45 -0
- package/bin/auto-run-docs.test.js +60 -0
- package/bin/auto-scope-docs.test.js +33 -0
- package/bin/auto-slots-docs.test.js +45 -0
- package/bin/auto-uat-docs.test.js +47 -0
- package/bin/base-preflight-docs.test.js +65 -0
- package/bin/execute-split-docs.test.js +146 -0
- package/bin/grilling-holistic-review-docs.test.js +90 -0
- package/bin/isolation-answer-docs.test.js +41 -0
- package/bin/isolation-copy-docs.test.js +28 -0
- package/bin/lib/codex-projection.js +6 -6
- package/bin/lib/codex-projection.test.js +43 -2
- package/bin/needs-you-docs.test.js +41 -0
- package/bin/pick-stress-test-docs.test.js +105 -0
- package/bin/report-status-docs.test.js +195 -0
- package/bin/resolve-helpers-docs.test.js +36 -0
- package/bin/scope-confirm-skip-docs.test.js +70 -0
- package/bin/trailhead.js +6 -6
- package/bin/trailhead.test.js +355 -16
- package/bin/uat-setup-docs.test.js +65 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
- package/plugins/trailhead/agents/trailhead-debug.md +20 -1
- package/plugins/trailhead/agents/trailhead-executor.md +29 -2
- package/plugins/trailhead/agents/trailhead-fix.md +29 -2
- package/plugins/trailhead/agents/trailhead-plan.md +20 -1
- package/plugins/trailhead/agents/trailhead-research.md +22 -3
- package/plugins/trailhead/agents/trailhead-verify.md +15 -2
- package/plugins/trailhead/commands/auto.md +2 -2
- package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
- package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
- package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
- package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
- package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
- package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
- package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
- package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
- package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
- package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
- package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
- package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
- package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
- package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
- package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
- package/plugins/trailhead/hooks/mod/README.md +209 -36
- package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
- package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
- package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
- package/plugins/trailhead/hooks/mod/register.tsx +312 -40
- package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
- package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
- package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
- package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
- package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
- package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
- package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
- package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
- package/plugins/trailhead/skills/_shared/choices.md +11 -4
- package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
- package/plugins/trailhead/skills/_shared/load-first.md +3 -3
- package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
- package/plugins/trailhead/skills/_shared/substrate.md +9 -4
- package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +12 -2
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
- package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
- package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
- package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
- package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
- package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
- package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
- package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
- package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
- package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
- package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
- package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
- package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
- package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
- package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
- package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
- package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
- package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
- package/plugins/trailhead/tests/session-registry.test.ts +50 -0
- package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
- package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
- package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
- package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
- package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
- package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
- package/plugins/trailhead/types/index.d.ts +62 -3
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for templates/trailhead-uat-backfill.js. Run: node uat-backfill.test.js
|
|
3
|
+
// No framework: plain asserts, same style as dashboard.test.js.
|
|
4
|
+
//
|
|
5
|
+
// Layers covered:
|
|
6
|
+
// 1. main() against a fake tracker behind an injected `gh` runner: what is
|
|
7
|
+
// tagged, what is skipped, partial failure, the label ensure, the marker.
|
|
8
|
+
// 2. End-to-end: the script itself with a failing fake `gh` on PATH.
|
|
9
|
+
const assert = require('assert');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-uat-backfill.js');
|
|
16
|
+
const { main } = require(scriptPath);
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
20
|
+
|
|
21
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'uat-backfill-'));
|
|
22
|
+
let markerSeq = 0;
|
|
23
|
+
const newMarker = () => path.join(tmp, `m${markerSeq++}`, 'uat-backfill');
|
|
24
|
+
|
|
25
|
+
// --- fake tracker ------------------------------------------------------------
|
|
26
|
+
function makeTracker(init = {}) {
|
|
27
|
+
const t = {
|
|
28
|
+
issues: init.issues || [],
|
|
29
|
+
labelExists: init.labelExists === undefined ? false : init.labelExists,
|
|
30
|
+
createFails: !!init.createFails,
|
|
31
|
+
failEdit: init.failEdit || [],
|
|
32
|
+
failList: !!init.failList,
|
|
33
|
+
calls: [],
|
|
34
|
+
tagged: [],
|
|
35
|
+
};
|
|
36
|
+
const res = (stdout, status = 0) => ({ status, stdout, stderr: status ? 'forced failure' : '' });
|
|
37
|
+
t.gh = (args) => {
|
|
38
|
+
t.calls.push(args.join(' '));
|
|
39
|
+
if (args[0] === 'label' && args[1] === 'create') {
|
|
40
|
+
if (t.createFails) return res('', 1);
|
|
41
|
+
if (t.labelExists) return res('', 1); // already exists: gh exits non-zero
|
|
42
|
+
t.labelExists = true;
|
|
43
|
+
return res('');
|
|
44
|
+
}
|
|
45
|
+
if (args[0] === 'label' && args[1] === 'list') return res(JSON.stringify(t.labelExists ? [{ name: 'trailhead:uat' }] : []));
|
|
46
|
+
if (args[0] === 'issue' && args[1] === 'list') {
|
|
47
|
+
if (t.failList) return res('', 1);
|
|
48
|
+
return res(JSON.stringify(t.issues.map((i) => ({ number: i.number, body: i.body, labels: (i.labels || []).map((name) => ({ name })) }))));
|
|
49
|
+
}
|
|
50
|
+
if (args[0] === 'issue' && args[1] === 'edit') {
|
|
51
|
+
const n = Number(args[2]);
|
|
52
|
+
if (t.failEdit.includes(n)) return res('', 1);
|
|
53
|
+
t.tagged.push(n);
|
|
54
|
+
return res('');
|
|
55
|
+
}
|
|
56
|
+
return res('', 1);
|
|
57
|
+
};
|
|
58
|
+
return t;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function run(t, argv) {
|
|
62
|
+
let out = '';
|
|
63
|
+
const code = main(argv, { gh: t.gh, stdout: { write: (s) => { out += s; } }, stderr: { write: () => {} } });
|
|
64
|
+
let json = null;
|
|
65
|
+
try { json = JSON.parse(out.trim()); } catch { /* not json */ }
|
|
66
|
+
return { code, out, json };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const uat = (n) => ({ number: n, body: `Intro\n\nUAT of: [Parent](https://github.com/o/r/issues/1)\n\n- [ ] step` });
|
|
70
|
+
|
|
71
|
+
// --- tagging -----------------------------------------------------------------
|
|
72
|
+
{
|
|
73
|
+
const t = makeTracker({ issues: [uat(10), { number: 11, body: 'See the UAT of: thing mid-line' }, { number: 12, body: 'x', labels: [] }] });
|
|
74
|
+
const r = run(t, ['--repo', 'o/r']);
|
|
75
|
+
ok('tags a UAT of: line-start match', r.code === 0 && JSON.stringify(t.tagged) === '[10]');
|
|
76
|
+
ok('skips a body that only mentions UAT of: mid-line', !t.tagged.includes(11));
|
|
77
|
+
ok('prints one JSON line', r.json && r.json.status === 'ok' && JSON.stringify(r.json.tagged) === '[10]' && r.json.failed.length === 0 && r.out.trim().split('\n').length === 1);
|
|
78
|
+
ok('ensures the label before listing', t.calls.findIndex((c) => c.startsWith('label create trailhead:uat')) < t.calls.findIndex((c) => c.startsWith('issue list')));
|
|
79
|
+
ok('confirms the label with a label list', t.calls.some((c) => /^label list .*--search trailhead:uat/.test(c)));
|
|
80
|
+
ok('list query excludes the label and caps at 1000', t.calls.some((c) => c.startsWith('issue list') && c.includes('-label:trailhead:uat') && c.includes('--limit 1000') && c.includes('--json number,body,labels')));
|
|
81
|
+
}
|
|
82
|
+
{
|
|
83
|
+
const t = makeTracker({ issues: [{ ...uat(20), labels: ['trailhead:uat'] }, uat(21), uat(22)] });
|
|
84
|
+
const r = run(t, []);
|
|
85
|
+
ok('skips an already-labelled ticket, tags several', JSON.stringify(t.tagged) === '[21,22]' && JSON.stringify(r.json.tagged) === '[21,22]');
|
|
86
|
+
}
|
|
87
|
+
{
|
|
88
|
+
const t = makeTracker({ issues: [uat(30), uat(31), uat(32)], failEdit: [31] });
|
|
89
|
+
const mk = newMarker();
|
|
90
|
+
const r = run(t, ['--marker', mk]);
|
|
91
|
+
ok('a failing edit is reported and does not stop the rest', r.code === 0 && JSON.stringify(r.json.tagged) === '[30,32]' && JSON.stringify(r.json.failed) === '[31]');
|
|
92
|
+
ok('a run with failed edits writes no marker', !fs.existsSync(mk));
|
|
93
|
+
}
|
|
94
|
+
{
|
|
95
|
+
const t = makeTracker({ issues: [uat(40)], failList: true });
|
|
96
|
+
const mk = newMarker();
|
|
97
|
+
const r = run(t, ['--marker', mk]);
|
|
98
|
+
ok('a failing list read exits non-zero with nothing written', r.code === 1 && t.tagged.length === 0 && r.json && r.json.status === 'error' && !fs.existsSync(mk));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- label ensure ------------------------------------------------------------
|
|
102
|
+
{
|
|
103
|
+
const t = makeTracker({ labelExists: true, issues: [uat(50)] });
|
|
104
|
+
const r = run(t, []);
|
|
105
|
+
ok('an existing label (create exits non-zero) is tolerated', r.code === 0 && JSON.stringify(t.tagged) === '[50]');
|
|
106
|
+
}
|
|
107
|
+
{
|
|
108
|
+
const t = makeTracker({ createFails: true, issues: [uat(51)] });
|
|
109
|
+
const mk = newMarker();
|
|
110
|
+
const r = run(t, ['--marker', mk]);
|
|
111
|
+
ok('a create failure with the label absent is an error, nothing tagged, no marker', r.code === 1 && r.json.status === 'error' && t.tagged.length === 0 && !fs.existsSync(mk));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- marker ------------------------------------------------------------------
|
|
115
|
+
{
|
|
116
|
+
const t = makeTracker({ issues: [uat(60)] });
|
|
117
|
+
const mk = newMarker();
|
|
118
|
+
const r = run(t, ['--marker', mk]);
|
|
119
|
+
ok('a clean run writes the marker', r.code === 0 && fs.existsSync(mk));
|
|
120
|
+
const t2 = makeTracker({ issues: [uat(61)] });
|
|
121
|
+
const r2 = run(t2, ['--marker', mk]);
|
|
122
|
+
ok('a present marker exits skipped without calling gh', r2.code === 0 && r2.json.status === 'skipped' && t2.calls.length === 0 && t2.tagged.length === 0);
|
|
123
|
+
}
|
|
124
|
+
{
|
|
125
|
+
const t = makeTracker({ issues: [] });
|
|
126
|
+
const mk = newMarker();
|
|
127
|
+
const r = run(t, ['--marker', mk]);
|
|
128
|
+
ok('a clean run that tags nothing still writes the marker', r.code === 0 && JSON.stringify(r.json.tagged) === '[]' && fs.existsSync(mk));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- usage -------------------------------------------------------------------
|
|
132
|
+
for (const argv of [['--bogus'], ['--repo'], ['--repo', 'nope'], ['--marker']]) {
|
|
133
|
+
const t = makeTracker();
|
|
134
|
+
const r = run(t, argv);
|
|
135
|
+
ok(`usage error exit 2: ${argv.join(' ')}`, r.code === 2 && t.calls.length === 0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- end-to-end with a failing fake gh on PATH --------------------------------
|
|
139
|
+
{
|
|
140
|
+
const binDir = fs.mkdtempSync(path.join(tmp, 'bin-'));
|
|
141
|
+
fs.writeFileSync(path.join(binDir, 'gh'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
|
|
142
|
+
const mk = newMarker();
|
|
143
|
+
const res = spawnSync('node', [scriptPath, '--repo', 'o/r', '--marker', mk], { encoding: 'utf8', env: { ...process.env, PATH: binDir + path.delimiter + process.env.PATH } });
|
|
144
|
+
ok('e2e: gh failing exits 1 with one JSON error line and no marker', res.status === 1 && JSON.parse(res.stdout.trim()).status === 'error' && !fs.existsSync(mk));
|
|
145
|
+
const bad = spawnSync('node', [scriptPath, '--bogus'], { encoding: 'utf8' });
|
|
146
|
+
ok('e2e: usage error exits 2', bad.status === 2);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
150
|
+
console.log(`uat-backfill.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for templates/trailhead-uat-followup.js (#281). Run: node uat-followup.test.js
|
|
3
|
+
// No framework: plain asserts. A fake tracker sits behind an injected `gh` runner;
|
|
4
|
+
// one end-to-end run uses a fake `gh` on PATH.
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-uat-followup.js');
|
|
12
|
+
const { main } = require(scriptPath);
|
|
13
|
+
|
|
14
|
+
let passed = 0;
|
|
15
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
16
|
+
|
|
17
|
+
// Fake tracker behind `gh api graphql`: answers GraphQL-shaped pages, 'pageSize' issues each,
|
|
18
|
+
// following the `after` cursor (a page index as a string).
|
|
19
|
+
function makeTracker(init = {}) {
|
|
20
|
+
const t = { issues: init.issues || [], fail: !!init.fail, raw: init.raw, pageSize: init.pageSize || 100, respond: init.respond, calls: [], argLists: [] };
|
|
21
|
+
t.gh = (args) => {
|
|
22
|
+
t.calls.push(args.join(' '));
|
|
23
|
+
t.argLists.push(args);
|
|
24
|
+
if (t.fail) return { status: 1, stdout: '', stderr: 'forced failure' };
|
|
25
|
+
if (t.raw !== undefined) return { status: 0, stdout: t.raw, stderr: '' };
|
|
26
|
+
const afterIdx = args.findIndex((x) => typeof x === 'string' && x.startsWith('after='));
|
|
27
|
+
const page = afterIdx < 0 ? 0 : Number(args[afterIdx].slice('after='.length));
|
|
28
|
+
if (t.respond) return { status: 0, stdout: JSON.stringify(t.respond(page)), stderr: '' };
|
|
29
|
+
const slice = t.issues.slice(page * t.pageSize, (page + 1) * t.pageSize);
|
|
30
|
+
const more = (page + 1) * t.pageSize < t.issues.length;
|
|
31
|
+
const pageInfo = { hasNextPage: more, endCursor: more ? String(page + 1) : null };
|
|
32
|
+
return { status: 0, stdout: JSON.stringify({ data: { repository: { issues: { nodes: slice, pageInfo } } } }), stderr: '' };
|
|
33
|
+
};
|
|
34
|
+
return t;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function run(t, argv) {
|
|
38
|
+
let out = '';
|
|
39
|
+
let errOut = '';
|
|
40
|
+
const code = main(argv, { gh: t.gh, stdout: { write: (s) => { out += s; } }, stderr: { write: (s) => { errOut += s; } } });
|
|
41
|
+
let json = null;
|
|
42
|
+
try { json = JSON.parse(out.trim()); } catch { /* not json */ }
|
|
43
|
+
return { code, out, errOut, json };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const URL_ = (n, repo = 'o/r') => `https://github.com/${repo}/issues/${n}`;
|
|
47
|
+
const fu = (number, state, body, title = `UAT ${number}`) => ({ number, title, state, body });
|
|
48
|
+
const base = ['find', '--parent', '281', '--repo', 'o/r'];
|
|
49
|
+
|
|
50
|
+
// --- matching ----------------------------------------------------------------
|
|
51
|
+
{
|
|
52
|
+
// A URL whose number runs into letters is not a pointer to that issue.
|
|
53
|
+
const t = makeTracker({ issues: [fu(310, 'OPEN', 'UAT of: [P](https://github.com/o/r/issues/281abc)')] });
|
|
54
|
+
const r = run(t, base);
|
|
55
|
+
ok('URL with trailing letters after the number does not match', r.code === 0 && r.json.open.length === 0 && r.json.reuse === null);
|
|
56
|
+
}
|
|
57
|
+
{
|
|
58
|
+
const t = makeTracker({ issues: [
|
|
59
|
+
fu(300, 'OPEN', `Intro\n\nUAT of: [Parent](${URL_(281)})\n\n- [ ] step`),
|
|
60
|
+
fu(301, 'OPEN', 'UAT of: #281\n- [ ] step'),
|
|
61
|
+
fu(302, 'OPEN', 'See the UAT of: [P](' + URL_(281) + ') mid-line'),
|
|
62
|
+
fu(303, 'OPEN', `UAT of: [P](${URL_(28)})`),
|
|
63
|
+
fu(304, 'OPEN', `UAT of: [P](${URL_(2810)})`),
|
|
64
|
+
fu(305, 'OPEN', 'UAT of: #28'),
|
|
65
|
+
fu(306, 'OPEN', 'UAT of: #2810'),
|
|
66
|
+
fu(307, 'OPEN', `UAT of: [P](${URL_(281, 'other/r')})`),
|
|
67
|
+
fu(308, 'OPEN', 'no pointer at all'),
|
|
68
|
+
fu(309, 'OPEN', `UAT of: [P](${URL_(281, 'O/R')})`),
|
|
69
|
+
fu(310, 'OPEN', `UAT of: [P](${URL_(281)})\r\n- [ ] step`),
|
|
70
|
+
] });
|
|
71
|
+
const r = run(t, base);
|
|
72
|
+
const nums = r.json.open.map((x) => x.number);
|
|
73
|
+
ok('matches by URL, by #n, case-insensitive repo, CRLF', JSON.stringify(nums) === '[300,301,309,310]');
|
|
74
|
+
ok('ignores mid-line, prefix numbers, other repo, no pointer', !nums.some((n) => [302, 303, 304, 305, 306, 307, 308].includes(n)));
|
|
75
|
+
ok('prints one JSON line with title', r.code === 0 && r.json.status === 'ok' && r.json.parent === 281 && r.json.open[0].title === 'UAT 300' && r.out.trim().split('\n').length === 1);
|
|
76
|
+
ok('reuse is the lowest open', r.json.reuse === 300);
|
|
77
|
+
const a0 = t.calls[0];
|
|
78
|
+
ok('one graphql call with the label and both states, never issue list or search', t.calls.length === 1 && a0.includes('api graphql') && a0.includes('labels:["trailhead:uat"]') && a0.includes('states:[OPEN,CLOSED]') && !a0.includes('issue list') && !a0.includes('--search'));
|
|
79
|
+
ok('owner and name go as string fields, no cursor on the first page', a0.includes('-f owner=o') && a0.includes('-f name=r') && !a0.includes('after='));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- open vs closed ------------------------------------------------------------
|
|
83
|
+
{
|
|
84
|
+
const t = makeTracker({ issues: [
|
|
85
|
+
fu(12, 'CLOSED', 'UAT of: #281'),
|
|
86
|
+
fu(11, 'CLOSED', 'UAT of: #281'),
|
|
87
|
+
fu(20, 'OPEN', 'UAT of: #281'),
|
|
88
|
+
fu(15, 'OPEN', 'UAT of: #281'),
|
|
89
|
+
] });
|
|
90
|
+
const r = run(t, base);
|
|
91
|
+
ok('splits open and closed ascending', JSON.stringify(r.json.open.map((x) => x.number)) === '[15,20]' && JSON.stringify(r.json.closed.map((x) => x.number)) === '[11,12]');
|
|
92
|
+
ok('reuse picks the lowest open of several', r.json.reuse === 15);
|
|
93
|
+
}
|
|
94
|
+
{
|
|
95
|
+
const t = makeTracker({ issues: [fu(11, 'CLOSED', 'UAT of: #281')] });
|
|
96
|
+
const r = run(t, base);
|
|
97
|
+
ok('only closed: reported, reuse null', r.code === 0 && r.json.closed.length === 1 && r.json.open.length === 0 && r.json.reuse === null);
|
|
98
|
+
}
|
|
99
|
+
{
|
|
100
|
+
const r = run(makeTracker({ issues: [] }), base);
|
|
101
|
+
ok('none found: empty lists, reuse null', r.code === 0 && r.json.reuse === null && r.json.open.length === 0 && r.json.closed.length === 0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- errors --------------------------------------------------------------------
|
|
105
|
+
{
|
|
106
|
+
const r = run(makeTracker({ fail: true }), base);
|
|
107
|
+
ok('gh failure is a fail-closed error', r.code === 1 && r.json.status === 'error' && typeof r.json.reason === 'string');
|
|
108
|
+
const b = run(makeTracker({ raw: 'not json' }), base);
|
|
109
|
+
ok('bad JSON is an error', b.code === 1 && b.json.status === 'error');
|
|
110
|
+
const o = run(makeTracker({ raw: '{"a":1}' }), base);
|
|
111
|
+
ok('JSON without data.repository is an error', o.code === 1 && o.json.status === 'error');
|
|
112
|
+
const er = run(makeTracker({ raw: JSON.stringify({ errors: [{ message: 'boom' }], data: null }) }), base);
|
|
113
|
+
ok('errors array is an error', er.code === 1 && er.json.status === 'error' && /boom/.test(er.json.reason));
|
|
114
|
+
const nr = run(makeTracker({ raw: JSON.stringify({ data: { repository: null } }) }), base);
|
|
115
|
+
ok('null repository is an error', nr.code === 1 && nr.json.status === 'error');
|
|
116
|
+
const nn = run(makeTracker({ raw: JSON.stringify({ data: { repository: { issues: { pageInfo: { hasNextPage: false, endCursor: null } } } } }) }), base);
|
|
117
|
+
ok('missing nodes is an error', nn.code === 1 && nn.json.status === 'error');
|
|
118
|
+
const np = run(makeTracker({ raw: JSON.stringify({ data: { repository: { issues: { nodes: [] } } } }) }), base);
|
|
119
|
+
ok('missing pageInfo is an error', np.code === 1 && np.json.status === 'error');
|
|
120
|
+
const nc = run(makeTracker({ raw: JSON.stringify({ data: { repository: { issues: { nodes: [], pageInfo: { hasNextPage: true, endCursor: null } } } } }) }), base);
|
|
121
|
+
ok('hasNextPage without a cursor is an error', nc.code === 1 && nc.json.status === 'error');
|
|
122
|
+
const endless = makeTracker({ respond: (p) => ({ data: { repository: { issues: { nodes: [], pageInfo: { hasNextPage: true, endCursor: String(p + 1) } } } } }) });
|
|
123
|
+
const cap = run(endless, base);
|
|
124
|
+
ok('page cap exceeded is an error, never a partial answer', cap.code === 1 && cap.json.status === 'error' && /page/.test(cap.json.reason) && endless.calls.length === 100);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- pagination ----------------------------------------------------------------
|
|
128
|
+
{
|
|
129
|
+
const t = makeTracker({ pageSize: 2, issues: [
|
|
130
|
+
fu(1, 'OPEN', 'x'), fu(2, 'CLOSED', 'x'), fu(3, 'OPEN', 'x'),
|
|
131
|
+
fu(4, 'OPEN', 'UAT of: #281'), fu(5, 'CLOSED', 'UAT of: #281'),
|
|
132
|
+
] });
|
|
133
|
+
const r = run(t, base);
|
|
134
|
+
ok('follow-up on a later page is found', r.code === 0 && r.json.reuse === 4 && r.json.closed[0].number === 5);
|
|
135
|
+
ok('three pages, cursor passed on later ones only', t.calls.length === 3 && !t.calls[0].includes('after=') && t.calls[1].includes('-f after=1') && t.calls[2].includes('-f after=2'));
|
|
136
|
+
ok('no page uses issue list or search', t.calls.every((c) => c.includes('api graphql') && !c.includes('issue list') && !c.includes('--search')));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- usage ---------------------------------------------------------------------
|
|
140
|
+
for (const argv of [
|
|
141
|
+
[], ['bogus'], ['find'], ['find', '--parent', '281'], ['find', '--repo', 'o/r'],
|
|
142
|
+
['find', '--parent', '0', '--repo', 'o/r'], ['find', '--parent', 'abc', '--repo', 'o/r'], ['find', '--parent', '-3', '--repo', 'o/r'],
|
|
143
|
+
['find', '--parent', '281', '--repo', 'nope'], ['find', '--parent'], ['find', '--parent', '281', '--repo'],
|
|
144
|
+
['find', '--parent', '281', '--repo', 'o/r', '--bogus', 'x'],
|
|
145
|
+
]) {
|
|
146
|
+
const t = makeTracker();
|
|
147
|
+
const r = run(t, argv);
|
|
148
|
+
ok(`usage error exit 2: ${argv.join(' ') || '(none)'}`, r.code === 2 && t.calls.length === 0 && r.errOut.length > 0 && r.out === '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- end-to-end with a fake gh on PATH ------------------------------------------
|
|
152
|
+
{
|
|
153
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'uat-followup-'));
|
|
154
|
+
const binDir = path.join(tmp, 'bin');
|
|
155
|
+
fs.mkdirSync(binDir);
|
|
156
|
+
const payload = JSON.stringify({ data: { repository: { issues: { nodes: [fu(7, 'OPEN', 'UAT of: #281', 'Follow-up')], pageInfo: { hasNextPage: false, endCursor: null } } } } });
|
|
157
|
+
fs.writeFileSync(path.join(binDir, 'gh'), `#!/bin/sh\ncat <<'EOF_JSON'\n${payload}\nEOF_JSON\n`, { mode: 0o755 });
|
|
158
|
+
const env = { ...process.env, PATH: binDir + path.delimiter + process.env.PATH };
|
|
159
|
+
const res = spawnSync('node', [scriptPath, ...base], { encoding: 'utf8', env });
|
|
160
|
+
ok('e2e: finds the follow-up through gh on PATH', res.status === 0 && JSON.parse(res.stdout.trim()).reuse === 7);
|
|
161
|
+
fs.writeFileSync(path.join(binDir, 'gh'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
|
|
162
|
+
const bad = spawnSync('node', [scriptPath, ...base], { encoding: 'utf8', env });
|
|
163
|
+
ok('e2e: failing gh exits 1 with a JSON error line', bad.status === 1 && JSON.parse(bad.stdout.trim()).status === 'error');
|
|
164
|
+
const use = spawnSync('node', [scriptPath, '--bogus'], { encoding: 'utf8' });
|
|
165
|
+
ok('e2e: usage error exits 2', use.status === 2);
|
|
166
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(`uat-followup.test.js: ${passed} checks passed`);
|