@marcomigozzi/trailhead 0.15.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 +45 -9
- package/README.md +46 -10
- 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/grilling-holistic-review-docs.test.js +90 -0
- package/bin/lib/codex-projection.js +5 -5
- package/bin/lib/codex-projection.test.js +2 -2
- package/bin/needs-you-docs.test.js +41 -0
- package/bin/pick-stress-test-docs.test.js +105 -0
- package/bin/trailhead.js +3 -3
- package/bin/trailhead.test.js +17 -18
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- 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/checkout-in.test.js +173 -0
- package/plugins/trailhead/hooks/lib/dashboard.test.js +66 -3
- 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/statusline-context.test.js +140 -0
- package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
- package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
- package/plugins/trailhead/hooks/mod/README.md +180 -39
- 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 +150 -25
- package/plugins/trailhead/hooks/mod/map-pane.tsx +526 -92
- package/plugins/trailhead/hooks/mod/register.tsx +298 -39
- 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 +21 -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-labels-guard.js +231 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
- 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 +7 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +3 -3
- package/plugins/trailhead/skills/_shared/substrate-commands.md +26 -4
- package/plugins/trailhead/skills/_shared/substrate.md +4 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +2 -2
- 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/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +4 -4
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +21 -10
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +6 -6
- 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-checkout-in.js +88 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +32 -12
- 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-uat-followup.js +128 -0
- package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
- 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 +1732 -142
- package/plugins/trailhead/tests/map-pane-view.test.ts +308 -14
- 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 +19 -0
- package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
- package/plugins/trailhead/types/index.d.ts +26 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for templates/trailhead-auto-slot.js. Run: node auto-slot.test.js
|
|
3
|
+
// No framework: plain asserts, same style as auto-queue.test.js.
|
|
4
|
+
//
|
|
5
|
+
// Layers covered:
|
|
6
|
+
// 1. main() open / list against REAL temporary git repos (the default git runner).
|
|
7
|
+
// 2. Usage errors (exit 2).
|
|
8
|
+
// 3. End-to-end: the script itself through spawnSync.
|
|
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-auto-slot.js');
|
|
16
|
+
const { main } = require(scriptPath);
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
20
|
+
|
|
21
|
+
const git = (cwd, ...args) => {
|
|
22
|
+
const r = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
|
|
23
|
+
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`);
|
|
24
|
+
return r.stdout.trim();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// A parent dir holding <parent>/repo with two commits. Returns { parent, repo, first, second }.
|
|
28
|
+
function makeRepo() {
|
|
29
|
+
const parent = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'auto-slot-')));
|
|
30
|
+
const repo = path.join(parent, 'repo');
|
|
31
|
+
fs.mkdirSync(repo);
|
|
32
|
+
git(repo, 'init', '-q', '-b', 'main');
|
|
33
|
+
git(repo, 'config', 'user.name', 'T');
|
|
34
|
+
git(repo, 'config', 'user.email', 't@example.com');
|
|
35
|
+
git(repo, 'config', 'commit.gpgsign', 'false');
|
|
36
|
+
fs.writeFileSync(path.join(repo, 'a.txt'), 'one\n');
|
|
37
|
+
git(repo, 'add', 'a.txt');
|
|
38
|
+
git(repo, 'commit', '-q', '-m', 'first');
|
|
39
|
+
const first = git(repo, 'rev-parse', 'HEAD');
|
|
40
|
+
fs.writeFileSync(path.join(repo, 'a.txt'), 'two\n');
|
|
41
|
+
git(repo, 'commit', '-q', '-am', 'second');
|
|
42
|
+
const second = git(repo, 'rev-parse', 'HEAD');
|
|
43
|
+
return { parent, repo, first, second };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function run(argv, deps = {}) {
|
|
47
|
+
let out = '';
|
|
48
|
+
let errOut = '';
|
|
49
|
+
const code = main(argv, { stdout: { write: (s) => { out += s; } }, stderr: { write: (s) => { errOut += s; } }, ...deps });
|
|
50
|
+
let json = null;
|
|
51
|
+
try { json = JSON.parse(out.trim()); } catch { /* not json */ }
|
|
52
|
+
return { code, out, json, errOut };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const open = (repo, n, base, deps) => run(['open', '--repo', repo, '--ticket', String(n), '--base', base], deps);
|
|
56
|
+
const list = (repo, deps) => run(['list', '--repo', repo], deps);
|
|
57
|
+
|
|
58
|
+
// --- open: created -----------------------------------------------------------
|
|
59
|
+
{
|
|
60
|
+
const { parent, repo, first, second } = makeRepo();
|
|
61
|
+
const r = open(repo, 7, first);
|
|
62
|
+
const wt = path.join(parent, 'repo-t7');
|
|
63
|
+
ok('open: created exits 0', r.code === 0);
|
|
64
|
+
ok('open: created status, ticket, branch, path', r.json.status === 'created' && r.json.ticket === 7 && r.json.branch === 'trailhead/t7' && r.json.path === wt);
|
|
65
|
+
ok('open: the worktree directory exists', fs.existsSync(path.join(wt, 'a.txt')));
|
|
66
|
+
ok('open: branch points exactly at --base', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') === first);
|
|
67
|
+
ok('open: branch is not the repo HEAD', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') !== second && git(repo, 'rev-parse', 'HEAD') === second);
|
|
68
|
+
ok('open: worktree checked out on the branch', git(wt, 'rev-parse', '--abbrev-ref', 'HEAD') === 'trailhead/t7');
|
|
69
|
+
|
|
70
|
+
// reentered: same call again, nothing new created
|
|
71
|
+
const again = open(repo, 7, second);
|
|
72
|
+
ok('open: reentered when the branch is already checked out there', again.code === 0 && again.json.status === 'reentered' && again.json.path === wt);
|
|
73
|
+
ok('open: reentered does not move the branch', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') === first);
|
|
74
|
+
|
|
75
|
+
// reattached: worktree removed, branch kept
|
|
76
|
+
git(repo, 'worktree', 'remove', '--force', wt);
|
|
77
|
+
const re = open(repo, 7, second);
|
|
78
|
+
ok('open: reattached when the branch exists without a worktree', re.code === 0 && re.json.status === 'reattached' && re.json.path === wt);
|
|
79
|
+
ok('open: reattached keeps the branch commit', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') === first && git(wt, 'rev-parse', 'HEAD') === first);
|
|
80
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- open: branch checked out at another path -> reentered with that path ---
|
|
84
|
+
{
|
|
85
|
+
const { parent, repo, first } = makeRepo();
|
|
86
|
+
const other = path.join(parent, 'elsewhere');
|
|
87
|
+
git(repo, 'worktree', 'add', '-q', '-b', 'trailhead/t9', other, first);
|
|
88
|
+
const r = open(repo, 9, first);
|
|
89
|
+
ok('open: a hand-made worktree elsewhere is reentered with its path', r.code === 0 && r.json.status === 'reentered' && r.json.path === other);
|
|
90
|
+
ok('open: nothing created at the sibling path', !fs.existsSync(path.join(parent, 'repo-t9')));
|
|
91
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- open: path collision ----------------------------------------------------
|
|
95
|
+
{
|
|
96
|
+
const { parent, repo, first } = makeRepo();
|
|
97
|
+
const target = path.join(parent, 'repo-t3');
|
|
98
|
+
fs.mkdirSync(target);
|
|
99
|
+
fs.writeFileSync(path.join(target, 'keep.txt'), 'mine\n');
|
|
100
|
+
const r = open(repo, 3, first);
|
|
101
|
+
ok('open: existing path is an error', r.code === 1 && r.json.status === 'error' && /exist/.test(r.json.reason));
|
|
102
|
+
ok('open: existing path is never overwritten', fs.readFileSync(path.join(target, 'keep.txt'), 'utf8') === 'mine\n');
|
|
103
|
+
ok('open: no branch created on a path collision', spawnSync('git', ['-C', repo, 'rev-parse', '--verify', '--quiet', 'refs/heads/trailhead/t3']).status !== 0);
|
|
104
|
+
|
|
105
|
+
// reattach into an occupied path is an error too
|
|
106
|
+
git(repo, 'branch', 'trailhead/t4', first);
|
|
107
|
+
fs.mkdirSync(path.join(parent, 'repo-t4'));
|
|
108
|
+
const r4 = open(repo, 4, first);
|
|
109
|
+
ok('open: reattach onto an existing path is an error', r4.code === 1 && r4.json.status === 'error');
|
|
110
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- open: invalid base ------------------------------------------------------
|
|
114
|
+
{
|
|
115
|
+
const { parent, repo } = makeRepo();
|
|
116
|
+
const r = open(repo, 5, 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef');
|
|
117
|
+
ok('open: unresolvable base is an error', r.code === 1 && r.json.status === 'error' && /base/.test(r.json.reason));
|
|
118
|
+
ok('open: nothing created for an invalid base', !fs.existsSync(path.join(parent, 'repo-t5')));
|
|
119
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- open: clone-required on a superrepo -------------------------------------
|
|
123
|
+
{
|
|
124
|
+
const { parent, repo, first } = makeRepo();
|
|
125
|
+
fs.writeFileSync(path.join(repo, '.gitmodules'), '[submodule "lib"]\n\tpath = lib\n\turl = https://example.com/lib.git\n');
|
|
126
|
+
const r = open(repo, 8, first);
|
|
127
|
+
ok('open: a declared submodule needs a clone', r.code === 0 && r.json.status === 'clone-required' && r.json.path === path.join(parent, 'repo-t8') && r.json.branch === 'trailhead/t8');
|
|
128
|
+
ok('open: clone-required creates no worktree', !fs.existsSync(path.join(parent, 'repo-t8')));
|
|
129
|
+
fs.writeFileSync(path.join(repo, '.gitmodules'), '# nothing declared\n');
|
|
130
|
+
const none = open(repo, 8, first);
|
|
131
|
+
ok('open: a .gitmodules without a submodule section does not force a clone', none.json.status === 'created');
|
|
132
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- list --------------------------------------------------------------------
|
|
136
|
+
{
|
|
137
|
+
const { parent, repo, first, second } = makeRepo();
|
|
138
|
+
ok('list: a repo with no slot is empty', list(repo).json.slots.length === 0);
|
|
139
|
+
|
|
140
|
+
open(repo, 12, first);
|
|
141
|
+
open(repo, 4, second);
|
|
142
|
+
// worktree on a non-slot branch is not a slot
|
|
143
|
+
git(repo, 'worktree', 'add', '-q', '-b', 'feature/x', path.join(parent, 'feature-x'), first);
|
|
144
|
+
|
|
145
|
+
// a sibling clone on trailhead/t21
|
|
146
|
+
const clone = path.join(parent, 'repo-t21');
|
|
147
|
+
git(parent, 'clone', '-q', repo, clone);
|
|
148
|
+
git(clone, 'config', 'user.name', 'T');
|
|
149
|
+
git(clone, 'config', 'user.email', 't@example.com');
|
|
150
|
+
git(clone, 'checkout', '-q', '-b', 'trailhead/t21', first);
|
|
151
|
+
// a sibling clone on the wrong branch is ignored
|
|
152
|
+
const wrong = path.join(parent, 'repo-t22');
|
|
153
|
+
git(parent, 'clone', '-q', repo, wrong);
|
|
154
|
+
git(wrong, 'checkout', '-q', '-b', 'trailhead/t99');
|
|
155
|
+
// a sibling dir that is not a git checkout is ignored
|
|
156
|
+
fs.mkdirSync(path.join(parent, 'repo-t30'));
|
|
157
|
+
// a sibling dir inside another checkout (no own git dir) is ignored
|
|
158
|
+
fs.mkdirSync(path.join(repo, 'repo-t31'));
|
|
159
|
+
|
|
160
|
+
const wt12 = path.join(parent, 'repo-t12');
|
|
161
|
+
const wt4 = path.join(parent, 'repo-t4');
|
|
162
|
+
// work state: present on t12, unparseable on t4, absent on the clone
|
|
163
|
+
fs.mkdirSync(path.join(wt12, '.trailhead'), { recursive: true });
|
|
164
|
+
fs.writeFileSync(path.join(wt12, '.trailhead', 'work-state.json'), JSON.stringify({ ticket: 12, auto: true }));
|
|
165
|
+
fs.mkdirSync(path.join(wt4, '.trailhead'), { recursive: true });
|
|
166
|
+
fs.writeFileSync(path.join(wt4, '.trailhead', 'work-state.json'), '{not json');
|
|
167
|
+
// dirty: t12 has an untracked file (.trailhead is untracked too, so both worktrees are dirty); clone is clean
|
|
168
|
+
fs.writeFileSync(path.join(wt12, 'new.txt'), 'x\n');
|
|
169
|
+
|
|
170
|
+
const r = list(repo);
|
|
171
|
+
ok('list: exits 0 with status ok', r.code === 0 && r.json.status === 'ok');
|
|
172
|
+
ok('list: slots sorted by ticket', JSON.stringify(r.json.slots.map((s) => s.ticket)) === JSON.stringify([4, 12, 21]));
|
|
173
|
+
const by = Object.fromEntries(r.json.slots.map((s) => [s.ticket, s]));
|
|
174
|
+
ok('list: worktree kind, branch and path', by[12].kind === 'worktree' && by[12].branch === 'trailhead/t12' && by[12].path === wt12);
|
|
175
|
+
ok('list: sibling clone is listed as clone', by[21].kind === 'clone' && by[21].branch === 'trailhead/t21' && by[21].path === clone);
|
|
176
|
+
ok('list: non-slot worktree, wrong-branch clone, non-git dir and nested dir are ignored', !r.json.slots.some((s) => [99, 30, 31].includes(s.ticket)) && r.json.slots.length === 3);
|
|
177
|
+
ok('list: work state present is parsed', by[12].workState && by[12].workState.ticket === 12 && by[12].workState.auto === true);
|
|
178
|
+
ok('list: work state absent is null without a warning', by[21].workState === null && !r.json.warnings.some((w) => w.includes('repo-t21')));
|
|
179
|
+
ok('list: unparseable work state is null plus a warning', by[4].workState === null && r.json.warnings.length === 1 && r.json.warnings[0].includes('repo-t4'));
|
|
180
|
+
ok('list: dirty detected', by[12].dirty === true);
|
|
181
|
+
ok('list: a clean checkout is not dirty', by[21].dirty === false);
|
|
182
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- W1: stale worktree (directory deleted by hand) --------------------------
|
|
186
|
+
{
|
|
187
|
+
const { parent, repo, first } = makeRepo();
|
|
188
|
+
open(repo, 6, first);
|
|
189
|
+
open(repo, 7, first);
|
|
190
|
+
const wt = path.join(parent, 'repo-t6');
|
|
191
|
+
fs.rmSync(wt, { recursive: true, force: true });
|
|
192
|
+
const r = open(repo, 6, first);
|
|
193
|
+
ok('open: a stale worktree is an error naming it and git worktree prune', r.code === 1 && r.json.status === 'error' && r.json.reason.includes(wt) && /stale/.test(r.json.reason) && r.json.reason.includes('git worktree prune'));
|
|
194
|
+
ok('open: a stale worktree is not pruned automatically', git(repo, 'worktree', 'list', '--porcelain').includes(wt));
|
|
195
|
+
const l = list(repo);
|
|
196
|
+
ok('list: a stale worktree is kept out of slots', l.code === 0 && JSON.stringify(l.json.slots.map((x) => x.ticket)) === JSON.stringify([7]));
|
|
197
|
+
ok('list: a stale worktree is a warning naming it and suggesting prune', l.json.warnings.length === 1 && l.json.warnings[0].includes(wt) && /stale/.test(l.json.warnings[0]) && l.json.warnings[0].includes('git worktree prune'));
|
|
198
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- W2: superrepo with an existing slot path --------------------------------
|
|
202
|
+
{
|
|
203
|
+
const { parent, repo, first } = makeRepo();
|
|
204
|
+
fs.writeFileSync(path.join(repo, '.gitmodules'), '[submodule "lib"]\n\tpath = lib\n\turl = https://example.com/lib.git\n');
|
|
205
|
+
const target = path.join(parent, 'repo-t8');
|
|
206
|
+
const missing = open(repo, 8, first);
|
|
207
|
+
ok('open: missing path on a superrepo is clone-required with kind clone', missing.json.status === 'clone-required' && missing.json.kind === 'clone');
|
|
208
|
+
|
|
209
|
+
fs.mkdirSync(target);
|
|
210
|
+
fs.writeFileSync(path.join(target, 'keep.txt'), 'mine\n');
|
|
211
|
+
const plain = open(repo, 8, first);
|
|
212
|
+
ok('open: a plain existing dir on a superrepo is an error, never clone-required', plain.code === 1 && plain.json.status === 'error');
|
|
213
|
+
ok('open: the existing dir is untouched', fs.readFileSync(path.join(target, 'keep.txt'), 'utf8') === 'mine\n');
|
|
214
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
215
|
+
|
|
216
|
+
git(parent, 'clone', '-q', repo, target);
|
|
217
|
+
git(target, 'checkout', '-q', '-b', 'trailhead/t8', first);
|
|
218
|
+
const clone = open(repo, 8, first);
|
|
219
|
+
ok('open: an existing clone on the slot branch is reentered with kind clone', clone.code === 0 && clone.json.status === 'reentered' && clone.json.kind === 'clone' && clone.json.path === target && clone.json.branch === 'trailhead/t8');
|
|
220
|
+
|
|
221
|
+
git(target, 'checkout', '-q', '-b', 'other');
|
|
222
|
+
const wrong = open(repo, 8, first);
|
|
223
|
+
ok('open: an existing clone on another branch is an error', wrong.code === 1 && wrong.json.status === 'error');
|
|
224
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
{
|
|
227
|
+
const { parent, repo, first } = makeRepo();
|
|
228
|
+
ok('open: worktree statuses carry kind worktree', open(repo, 1, first).json.kind === 'worktree' && open(repo, 1, first).json.kind === 'worktree');
|
|
229
|
+
git(repo, 'worktree', 'remove', '--force', path.join(parent, 'repo-t1'));
|
|
230
|
+
const rea = open(repo, 1, first);
|
|
231
|
+
ok('open: reattached carries kind worktree', rea.json.status === 'reattached' && rea.json.kind === 'worktree');
|
|
232
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- list: a git failure on the main repo ------------------------------------
|
|
236
|
+
{
|
|
237
|
+
const notRepo = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'auto-slot-nr-')));
|
|
238
|
+
const r = list(notRepo);
|
|
239
|
+
ok('list: git failing on the main repo is an error', r.code === 1 && r.json.status === 'error');
|
|
240
|
+
fs.rmSync(notRepo, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --- injectable git runner ---------------------------------------------------
|
|
244
|
+
{
|
|
245
|
+
const calls = [];
|
|
246
|
+
const fake = (args) => { calls.push(args.join(' ')); return { status: 1, stdout: '', stderr: 'boom' }; };
|
|
247
|
+
const r = list('/nowhere/repo', { git: fake });
|
|
248
|
+
ok('seam: injected git runner is used and its failure is an error', r.code === 1 && r.json.status === 'error' && calls.length >= 1 && calls[0].startsWith('-C /nowhere/repo'));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- usage errors ------------------------------------------------------------
|
|
252
|
+
{
|
|
253
|
+
const u = (argv) => run(argv);
|
|
254
|
+
ok('usage: no command exits 2', u([]).code === 2);
|
|
255
|
+
ok('usage: unknown command exits 2', u(['remove', '--repo', '/x']).code === 2);
|
|
256
|
+
ok('usage: open without --ticket exits 2', u(['open', '--repo', '/x', '--base', 'abc']).code === 2);
|
|
257
|
+
ok('usage: open without --base exits 2', u(['open', '--repo', '/x', '--ticket', '3']).code === 2);
|
|
258
|
+
ok('usage: open without --repo exits 2', u(['open', '--ticket', '3', '--base', 'abc']).code === 2);
|
|
259
|
+
ok('usage: bad --ticket exits 2', u(['open', '--repo', '/x', '--ticket', 'x', '--base', 'abc']).code === 2);
|
|
260
|
+
ok('usage: list without --repo exits 2', u(['list']).code === 2);
|
|
261
|
+
ok('usage: unknown flag exits 2', u(['list', '--repo', '/x', '--bogus', '1']).code === 2);
|
|
262
|
+
ok('usage: errors go to stderr, stdout stays empty', u(['open']).errOut.startsWith('usage:') && u(['open']).out === '');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- end-to-end --------------------------------------------------------------
|
|
266
|
+
{
|
|
267
|
+
const { parent, repo, first } = makeRepo();
|
|
268
|
+
const res = spawnSync('node', [scriptPath, 'open', '--repo', repo, '--ticket', '15', '--base', first], { encoding: 'utf8' });
|
|
269
|
+
const j = JSON.parse(res.stdout.trim());
|
|
270
|
+
ok('e2e: open prints one JSON line and exits 0', res.status === 0 && j.status === 'created' && j.path === path.join(parent, 'repo-t15'));
|
|
271
|
+
const ls = spawnSync('node', [scriptPath, 'list', '--repo', repo], { encoding: 'utf8' });
|
|
272
|
+
const lj = JSON.parse(ls.stdout.trim());
|
|
273
|
+
ok('e2e: list finds the new slot', ls.status === 0 && lj.slots.length === 1 && lj.slots[0].ticket === 15);
|
|
274
|
+
ok('e2e: usage error exits 2', spawnSync('node', [scriptPath, '--bogus'], { encoding: 'utf8' }).status === 2);
|
|
275
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(`auto-slot.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for templates/trailhead-checkout-in.js. Run: node checkout-in.test.js
|
|
3
|
+
// No framework: plain asserts, same style as uat-backfill.test.js.
|
|
4
|
+
//
|
|
5
|
+
// Layers covered:
|
|
6
|
+
// 1. The pure token helpers: encode / decode round-trip.
|
|
7
|
+
// 2. The CLI against real temp git repos: invalid, here, move, the `cd` field.
|
|
8
|
+
// 3. The protocol contract: the work skill moves before it loads or preflights.
|
|
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-checkout-in.js');
|
|
16
|
+
const skillPath = path.resolve(__dirname, '..', '..', 'skills', 'trailhead-work', 'SKILL.md');
|
|
17
|
+
const { encodeIn, decodeIn } = require(scriptPath);
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
21
|
+
const eq = (name, a, b) => { assert.deepStrictEqual(a, b, name); passed++; };
|
|
22
|
+
|
|
23
|
+
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'checkout-in-')));
|
|
24
|
+
const sh = (cwd, ...args) => {
|
|
25
|
+
const r = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
|
|
26
|
+
assert.strictEqual(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
|
|
27
|
+
return r.stdout;
|
|
28
|
+
};
|
|
29
|
+
function makeRepo(dir) {
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
sh(dir, 'init', '-q', '-b', 'main');
|
|
32
|
+
sh(dir, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '--allow-empty', '-m', 'init');
|
|
33
|
+
return fs.realpathSync(dir);
|
|
34
|
+
}
|
|
35
|
+
const run = (inArg, cwd) => {
|
|
36
|
+
const r = spawnSync(process.execPath, [scriptPath, '--in', inArg, '--cwd', cwd], { encoding: 'utf8' });
|
|
37
|
+
return { code: r.status, out: r.stdout.trim() ? JSON.parse(r.stdout.trim()) : null, err: r.stderr };
|
|
38
|
+
};
|
|
39
|
+
const tokenOf = (p) => encodeIn(p).slice('--in='.length);
|
|
40
|
+
|
|
41
|
+
// --- 1. pure helpers ---------------------------------------------------------------
|
|
42
|
+
const odd = [
|
|
43
|
+
'/tmp/with space/repo',
|
|
44
|
+
"/tmp/it's/repo",
|
|
45
|
+
'/tmp/"dq"/repo',
|
|
46
|
+
'/tmp/-leading/repo',
|
|
47
|
+
'-dash-first',
|
|
48
|
+
'/tmp/uniè中文/repo',
|
|
49
|
+
"/tmp/a'b'c d/$HOME/`x`",
|
|
50
|
+
];
|
|
51
|
+
for (const p of odd) {
|
|
52
|
+
const enc = encodeIn(p);
|
|
53
|
+
ok(`token shape ${p}`, /^--in=[A-Za-z0-9_-]+$/.test(enc));
|
|
54
|
+
eq(`round-trip ${p}`, decodeIn(enc.slice(5)), p);
|
|
55
|
+
}
|
|
56
|
+
eq('decode of garbage', decodeIn('!!!'), null);
|
|
57
|
+
eq('decode of empty', decodeIn(''), null);
|
|
58
|
+
|
|
59
|
+
// --- 2. CLI ------------------------------------------------------------------------
|
|
60
|
+
const main = makeRepo(path.join(tmp, 'main repo'));
|
|
61
|
+
const other = makeRepo(path.join(tmp, 'other'));
|
|
62
|
+
const plain = path.join(tmp, 'plain');
|
|
63
|
+
fs.mkdirSync(plain);
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
const r = run(tokenOf(main), main);
|
|
67
|
+
eq('here in the root', [r.code, r.out.status, r.out.path], [0, 'here', main]);
|
|
68
|
+
}
|
|
69
|
+
{
|
|
70
|
+
fs.mkdirSync(path.join(main, 'sub'));
|
|
71
|
+
const r = run(tokenOf(main), path.join(main, 'sub'));
|
|
72
|
+
eq('here from a subdir of the same checkout', r.out.status, 'here');
|
|
73
|
+
}
|
|
74
|
+
{
|
|
75
|
+
const r = run(tokenOf(main), other);
|
|
76
|
+
eq('move from another repo', [r.code, r.out.status, r.out.path], [0, 'move', main]);
|
|
77
|
+
ok('move carries a cd', r.out.cd === `cd -- '${main}'`);
|
|
78
|
+
}
|
|
79
|
+
{
|
|
80
|
+
const r = run(tokenOf(main), plain);
|
|
81
|
+
eq('move from outside any repo', r.out.status, 'move');
|
|
82
|
+
}
|
|
83
|
+
{
|
|
84
|
+
const r = run(tokenOf(main), path.join(tmp, 'missing-cwd'));
|
|
85
|
+
eq('move from a cwd that does not exist', r.out.status, 'move');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A worktree root is its own checkout: here only for itself.
|
|
89
|
+
{
|
|
90
|
+
const wt = path.join(tmp, 'wt');
|
|
91
|
+
sh(main, 'worktree', 'add', '-q', '-b', 'wt-branch', wt);
|
|
92
|
+
const wtReal = fs.realpathSync(wt);
|
|
93
|
+
eq('worktree root is here for itself', run(tokenOf(wtReal), wtReal).out.status, 'here');
|
|
94
|
+
eq('worktree root is move from the main checkout', run(tokenOf(wtReal), main).out.status, 'move');
|
|
95
|
+
eq('main checkout is move from the worktree', run(tokenOf(main), wtReal).out.status, 'move');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A symlinked path compares canonical.
|
|
99
|
+
{
|
|
100
|
+
const link = path.join(tmp, 'link-to-main');
|
|
101
|
+
fs.symlinkSync(main, link);
|
|
102
|
+
const r = run(tokenOf(link), main);
|
|
103
|
+
eq('symlinked path is here by canonical compare', r.out.status, 'here');
|
|
104
|
+
eq('symlinked cwd is here too', run(tokenOf(main), link).out.status, 'here');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// invalid: never searches for another checkout.
|
|
108
|
+
{
|
|
109
|
+
eq('relative path is invalid', run(tokenOf('relative/dir'), main).out.status, 'invalid');
|
|
110
|
+
eq('missing path is invalid', run(tokenOf(path.join(tmp, 'nope')), main).out.status, 'invalid');
|
|
111
|
+
eq('non-repo dir is invalid', run(tokenOf(plain), main).out.status, 'invalid');
|
|
112
|
+
eq('subdir of a repo is invalid (not a root)', run(tokenOf(path.join(main, 'sub')), other).out.status, 'invalid');
|
|
113
|
+
const bad = run('!!!', main);
|
|
114
|
+
eq('undecodable token is invalid', [bad.code, bad.out.status], [0, 'invalid']);
|
|
115
|
+
ok('invalid has a reason', typeof bad.out.reason === 'string' && bad.out.reason.length > 0);
|
|
116
|
+
ok('invalid has no cd', !('cd' in run(tokenOf(plain), main).out));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// usage errors
|
|
120
|
+
{
|
|
121
|
+
const r = spawnSync(process.execPath, [scriptPath, '--cwd', main], { encoding: 'utf8' });
|
|
122
|
+
eq('missing --in is a usage error', r.status, 2);
|
|
123
|
+
const r2 = spawnSync(process.execPath, [scriptPath, '--in', tokenOf(main)], { encoding: 'utf8' });
|
|
124
|
+
eq('missing --cwd is a usage error', r2.status, 2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The cd field, run by a shell, lands in the path for every pathological case.
|
|
128
|
+
{
|
|
129
|
+
let n = 0;
|
|
130
|
+
for (const p of odd) {
|
|
131
|
+
if (!path.isAbsolute(p)) continue;
|
|
132
|
+
const dir = path.join(tmp, `odd${n++}`, p.replace(/^\/tmp\//, '').replace(/\//g, '_'));
|
|
133
|
+
const real = makeRepo(dir);
|
|
134
|
+
const r = run(tokenOf(real), plain);
|
|
135
|
+
eq(`move for ${dir}`, r.out.status, 'move');
|
|
136
|
+
const cd = spawnSync('sh', ['-c', `${r.out.cd} && pwd -P`], { encoding: 'utf8', cwd: plain });
|
|
137
|
+
eq(`cd lands in ${dir}`, cd.stdout.trim(), real);
|
|
138
|
+
}
|
|
139
|
+
// A quote and a leading dash in the actual directory names.
|
|
140
|
+
for (const name of ["it's here", '-dash', 'sp ace "dq" $x `y`']) {
|
|
141
|
+
const real = makeRepo(path.join(tmp, 'pathological', name));
|
|
142
|
+
const r = run(tokenOf(real), plain);
|
|
143
|
+
const cd = spawnSync('sh', ['-c', `${r.out.cd} && pwd -P`], { encoding: 'utf8', cwd: plain });
|
|
144
|
+
eq(`cd lands in ${name}`, cd.stdout.trim(), real);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- 3. protocol contract: the move comes before anything else -----------------------
|
|
149
|
+
{
|
|
150
|
+
const skill = fs.readFileSync(skillPath, 'utf8');
|
|
151
|
+
const move = skill.indexOf('## Move into the handed checkout');
|
|
152
|
+
ok('move section exists', move >= 0);
|
|
153
|
+
const load = skill.indexOf('## Load first');
|
|
154
|
+
const pre = skill.indexOf('## Preflight');
|
|
155
|
+
ok('move section sits before Load first', load > move);
|
|
156
|
+
ok('move section sits before the Preflight', pre > move);
|
|
157
|
+
const section = skill.slice(move, load);
|
|
158
|
+
ok('names the helper', section.includes('trailhead-checkout-in.js'));
|
|
159
|
+
ok('names the --in= word', section.includes('--in='));
|
|
160
|
+
ok('names work and resume', /`work`/.test(section) && /`resume`/.test(section));
|
|
161
|
+
ok('runs the cd field verbatim', /`cd` field[^.]*verbatim/.test(section));
|
|
162
|
+
ok('cd is its own Bash call', /own Bash call/.test(section));
|
|
163
|
+
ok('post-move re-check requires here', /re-run the helper/.test(section) && /`here`/.test(section));
|
|
164
|
+
ok('stops on invalid', /`invalid`[^.]*stop/.test(section));
|
|
165
|
+
ok('stops on a failed cd', /failed `cd`[^.]*stop|`cd` fails[^.]*stop|stop[^.]*`cd` fail/i.test(section));
|
|
166
|
+
ok('stops when the post-check is not here', /could not move/.test(section));
|
|
167
|
+
ok('stops when the helper is missing', /missing[^.]*stop|stop[^.]*missing/i.test(section));
|
|
168
|
+
ok('never moves by hand', /by hand/.test(section));
|
|
169
|
+
ok('Routing points at the move section', /Routing[\s\S]*Move into the handed checkout/.test(skill.slice(skill.indexOf('## Routing'))));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
173
|
+
console.log(`checkout-in: ${passed} assertions passed`);
|
|
@@ -131,6 +131,8 @@ function makeTracker(init = {}) {
|
|
|
131
131
|
created: [],
|
|
132
132
|
pins: [],
|
|
133
133
|
failOn: init.failOn || null,
|
|
134
|
+
// extra open issues per singleton label (duplicates): { label: [{ number, title }] }
|
|
135
|
+
dups: init.dups || {},
|
|
134
136
|
};
|
|
135
137
|
const ok_ = (stdout) => ({ status: 0, stdout, stderr: '' });
|
|
136
138
|
t.gh = (args) => {
|
|
@@ -150,9 +152,11 @@ function makeTracker(init = {}) {
|
|
|
150
152
|
}
|
|
151
153
|
if (label === 'trailhead:whiteboard' && state === 'open') return ok_(JSON.stringify(t.whiteboardOpen));
|
|
152
154
|
if (label === 'trailhead:whiteboard' && state === 'closed') return ok_(JSON.stringify(t.closed));
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (label === 'trailhead:
|
|
155
|
+
const lim = Number(args[args.indexOf('--limit') + 1]);
|
|
156
|
+
const extra = (t.dups[label] || []);
|
|
157
|
+
if (label === 'trailhead:codebase') return ok_(JSON.stringify((t.codebase ? [{ number: t.codebase, title: 'Codebase' }] : []).concat(extra).slice(0, lim)));
|
|
158
|
+
if (label === 'trailhead:conventions') return ok_(JSON.stringify((t.conventions ? [{ number: t.conventions, title: 'Conventions' }] : []).concat(extra).slice(0, lim)));
|
|
159
|
+
if (label === 'trailhead:dashboard') return ok_(JSON.stringify((t.dash ? [{ number: t.dash.number, title: '📊 trailhead dashboard' }] : []).concat(extra).slice(0, lim)));
|
|
156
160
|
if (label === undefined) return ok_(JSON.stringify(t.openIssues));
|
|
157
161
|
}
|
|
158
162
|
if (args[0] === 'issue' && args[1] === 'view') {
|
|
@@ -232,6 +236,65 @@ function runMain(argv, t, extra = {}) {
|
|
|
232
236
|
ok('regen: already pinned, no pin call', t.pins.length === 0);
|
|
233
237
|
}
|
|
234
238
|
|
|
239
|
+
// --- singleton lookups fail closed on duplicates (#274) -----------------------
|
|
240
|
+
{
|
|
241
|
+
const noWrites = (t) => t.writes.length === 0 && t.created.length === 0 && t.pins.length === 0 && !t.calls.some((c) => c.startsWith('issue edit') || c.startsWith('issue create') || c.startsWith('label create'));
|
|
242
|
+
// many dashboards
|
|
243
|
+
for (const flags of [[], ['--dry-run']]) {
|
|
244
|
+
const t = makeTracker({ dups: { 'trailhead:dashboard': [{ number: 41, title: 'Dashboard twin\nsecond line' }] } });
|
|
245
|
+
const r = runMain(['--repo', 'o/r', ...flags], t);
|
|
246
|
+
ok(`dups dashboard${flags[0] || ''}: exit 1 and aborted`, r.code === 1 && /^aborted: /.test(r.out));
|
|
247
|
+
ok(`dups dashboard${flags[0] || ''}: names both numbers and titles`, /trailhead:dashboard: .*\(#33\).*\(#41\)/.test(r.out) && r.out.includes('Dashboard twin second line'));
|
|
248
|
+
ok(`dups dashboard${flags[0] || ''}: one line, nothing written`, r.out.split('\n').filter(Boolean).length === 1 && noWrites(t));
|
|
249
|
+
}
|
|
250
|
+
// many codebase / conventions, named together in one abort
|
|
251
|
+
{
|
|
252
|
+
const t = makeTracker({ dups: { 'trailhead:codebase': [{ number: 8, title: 'Codebase 2' }], 'trailhead:conventions': [{ number: 9, title: 'Conv 2' }] } });
|
|
253
|
+
const r = runMain(['--repo', 'o/r'], t);
|
|
254
|
+
ok('dups anchors: exit 1', r.code === 1 && /^aborted: /.test(r.out));
|
|
255
|
+
ok('dups anchors: codebase and conventions both named', /trailhead:codebase: Codebase \(#2\), Codebase 2 \(#8\)/.test(r.out) && /trailhead:conventions: Conv/.test(r.out) && r.out.includes('(#3)') && r.out.includes('(#9)'));
|
|
256
|
+
ok('dups anchors: nothing written', noWrites(t));
|
|
257
|
+
}
|
|
258
|
+
{
|
|
259
|
+
const t = makeTracker({ dups: { 'trailhead:codebase': [{ number: 8, title: 'Codebase 2' }] } });
|
|
260
|
+
const r = runMain(['--repo', 'o/r'], t);
|
|
261
|
+
ok('dups codebase only: aborts, conventions not named', r.code === 1 && /\(#8\)/.test(r.out) && !/trailhead:conventions/.test(r.out) && noWrites(t));
|
|
262
|
+
}
|
|
263
|
+
// many dashboards and no existing dashboard path is untouched: zero dashboard + dup anchors still aborts before create
|
|
264
|
+
{
|
|
265
|
+
const t = makeTracker({ dash: null, dups: { 'trailhead:conventions': [{ number: 9, title: 'Conv 2' }] } });
|
|
266
|
+
const r = runMain(['--repo', 'o/r'], t);
|
|
267
|
+
ok('dups with no dashboard: aborts before create', r.code === 1 && noWrites(t));
|
|
268
|
+
}
|
|
269
|
+
// a window hit by the fetch limit says more may exist (I1)
|
|
270
|
+
{
|
|
271
|
+
const many = Array.from({ length: 11 }, (_, i) => ({ number: 100 + i, title: 'Twin ' + i }));
|
|
272
|
+
const t = makeTracker({ dups: { 'trailhead:dashboard': many } });
|
|
273
|
+
const r = runMain(['--repo', 'o/r'], t);
|
|
274
|
+
ok('dups at the limit: aborts and says possibly more', r.code === 1 && /\(#108\), and possibly more/.test(r.out) && !r.out.includes("(#109)") && noWrites(t));
|
|
275
|
+
const t2 = makeTracker({ dups: { 'trailhead:dashboard': [{ number: 41, title: 'Twin' }] } });
|
|
276
|
+
ok('dups under the limit: no "possibly more"', !/possibly more/.test(runMain(['--repo', 'o/r'], t2).out));
|
|
277
|
+
}
|
|
278
|
+
// control characters in a title never reach the abort line (I2)
|
|
279
|
+
{
|
|
280
|
+
const t = makeTracker({ dups: { 'trailhead:dashboard': [{ number: 41, title: 'Evil\x1b[31m twin\x07\x7f' }] } });
|
|
281
|
+
const r = runMain(['--repo', 'o/r'], t);
|
|
282
|
+
ok('dups: control characters stripped from titles', r.code === 1 && /Evil\\\[31m twin/.test(r.out) && !/[\x00-\x08\x0b-\x1f\x7f]/.test(r.out));
|
|
283
|
+
}
|
|
284
|
+
// zero and one: unchanged
|
|
285
|
+
{
|
|
286
|
+
const t = makeTracker();
|
|
287
|
+
const r = runMain(['--repo', 'o/r'], t);
|
|
288
|
+
ok('one each: writes as before', r.code === 0 && t.writes.length === 1 && /^dashboard #33:/.test(r.out));
|
|
289
|
+
ok('lookups fetch more than one result', t.calls.filter((c) => /--label trailhead:(dashboard|codebase|conventions) /.test(c)).every((c) => !/--limit 1$/.test(c)));
|
|
290
|
+
}
|
|
291
|
+
{
|
|
292
|
+
const t = makeTracker({ dash: null, codebase: 0, conventions: 0 });
|
|
293
|
+
const r = runMain(['--repo', 'o/r'], t);
|
|
294
|
+
ok('zero each: create and pin path unchanged, anchors absent', r.code === 0 && t.created.length === 1 && t.pins.length === 1 && !/Anchors:/.test(t.created[0].text));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
235
298
|
// --- what is (not) gathered --------------------------------------------------
|
|
236
299
|
{
|
|
237
300
|
const t = makeTracker();
|