@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,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`);
|