@nickmeriano/task 0.8.0 → 0.10.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.md +137 -22
- package/dist/asks.test.d.ts +17 -0
- package/dist/asks.test.d.ts.map +1 -0
- package/dist/asks.test.js +278 -0
- package/dist/asks.test.js.map +1 -0
- package/dist/check.d.ts +3 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -60
- package/dist/check.js.map +1 -1
- package/dist/check.test.js +99 -60
- package/dist/check.test.js.map +1 -1
- package/dist/claim-io.d.ts +73 -0
- package/dist/claim-io.d.ts.map +1 -0
- package/dist/claim-io.js +344 -0
- package/dist/claim-io.js.map +1 -0
- package/dist/claim.d.ts +108 -23
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +320 -86
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.d.ts +10 -2
- package/dist/claim.test.d.ts.map +1 -1
- package/dist/claim.test.js +282 -96
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +346 -68
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +70 -24
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +319 -141
- package/dist/file-store.js.map +1 -1
- package/dist/git-serve.d.ts +183 -0
- package/dist/git-serve.d.ts.map +1 -0
- package/dist/git-serve.js +503 -0
- package/dist/git-serve.js.map +1 -0
- package/dist/git-serve.test.d.ts +16 -0
- package/dist/git-serve.test.d.ts.map +1 -0
- package/dist/git-serve.test.js +183 -0
- package/dist/git-serve.test.js.map +1 -0
- package/dist/git.d.ts +65 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +114 -0
- package/dist/git.js.map +1 -0
- package/dist/id.d.ts +39 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +67 -0
- package/dist/id.js.map +1 -0
- package/dist/inbox.d.ts +41 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +56 -0
- package/dist/inbox.js.map +1 -0
- package/dist/overview.d.ts +9 -0
- package/dist/overview.d.ts.map +1 -1
- package/dist/overview.js +19 -11
- package/dist/overview.js.map +1 -1
- package/dist/overview.test.js +6 -5
- package/dist/overview.test.js.map +1 -1
- package/dist/promote.test.d.ts +3 -2
- package/dist/promote.test.d.ts.map +1 -1
- package/dist/promote.test.js +29 -31
- package/dist/promote.test.js.map +1 -1
- package/dist/publish.d.ts +2 -17
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +4 -49
- package/dist/publish.js.map +1 -1
- package/dist/search.d.ts +3 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +15 -2
- package/dist/search.js.map +1 -1
- package/dist/search.test.js +9 -9
- package/dist/search.test.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -21
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +28 -14
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/store.test.js +88 -75
- package/dist/store.test.js.map +1 -1
- package/dist/ticket-doc.d.ts +26 -10
- package/dist/ticket-doc.d.ts.map +1 -1
- package/dist/ticket-doc.js +71 -27
- package/dist/ticket-doc.js.map +1 -1
- package/dist/types.d.ts +51 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +96 -24
- package/src/asks.test.ts +355 -0
- package/src/check.test.ts +129 -72
- package/src/check.ts +101 -58
- package/src/claim-io.ts +401 -0
- package/src/claim.test.ts +372 -101
- package/src/claim.ts +418 -99
- package/src/cli.ts +370 -67
- package/src/file-store.ts +357 -145
- package/src/git-serve.test.ts +240 -0
- package/src/git-serve.ts +595 -0
- package/src/git.ts +141 -0
- package/src/id.ts +68 -0
- package/src/inbox.ts +77 -0
- package/src/overview.test.ts +6 -5
- package/src/overview.ts +25 -10
- package/src/promote.test.ts +40 -28
- package/src/publish.ts +6 -53
- package/src/search.test.ts +11 -11
- package/src/search.ts +17 -3
- package/src/server.ts +221 -19
- package/src/store.test.ts +95 -77
- package/src/store.ts +28 -13
- package/src/ticket-doc.ts +93 -30
- package/src/types.ts +52 -17
- package/ui/dist/assets/index-BjsorZOU.js +229 -0
- package/ui/dist/assets/index-CoKCUYic.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BJmOsOdR.js +0 -229
- package/ui/dist/assets/index-BoqQlqSU.css +0 -1
package/dist/claim.test.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim.test.d.ts","sourceRoot":"","sources":["../src/claim.test.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"claim.test.d.ts","sourceRoot":"","sources":["../src/claim.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG"}
|
package/dist/claim.test.js
CHANGED
|
@@ -6,15 +6,23 @@
|
|
|
6
6
|
* 1. Two concurrent `task claim <same-id>`: exactly one exits 0, the other
|
|
7
7
|
* exits 1 — git's atomic ref creation is the lock.
|
|
8
8
|
* 2. Validation failures exit 2 before anything touches git.
|
|
9
|
-
* 3. `task list --claimable` is the queue: position order, minus blocked
|
|
10
|
-
*
|
|
9
|
+
* 3. `task list --claimable` is the queue: position order, minus blocked
|
|
10
|
+
* and already-claimed tickets — open asks never exclude a ticket.
|
|
11
11
|
* 4. `task claim --release` deletes the branch both sides and the ticket is
|
|
12
12
|
* claimable again.
|
|
13
|
+
* 5. `task claim --lock-only` takes the same lock without touching the
|
|
14
|
+
* checkout — a branch-pinned (even dirty) session can hold it — and
|
|
15
|
+
* `--release` works on it symmetrically.
|
|
16
|
+
* 6. Deletions are verified: against a remote that silently drops deletion
|
|
17
|
+
* pushes (TAS-8b7s9), release and sweep fail loudly instead of lying.
|
|
18
|
+
* 7. `task sweep` deletes claim branches whose tickets are done, canceled or
|
|
19
|
+
* missing on the current checkout, and leaves live ones — and any branch
|
|
20
|
+
* named outside the claim scheme — alone.
|
|
13
21
|
*/
|
|
14
22
|
import assert from "node:assert/strict";
|
|
15
23
|
import { test } from "node:test";
|
|
16
24
|
import { execFile, spawnSync } from "node:child_process";
|
|
17
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
18
26
|
import { readFileSync } from "node:fs";
|
|
19
27
|
import { tmpdir } from "node:os";
|
|
20
28
|
import { join } from "node:path";
|
|
@@ -35,24 +43,44 @@ function sh(cwd, command, ...args) {
|
|
|
35
43
|
}
|
|
36
44
|
/** The CLI as callers see it — a subprocess with an exit code. */
|
|
37
45
|
function cli(cwd, ...args) {
|
|
46
|
+
return cliEnv(cwd, {}, ...args);
|
|
47
|
+
}
|
|
48
|
+
/** Like cli(), with extra env — the delete-dropping git shim rides in PATH. */
|
|
49
|
+
function cliEnv(cwd, env, ...args) {
|
|
38
50
|
return new Promise((resolve) => {
|
|
39
|
-
execFile(process.execPath, ["--experimental-strip-types", CLI, ...args], { cwd }, (error, stdout, stderr) => {
|
|
51
|
+
execFile(process.execPath, ["--experimental-strip-types", CLI, ...args], { cwd, env: { ...process.env, ...env } }, (error, stdout, stderr) => {
|
|
40
52
|
resolve({ code: error ? (error.code ?? 1) : 0, stdout, stderr });
|
|
41
53
|
});
|
|
42
54
|
});
|
|
43
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A PATH whose `git` silently swallows branch-deletion pushes — exit 0,
|
|
58
|
+
* nothing deleted — and delegates everything else to the real git. This is
|
|
59
|
+
* the observed cloud-proxy behavior TAS-8b7s9 documents: the deletion "works"
|
|
60
|
+
* while the ref survives, which is exactly what verified release must catch.
|
|
61
|
+
* POSIX-only by construction, like the failure it reproduces.
|
|
62
|
+
*/
|
|
63
|
+
function deleteDroppingPath() {
|
|
64
|
+
const dir = mkdtempSync(join(tmpdir(), "task-fake-git-"));
|
|
65
|
+
process.on("exit", () => rmSync(dir, { recursive: true, force: true }));
|
|
66
|
+
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
|
|
67
|
+
const shim = join(dir, "git");
|
|
68
|
+
writeFileSync(shim, `#!/bin/sh\nfor a in "$@"; do case "$a" in --delete) exit 0;; esac; done\nexec "${real}" "$@"\n`);
|
|
69
|
+
chmodSync(shim, 0o755);
|
|
70
|
+
return `${dir}:${process.env.PATH ?? ""}`;
|
|
71
|
+
}
|
|
44
72
|
/**
|
|
45
73
|
* A bare origin holding a board with the claimable shapes:
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
74
|
+
* one "Ready one" todo
|
|
75
|
+
* two "Ready two" todo
|
|
76
|
+
* blocked "Blocked" todo, blocked by `one`
|
|
77
|
+
* person "For a person" todo, carrying an open ask
|
|
78
|
+
* parked "Still parked" backlog
|
|
79
|
+
* jumper "Jumped queue" todo, moved to the top of the column after
|
|
80
|
+
* creation — claimable order must be [jumper, one, two], proving
|
|
81
|
+
* position order beats creation order.
|
|
54
82
|
*/
|
|
55
|
-
function fixture(
|
|
83
|
+
function fixture(branchPrefix) {
|
|
56
84
|
const home = tempDir();
|
|
57
85
|
const bare = join(home, "origin.git");
|
|
58
86
|
sh(home, "git", "init", "--quiet", "--bare", "-b", "main", bare);
|
|
@@ -61,20 +89,29 @@ function fixture(claimPrefix) {
|
|
|
61
89
|
sh(seed, "git", "config", "user.name", "Test");
|
|
62
90
|
sh(seed, "git", "config", "user.email", "test@example.com");
|
|
63
91
|
const store = initProject(seed, { name: "claim board", prefix: "CLM" });
|
|
64
|
-
if (
|
|
92
|
+
if (branchPrefix !== undefined) {
|
|
65
93
|
const configPath = join(seed, ".task", "config.json");
|
|
66
94
|
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
67
|
-
config.
|
|
95
|
+
config.claims = { branchPrefix };
|
|
68
96
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
69
97
|
}
|
|
70
|
-
store.create({ title: "Ready one", status: "todo" });
|
|
71
|
-
store.create({ title: "Ready two", status: "todo" });
|
|
72
|
-
store.create({ title: "Blocked", status: "todo", blockedBy: [
|
|
73
|
-
store.create({ title: "For a person", status: "todo"
|
|
74
|
-
store.
|
|
98
|
+
const one = store.create({ title: "Ready one", status: "todo" });
|
|
99
|
+
const two = store.create({ title: "Ready two", status: "todo" });
|
|
100
|
+
const blocked = store.create({ title: "Blocked", status: "todo", blockedBy: [one.key] });
|
|
101
|
+
const person = store.create({ title: "For a person", status: "todo" });
|
|
102
|
+
store.addAsk(person.key, "Approve the vendor contract", "claude");
|
|
103
|
+
const parked = store.create({ title: "Still parked", status: "backlog" });
|
|
75
104
|
const jumper = store.create({ title: "Jumped queue", status: "backlog" });
|
|
76
105
|
// A status move without an explicit slot lands on top of the new column.
|
|
77
|
-
store.update(jumper.
|
|
106
|
+
store.update(jumper.key, { status: "todo" });
|
|
107
|
+
const keys = {
|
|
108
|
+
one: one.key,
|
|
109
|
+
two: two.key,
|
|
110
|
+
blocked: blocked.key,
|
|
111
|
+
person: person.key,
|
|
112
|
+
parked: parked.key,
|
|
113
|
+
jumper: jumper.key,
|
|
114
|
+
};
|
|
78
115
|
sh(seed, "git", "add", "-A");
|
|
79
116
|
sh(seed, "git", "commit", "--quiet", "-m", "board");
|
|
80
117
|
sh(seed, "git", "push", "--quiet", "-u", "origin", "main");
|
|
@@ -86,113 +123,120 @@ function fixture(claimPrefix) {
|
|
|
86
123
|
sh(dir, "git", "config", "user.email", "test@example.com");
|
|
87
124
|
return dir;
|
|
88
125
|
};
|
|
89
|
-
return { bare, clone };
|
|
126
|
+
return { bare, clone, keys };
|
|
90
127
|
}
|
|
91
128
|
test("two concurrent claims of one ticket: exactly one wins", async () => {
|
|
92
|
-
const { bare, clone } = fixture();
|
|
129
|
+
const { bare, clone, keys } = fixture();
|
|
93
130
|
const a = clone();
|
|
94
131
|
const b = clone();
|
|
95
|
-
const [ra, rb] = await Promise.all([cli(a, "claim",
|
|
132
|
+
const [ra, rb] = await Promise.all([cli(a, "claim", keys.one), cli(b, "claim", keys.one)]);
|
|
96
133
|
assert.deepEqual([ra.code, rb.code].sort(), [0, 1], `a: ${ra.stderr}\nb: ${rb.stderr}`);
|
|
97
134
|
const winner = ra.code === 0 ? a : b;
|
|
98
135
|
const loser = ra.code === 0 ? rb : ra;
|
|
99
136
|
assert.match(loser.stderr, /already claimed|rejected/);
|
|
100
137
|
// The winner is left on the claim branch, flip committed and pushed.
|
|
101
|
-
|
|
102
|
-
assert.
|
|
103
|
-
assert.match(
|
|
138
|
+
const branch = `task/claim/clm-${keys.one}`;
|
|
139
|
+
assert.equal(sh(winner, "git", "rev-parse", "--abbrev-ref", "HEAD"), branch);
|
|
140
|
+
assert.match(readFileSync(join(winner, ".task", "tickets", keys.one, "ticket.md"), "utf8"), /status: in_progress/);
|
|
141
|
+
assert.ok(sh(winner, "git", "ls-remote", "--heads", "origin", branch).includes(`refs/heads/${branch}`));
|
|
104
142
|
// Origin's default branch never saw the flip — the claim rides the branch.
|
|
105
143
|
assert.equal(sh(winner, "git", "diff", "--stat", "origin/main..HEAD", "--", "README.md"), "");
|
|
106
|
-
assert.match(sh(winner, "git", "show",
|
|
144
|
+
assert.match(sh(winner, "git", "show", `origin/main:.task/tickets/${keys.one}/ticket.md`), /status: todo/);
|
|
107
145
|
void bare;
|
|
108
146
|
});
|
|
109
147
|
test("unclaimable tickets exit 2, before anything touches origin", async () => {
|
|
110
|
-
const { clone } = fixture();
|
|
148
|
+
const { clone, keys } = fixture();
|
|
111
149
|
const dir = clone();
|
|
112
|
-
assert.equal((await cli(dir, "claim",
|
|
113
|
-
assert.equal((await cli(dir, "claim",
|
|
114
|
-
assert.equal((await cli(dir, "claim", "
|
|
115
|
-
assert.
|
|
116
|
-
assert.match((await cli(dir, "claim", "3")).stderr, /blocked by CLM-1/);
|
|
150
|
+
assert.equal((await cli(dir, "claim", keys.parked)).code, 2); // backlog
|
|
151
|
+
assert.equal((await cli(dir, "claim", keys.blocked)).code, 2); // blocked
|
|
152
|
+
assert.equal((await cli(dir, "claim", "zz9zz")).code, 2); // missing
|
|
153
|
+
assert.match((await cli(dir, "claim", keys.blocked)).stderr, new RegExp(`blocked by CLM-${keys.one}`));
|
|
117
154
|
// A dirty tree refuses too — claiming switches branches. Only *tracked*
|
|
118
155
|
// modifications count: untracked files ride a branch switch untouched
|
|
119
156
|
// (release deliberately leaves one — the reason comment).
|
|
120
157
|
const gitignore = join(dir, ".task", ".gitignore");
|
|
121
158
|
writeFileSync(gitignore, `${readFileSync(gitignore, "utf8")}\nwip\n`);
|
|
122
|
-
const dirty = await cli(dir, "claim",
|
|
159
|
+
const dirty = await cli(dir, "claim", keys.two);
|
|
123
160
|
assert.equal(dirty.code, 2);
|
|
124
161
|
assert.match(dirty.stderr, /working tree is dirty/);
|
|
125
162
|
sh(dir, "git", "checkout", "--", ".task/.gitignore");
|
|
126
163
|
writeFileSync(join(dir, "scratch.txt"), "untracked is fine");
|
|
127
|
-
assert.equal((await cli(dir, "claim",
|
|
164
|
+
assert.equal((await cli(dir, "claim", keys.two)).code, 0);
|
|
128
165
|
// Nothing above created a stray claim branch for the failed ids.
|
|
129
166
|
const remote = sh(dir, "git", "ls-remote", "--heads", "origin", "task/claim/*");
|
|
130
|
-
assert.deepEqual(remote.split("\n").map((l) => l.split("refs/heads/")[1]), [
|
|
167
|
+
assert.deepEqual(remote.split("\n").map((l) => l.split("refs/heads/")[1]), [`task/claim/clm-${keys.two}`]);
|
|
131
168
|
});
|
|
132
169
|
test("list --claimable is the queue: position order, minus everything unclaimable", async () => {
|
|
133
|
-
const { clone } = fixture();
|
|
170
|
+
const { clone, keys } = fixture();
|
|
134
171
|
const a = clone();
|
|
135
|
-
assert.equal((await cli(a, "claim",
|
|
172
|
+
assert.equal((await cli(a, "claim", keys.jumper)).code, 0);
|
|
136
173
|
const b = clone();
|
|
137
174
|
const listed = await cli(b, "list", "--claimable", "--json");
|
|
138
175
|
assert.equal(listed.code, 0);
|
|
139
176
|
const { tasks } = JSON.parse(listed.stdout);
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
177
|
+
// jumper claimed, blocked blocked, parked not todo — position order keeps
|
|
178
|
+
// one before two (creation order within the column). person carries an open
|
|
179
|
+
// ask and is claimable anyway: asks gate done, not claim.
|
|
180
|
+
assert.deepEqual(tasks.map((t) => t.key), [keys.one, keys.two, keys.person]);
|
|
143
181
|
assert.equal((await cli(b, "list", "--claimable", "--status", "todo")).code, 1);
|
|
144
182
|
});
|
|
145
183
|
test("release deletes the branch everywhere and reopens the claim", async () => {
|
|
146
|
-
const { clone } = fixture();
|
|
184
|
+
const { clone, keys } = fixture();
|
|
147
185
|
const dir = clone();
|
|
148
|
-
assert.equal((await cli(dir, "claim",
|
|
149
|
-
assert.equal((await cli(dir, "claim",
|
|
150
|
-
const released = await cli(dir, "claim", "--release",
|
|
186
|
+
assert.equal((await cli(dir, "claim", keys.two)).code, 0);
|
|
187
|
+
assert.equal((await cli(dir, "claim", keys.two)).code, 1); // locally claimed too
|
|
188
|
+
const released = await cli(dir, "claim", "--release", keys.two, "--comment", "trying another shape");
|
|
151
189
|
assert.equal(released.code, 0, released.stderr);
|
|
152
|
-
assert.
|
|
190
|
+
assert.ok(released.stdout.includes(`released CLM-${keys.two}`), released.stdout);
|
|
153
191
|
// Back on the default branch, branch gone on both sides, ticket todo again.
|
|
154
192
|
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), "main");
|
|
155
|
-
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin",
|
|
156
|
-
assert.match(readFileSync(join(dir, ".task", "tickets",
|
|
157
|
-
assert.equal((await cli(dir, "claim",
|
|
193
|
+
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.two}`), "");
|
|
194
|
+
assert.match(readFileSync(join(dir, ".task", "tickets", keys.two, "ticket.md"), "utf8"), /status: todo/);
|
|
195
|
+
assert.equal((await cli(dir, "claim", keys.two)).code, 0);
|
|
158
196
|
// Releasing an unclaimed ticket is a quiet no-op, not an error.
|
|
159
|
-
const noop = await cli(dir, "claim", "--release",
|
|
197
|
+
const noop = await cli(dir, "claim", "--release", keys.one, "--comment", "noop");
|
|
160
198
|
assert.equal(noop.code, 0);
|
|
161
199
|
assert.match(noop.stdout, /wasn't claimed/);
|
|
162
200
|
});
|
|
163
|
-
test("a configured
|
|
164
|
-
const { clone } = fixture("claude/task/");
|
|
201
|
+
test("a configured claims.branchPrefix moves the whole namespace", async () => {
|
|
202
|
+
const { clone, keys } = fixture("claude/task/");
|
|
165
203
|
const dir = clone();
|
|
166
|
-
assert.equal((await cli(dir, "claim",
|
|
167
|
-
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"),
|
|
168
|
-
assert.
|
|
204
|
+
assert.equal((await cli(dir, "claim", keys.one)).code, 0);
|
|
205
|
+
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), `claude/task/clm-${keys.one}`);
|
|
206
|
+
assert.ok(sh(dir, "git", "ls-remote", "--heads", "origin", "claude/task/*").includes(`refs/heads/claude/task/clm-${keys.one}`));
|
|
169
207
|
// Every side of the feature reads the same key: the queue looks for claims
|
|
170
208
|
// under the configured namespace, and release deletes there too.
|
|
171
209
|
const other = clone();
|
|
172
210
|
const listed = await cli(other, "list", "--claimable", "--json");
|
|
173
211
|
const { tasks } = JSON.parse(listed.stdout);
|
|
174
|
-
assert.deepEqual(tasks.map((t) => t.
|
|
175
|
-
assert.equal((await cli(other, "claim",
|
|
176
|
-
assert.equal((await cli(dir, "claim", "--release",
|
|
212
|
+
assert.deepEqual(tasks.map((t) => t.key), [keys.jumper, keys.two, keys.person]);
|
|
213
|
+
assert.equal((await cli(other, "claim", keys.one)).code, 1);
|
|
214
|
+
assert.equal((await cli(dir, "claim", "--release", keys.one, "--comment", "handing it back")).code, 0);
|
|
177
215
|
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", "claude/task/*"), "");
|
|
178
216
|
// A missing trailing slash is forgiven; a malformed value fails loudly.
|
|
179
217
|
const bad = clone();
|
|
180
218
|
const configPath = join(bad, ".task", "config.json");
|
|
181
219
|
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
182
|
-
config.
|
|
220
|
+
config.claims = { branchPrefix: "spaces are not refs/" };
|
|
183
221
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
184
222
|
sh(bad, "git", "commit", "--quiet", "-am", "bad prefix");
|
|
185
|
-
const invalid = await cli(bad, "claim",
|
|
223
|
+
const invalid = await cli(bad, "claim", keys.two);
|
|
186
224
|
assert.equal(invalid.code, 2);
|
|
187
225
|
assert.match(invalid.stderr, /invalid claims\.branchPrefix/);
|
|
226
|
+
// The retired pre-0.8 `claimPrefix` spelling is not honored (TAS-41): a
|
|
227
|
+
// board carrying only it refuses by name rather than silently defaulting
|
|
228
|
+
// the lock convention out from under its workers.
|
|
229
|
+
const retired = clone();
|
|
230
|
+
const retiredPath = join(retired, ".task", "config.json");
|
|
231
|
+
const retiredConfig = JSON.parse(readFileSync(retiredPath, "utf8"));
|
|
232
|
+
delete retiredConfig.claims;
|
|
233
|
+
retiredConfig.claimPrefix = "claude/task/";
|
|
234
|
+
writeFileSync(retiredPath, `${JSON.stringify(retiredConfig, null, 2)}\n`);
|
|
235
|
+
sh(retired, "git", "commit", "--quiet", "-am", "retired key");
|
|
236
|
+
const refused = await cli(retired, "claim", keys.two);
|
|
237
|
+
assert.equal(refused.code, 2);
|
|
238
|
+
assert.match(refused.stderr, /pre-0\.8 spelling and is no longer read/);
|
|
188
239
|
});
|
|
189
|
-
/**
|
|
190
|
-
* A monorepo with two boards for the multi-board selection paths (TAS-27):
|
|
191
|
-
* a root board (ROO, one ready ticket) and a nested one under pkg/ (NES,
|
|
192
|
-
* one ready ticket). The root config carries the new `claims` block and,
|
|
193
|
-
* optionally, the top-level `boards` list — NES deliberately ahead of ROO,
|
|
194
|
-
* so priority provably follows list order, not nearness.
|
|
195
|
-
*/
|
|
196
240
|
function multiFixture(options = {}) {
|
|
197
241
|
const home = tempDir();
|
|
198
242
|
const bare = join(home, "origin.git");
|
|
@@ -208,10 +252,10 @@ function multiFixture(options = {}) {
|
|
|
208
252
|
config.boards = options.boards;
|
|
209
253
|
config.claims = { branchPrefix: "task/claim/", maxOpenCount: options.maxOpenCount };
|
|
210
254
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
211
|
-
root.create({ title: "Root ready", status: "todo" });
|
|
255
|
+
const rootKey = root.create({ title: "Root ready", status: "todo" }).key;
|
|
212
256
|
mkdirSync(join(seed, "pkg"), { recursive: true });
|
|
213
257
|
const nested = initProject(join(seed, "pkg"), { name: "nested board", prefix: "NES" });
|
|
214
|
-
nested.create({ title: "Nested ready", status: "todo" });
|
|
258
|
+
const nestedKey = nested.create({ title: "Nested ready", status: "todo" }).key;
|
|
215
259
|
sh(seed, "git", "add", "-A");
|
|
216
260
|
sh(seed, "git", "commit", "--quiet", "-m", "boards");
|
|
217
261
|
sh(seed, "git", "push", "--quiet", "-u", "origin", "main");
|
|
@@ -223,10 +267,10 @@ function multiFixture(options = {}) {
|
|
|
223
267
|
sh(dir, "git", "config", "user.email", "test@example.com");
|
|
224
268
|
return dir;
|
|
225
269
|
};
|
|
226
|
-
return { bare, clone };
|
|
270
|
+
return { bare, clone, rootKey, nestedKey };
|
|
227
271
|
}
|
|
228
272
|
test("claim --next drains the queue in position order and exits 1 when empty", async () => {
|
|
229
|
-
const { clone } = fixture();
|
|
273
|
+
const { clone, keys } = fixture();
|
|
230
274
|
const dir = clone();
|
|
231
275
|
const order = [];
|
|
232
276
|
for (;;) {
|
|
@@ -237,12 +281,13 @@ test("claim --next drains the queue in position order and exits 1 when empty", a
|
|
|
237
281
|
}
|
|
238
282
|
assert.equal(result.code, 0, result.stderr);
|
|
239
283
|
const { task, branch } = JSON.parse(result.stdout);
|
|
240
|
-
order.push(task.
|
|
284
|
+
order.push(task.key);
|
|
241
285
|
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), branch);
|
|
242
286
|
}
|
|
243
|
-
// The jumper leads (position order), then creation order; blocked
|
|
244
|
-
//
|
|
245
|
-
|
|
287
|
+
// The jumper leads (position order), then creation order; blocked and
|
|
288
|
+
// backlog tickets never surface, while the open ask on `person` doesn't
|
|
289
|
+
// keep it out of the queue.
|
|
290
|
+
assert.deepEqual(order, [keys.jumper, keys.one, keys.two, keys.person]);
|
|
246
291
|
});
|
|
247
292
|
test("two concurrent claim --next calls land on different tickets", async () => {
|
|
248
293
|
const { clone } = fixture();
|
|
@@ -258,40 +303,40 @@ test("two concurrent claim --next calls land on different tickets", async () =>
|
|
|
258
303
|
assert.notEqual(ids[0], ids[1], `both claimed ${ids[0]}`);
|
|
259
304
|
});
|
|
260
305
|
test("--next walks the root boards list in priority order; --board overrides it", async () => {
|
|
261
|
-
const { clone } = multiFixture({ boards: ["NES", "ROO"] });
|
|
306
|
+
const { clone, rootKey, nestedKey } = multiFixture({ boards: ["NES", "ROO"] });
|
|
262
307
|
const a = clone();
|
|
263
308
|
// list --claimable reads the same scope, in the same order.
|
|
264
309
|
const listed = await cli(a, "list", "--claimable", "--json");
|
|
265
310
|
assert.equal(listed.code, 0, listed.stderr);
|
|
266
311
|
const { tasks } = JSON.parse(listed.stdout);
|
|
267
|
-
assert.deepEqual(tasks.map((t) => t.id), [
|
|
312
|
+
assert.deepEqual(tasks.map((t) => t.id), [`NES-${nestedKey}`, `ROO-${rootKey}`]);
|
|
268
313
|
// The nested board wins despite the root board being nearest.
|
|
269
314
|
const next = await cli(a, "claim", "--next", "--json");
|
|
270
315
|
assert.equal(next.code, 0, next.stderr);
|
|
271
316
|
const claimed = JSON.parse(next.stdout);
|
|
272
|
-
assert.equal(claimed.task.id,
|
|
273
|
-
assert.equal(claimed.branch,
|
|
317
|
+
assert.equal(claimed.task.id, `NES-${nestedKey}`);
|
|
318
|
+
assert.equal(claimed.branch, `task/claim/nes-${nestedKey}`);
|
|
274
319
|
// An explicit --board list is the whole scope, in the given order.
|
|
275
320
|
const b = clone();
|
|
276
321
|
const overridden = await cli(b, "claim", "--next", "--board", "ROO", "--json");
|
|
277
322
|
assert.equal(overridden.code, 0, overridden.stderr);
|
|
278
|
-
assert.equal(JSON.parse(overridden.stdout).task.id,
|
|
323
|
+
assert.equal(JSON.parse(overridden.stdout).task.id, `ROO-${rootKey}`);
|
|
279
324
|
const unknown = await cli(b, "claim", "--next", "--board", "NOPE");
|
|
280
325
|
assert.equal(unknown.code, 2);
|
|
281
326
|
assert.match(unknown.stderr, /no board with prefix NOPE/);
|
|
282
327
|
});
|
|
283
328
|
test("without a boards list, --next sees the nearest board only", async () => {
|
|
284
|
-
const { clone } = multiFixture();
|
|
329
|
+
const { clone, rootKey } = multiFixture();
|
|
285
330
|
const dir = clone();
|
|
286
331
|
const next = await cli(dir, "claim", "--next", "--json");
|
|
287
332
|
assert.equal(next.code, 0, next.stderr);
|
|
288
|
-
assert.equal(JSON.parse(next.stdout).task.id,
|
|
333
|
+
assert.equal(JSON.parse(next.stdout).task.id, `ROO-${rootKey}`);
|
|
289
334
|
// ROO's queue is drained and NES never entered the scope.
|
|
290
335
|
const empty = await cli(dir, "claim", "--next", "--json");
|
|
291
336
|
assert.equal(empty.code, 1);
|
|
292
337
|
});
|
|
293
338
|
test("maxOpenCount caps every claim repo-wide; --force is the escape", async () => {
|
|
294
|
-
const { clone } = multiFixture({ boards: ["NES", "ROO"], maxOpenCount: 1 });
|
|
339
|
+
const { clone, rootKey } = multiFixture({ boards: ["NES", "ROO"], maxOpenCount: 1 });
|
|
295
340
|
const a = clone();
|
|
296
341
|
assert.equal((await cli(a, "claim", "--next", "--json")).code, 0);
|
|
297
342
|
// At the cap, --next and explicit-id claims both refuse with exit 2 —
|
|
@@ -300,40 +345,181 @@ test("maxOpenCount caps every claim repo-wide; --force is the escape", async ()
|
|
|
300
345
|
const capped = await cli(b, "claim", "--next");
|
|
301
346
|
assert.equal(capped.code, 2);
|
|
302
347
|
assert.match(capped.stderr, /maxOpenCount cap \(1\)/);
|
|
303
|
-
const explicit = await cli(b, "claim",
|
|
348
|
+
const explicit = await cli(b, "claim", `ROO-${rootKey}`);
|
|
304
349
|
assert.equal(explicit.code, 2);
|
|
305
350
|
assert.match(explicit.stderr, /maxOpenCount cap \(1\)/);
|
|
306
351
|
const forced = await cli(b, "claim", "--next", "--force", "--json");
|
|
307
352
|
assert.equal(forced.code, 0, forced.stderr);
|
|
308
|
-
assert.equal(JSON.parse(forced.stdout).task.id,
|
|
353
|
+
assert.equal(JSON.parse(forced.stdout).task.id, `ROO-${rootKey}`);
|
|
309
354
|
});
|
|
310
355
|
test("release requires a reason and leaves it as a comment on the ticket", async () => {
|
|
311
|
-
const { clone } = fixture();
|
|
356
|
+
const { clone, keys } = fixture();
|
|
312
357
|
const dir = clone();
|
|
313
|
-
assert.equal((await cli(dir, "claim",
|
|
358
|
+
assert.equal((await cli(dir, "claim", keys.one)).code, 0);
|
|
314
359
|
// No comment, and an empty one, are both refused before anything happens.
|
|
315
|
-
const missing = await cli(dir, "claim", "--release",
|
|
360
|
+
const missing = await cli(dir, "claim", "--release", keys.one);
|
|
316
361
|
assert.equal(missing.code, 1);
|
|
317
362
|
assert.match(missing.stderr, /requires --comment/);
|
|
318
|
-
const empty = await cli(dir, "claim", "--release",
|
|
363
|
+
const empty = await cli(dir, "claim", "--release", keys.one, "--comment", "");
|
|
319
364
|
assert.equal(empty.code, 1);
|
|
320
365
|
assert.match(empty.stderr, /requires --comment/);
|
|
321
|
-
assert.
|
|
322
|
-
const released = await cli(dir, "claim", "--release",
|
|
366
|
+
assert.ok(sh(dir, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.one}`).includes(`clm-${keys.one}`));
|
|
367
|
+
const released = await cli(dir, "claim", "--release", keys.one, "--comment", "the migration kept flaking — needs a fixture, not a retry", "--author", "claude");
|
|
323
368
|
assert.equal(released.code, 0, released.stderr);
|
|
324
369
|
// The reason survives the branch: an ordinary attributed comment, sitting
|
|
325
370
|
// uncommitted in the working tree on the default branch.
|
|
326
|
-
const shown = await cli(dir, "show",
|
|
371
|
+
const shown = await cli(dir, "show", keys.one, "--json");
|
|
327
372
|
const { comments } = JSON.parse(shown.stdout);
|
|
328
373
|
assert.equal(comments.length, 1);
|
|
329
374
|
assert.equal(comments[0].author, "claude");
|
|
330
375
|
assert.match(comments[0].body, /kept flaking/);
|
|
331
|
-
assert.
|
|
376
|
+
assert.ok(sh(dir, "git", "status", "--porcelain").includes(`tickets/${keys.one}/comments`));
|
|
332
377
|
// Releasing an unclaimed ticket stays a no-op — no second comment.
|
|
333
|
-
const noop = await cli(dir, "claim", "--release",
|
|
378
|
+
const noop = await cli(dir, "claim", "--release", keys.one, "--comment", "again");
|
|
334
379
|
assert.equal(noop.code, 0);
|
|
335
380
|
assert.match(noop.stdout, /wasn't claimed/);
|
|
336
|
-
const after = await cli(dir, "show",
|
|
381
|
+
const after = await cli(dir, "show", keys.one, "--json");
|
|
337
382
|
assert.equal(JSON.parse(after.stdout).comments.length, 1);
|
|
338
383
|
});
|
|
384
|
+
test("--lock-only claims from a dirty, branch-pinned checkout without touching it", async () => {
|
|
385
|
+
const { clone, keys } = fixture();
|
|
386
|
+
const dir = clone();
|
|
387
|
+
const branch = `task/claim/clm-${keys.one}`;
|
|
388
|
+
// The branch-pinned session: a provisioned branch, uncommitted tracked work.
|
|
389
|
+
sh(dir, "git", "checkout", "--quiet", "-b", "pinned");
|
|
390
|
+
const gitignore = join(dir, ".task", ".gitignore");
|
|
391
|
+
writeFileSync(gitignore, `${readFileSync(gitignore, "utf8")}\nwip\n`);
|
|
392
|
+
const claimed = await cli(dir, "claim", keys.one, "--lock-only", "--json");
|
|
393
|
+
assert.equal(claimed.code, 0, claimed.stderr);
|
|
394
|
+
const parsed = JSON.parse(claimed.stdout);
|
|
395
|
+
assert.equal(parsed.branch, branch);
|
|
396
|
+
assert.equal(parsed.deliveredBy, "pinned");
|
|
397
|
+
assert.equal(parsed.task.status, "in_progress");
|
|
398
|
+
// The checkout is exactly where it was: same branch, same dirty file, and
|
|
399
|
+
// the local ticket file never flipped — the claim lives on origin only.
|
|
400
|
+
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), "pinned");
|
|
401
|
+
assert.match(sh(dir, "git", "status", "--porcelain", "-uno"), /\.task\/\.gitignore/);
|
|
402
|
+
assert.match(readFileSync(join(dir, ".task", "tickets", keys.one, "ticket.md"), "utf8"), /status: todo/);
|
|
403
|
+
// On origin the claim is a real flip commit built on the default branch,
|
|
404
|
+
// carrying the delivery branch as a trailer.
|
|
405
|
+
assert.ok(sh(dir, "git", "ls-remote", "--heads", "origin", branch).includes(`refs/heads/${branch}`));
|
|
406
|
+
assert.equal(sh(dir, "git", "rev-parse", `origin/${branch}~1`), sh(dir, "git", "rev-parse", "origin/main"));
|
|
407
|
+
assert.match(sh(dir, "git", "show", `origin/${branch}:.task/tickets/${keys.one}/ticket.md`), /status: in_progress/);
|
|
408
|
+
const message = sh(dir, "git", "log", "-1", "--format=%B", `origin/${branch}`);
|
|
409
|
+
assert.match(message, /lock-only/);
|
|
410
|
+
assert.match(message, new RegExp(`delivered_by: pinned`));
|
|
411
|
+
// The lock is the same lock: both claim flavors lose to it from elsewhere.
|
|
412
|
+
const other = clone();
|
|
413
|
+
assert.equal((await cli(other, "claim", keys.one)).code, 1);
|
|
414
|
+
assert.equal((await cli(other, "claim", keys.one, "--lock-only")).code, 1);
|
|
415
|
+
// Release is symmetric: no local branch to delete, the origin one goes, and
|
|
416
|
+
// the pinned checkout still isn't touched.
|
|
417
|
+
const released = await cli(dir, "claim", "--release", keys.one, "--comment", "handing back a lock-only claim");
|
|
418
|
+
assert.equal(released.code, 0, released.stderr);
|
|
419
|
+
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), "pinned");
|
|
420
|
+
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", branch), "");
|
|
421
|
+
assert.equal((await cli(other, "claim", keys.one)).code, 0);
|
|
422
|
+
});
|
|
423
|
+
test("--lock-only validates against origin's default branch, not the checkout", async () => {
|
|
424
|
+
const { clone, keys } = fixture();
|
|
425
|
+
const dir = clone();
|
|
426
|
+
// Not todo on the base tree → exit 2, and nothing minted on origin.
|
|
427
|
+
const parked = await cli(dir, "claim", keys.parked, "--lock-only");
|
|
428
|
+
assert.equal(parked.code, 2);
|
|
429
|
+
assert.match(parked.stderr, /is backlog on origin\/main/);
|
|
430
|
+
// A local promote that hasn't landed on the default branch doesn't count:
|
|
431
|
+
// the flip commit is built on origin/main, where the ticket is still backlog.
|
|
432
|
+
assert.equal((await cli(dir, "promote", keys.parked)).code, 0);
|
|
433
|
+
const promoted = await cli(dir, "claim", keys.parked, "--lock-only");
|
|
434
|
+
assert.equal(promoted.code, 2);
|
|
435
|
+
assert.match(promoted.stderr, /is backlog on origin\/main/);
|
|
436
|
+
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.parked}`), "");
|
|
437
|
+
// Blocked on the base tree refuses too.
|
|
438
|
+
const blocked = await cli(dir, "claim", keys.blocked, "--lock-only");
|
|
439
|
+
assert.equal(blocked.code, 2);
|
|
440
|
+
assert.match(blocked.stderr, new RegExp(`blocked by CLM-${keys.one}`));
|
|
441
|
+
});
|
|
442
|
+
test("plain claim warns when it moves the checkout off a non-default branch", async () => {
|
|
443
|
+
const { clone, keys } = fixture();
|
|
444
|
+
const dir = clone();
|
|
445
|
+
sh(dir, "git", "checkout", "--quiet", "-b", "feature");
|
|
446
|
+
const claimed = await cli(dir, "claim", keys.one);
|
|
447
|
+
assert.equal(claimed.code, 0, claimed.stderr);
|
|
448
|
+
assert.match(claimed.stdout, /moved you off feature/);
|
|
449
|
+
assert.match(claimed.stdout, /--lock-only/);
|
|
450
|
+
// From the default branch there is nothing to warn about.
|
|
451
|
+
const other = clone();
|
|
452
|
+
const quiet = await cli(other, "claim", keys.two);
|
|
453
|
+
assert.equal(quiet.code, 0, quiet.stderr);
|
|
454
|
+
assert.doesNotMatch(quiet.stdout, /moved you off/);
|
|
455
|
+
});
|
|
456
|
+
test("sweep deletes done/canceled/missing claims and keeps live ones", async () => {
|
|
457
|
+
const { clone, keys } = fixture();
|
|
458
|
+
const a = clone();
|
|
459
|
+
assert.equal((await cli(a, "claim", keys.one)).code, 0);
|
|
460
|
+
assert.equal((await cli(a, "claim", keys.two)).code, 0);
|
|
461
|
+
assert.equal((await cli(a, "claim", keys.person)).code, 0);
|
|
462
|
+
// A second checkout is where the tickets' fates land (a merge, normally):
|
|
463
|
+
// one finishes, person is canceled, two stays live — plus a branch under
|
|
464
|
+
// the namespace whose ticket never existed.
|
|
465
|
+
const b = clone();
|
|
466
|
+
assert.equal((await cli(b, "move", keys.one, "done")).code, 0);
|
|
467
|
+
assert.equal((await cli(b, "move", keys.person, "canceled")).code, 0);
|
|
468
|
+
sh(b, "git", "push", "--quiet", "origin", "main:refs/heads/task/claim/clm-zz9zz");
|
|
469
|
+
// Namespace squatters sweep must NOT touch: a pre-migration numeric claim
|
|
470
|
+
// (its branch may head an open PR carrying unmerged work — the TAS-31 case)
|
|
471
|
+
// and a branch matching no board's prefix. Skipped and reported, never
|
|
472
|
+
// deleted — sweep's jurisdiction is exactly the names `claim` mints.
|
|
473
|
+
sh(b, "git", "push", "--quiet", "origin", "main:refs/heads/task/claim/clm-31");
|
|
474
|
+
sh(b, "git", "push", "--quiet", "origin", "main:refs/heads/task/claim/other-zz9zz");
|
|
475
|
+
const swept = await cli(b, "sweep", "--json");
|
|
476
|
+
assert.equal(swept.code, 0, swept.stderr);
|
|
477
|
+
const { sweep } = JSON.parse(swept.stdout);
|
|
478
|
+
assert.deepEqual(sweep.swept.map((s) => [s.branch, s.reason]).sort(), [
|
|
479
|
+
[`task/claim/clm-${keys.one}`, "done"],
|
|
480
|
+
[`task/claim/clm-${keys.person}`, "canceled"],
|
|
481
|
+
["task/claim/clm-zz9zz", "missing"],
|
|
482
|
+
].sort());
|
|
483
|
+
assert.deepEqual(sweep.kept, [
|
|
484
|
+
{ branch: `task/claim/clm-${keys.two}`, ticket: `CLM-${keys.two}`, status: "todo" },
|
|
485
|
+
]);
|
|
486
|
+
assert.deepEqual(sweep.skipped.sort(), ["task/claim/clm-31", "task/claim/other-zz9zz"]);
|
|
487
|
+
assert.equal(sweep.failed.length, 0);
|
|
488
|
+
// Origin agrees: the live claim and the out-of-scheme branches survive,
|
|
489
|
+
// and a re-run is a no-op.
|
|
490
|
+
const remote = sh(b, "git", "ls-remote", "--heads", "origin", "task/claim/*");
|
|
491
|
+
assert.deepEqual(remote.split("\n").map((l) => l.split("refs/heads/")[1]).sort(), ["task/claim/clm-31", `task/claim/clm-${keys.two}`, "task/claim/other-zz9zz"].sort());
|
|
492
|
+
const again = await cli(b, "sweep", "--json");
|
|
493
|
+
assert.equal(again.code, 0);
|
|
494
|
+
assert.equal(JSON.parse(again.stdout).sweep.swept.length, 0);
|
|
495
|
+
});
|
|
496
|
+
test("verified deletion: a remote that drops deletions makes release and sweep fail loudly", { skip: process.platform === "win32" }, async () => {
|
|
497
|
+
const { clone, keys } = fixture();
|
|
498
|
+
const path = deleteDroppingPath();
|
|
499
|
+
const dir = clone();
|
|
500
|
+
assert.equal((await cli(dir, "claim", keys.one)).code, 0);
|
|
501
|
+
// Release under the shim: git says the deletion worked, the ref survives —
|
|
502
|
+
// pre-TAS-8b7s9 this reported "released" and left a stale lock behind.
|
|
503
|
+
const released = await cliEnv(dir, { PATH: path }, "claim", "--release", keys.one, "--comment", "should not land");
|
|
504
|
+
assert.notEqual(released.code, 0);
|
|
505
|
+
assert.match(released.stderr, /origin still has/);
|
|
506
|
+
assert.doesNotMatch(released.stdout, /released CLM/);
|
|
507
|
+
assert.ok(sh(dir, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.one}`).includes(`clm-${keys.one}`));
|
|
508
|
+
// No release, no comment: the failure must not read as a done release.
|
|
509
|
+
const shown = await cli(dir, "show", keys.one, "--json");
|
|
510
|
+
assert.equal(JSON.parse(shown.stdout).comments.length, 0);
|
|
511
|
+
// Sweep under the shim reports the failed deletion and exits non-zero.
|
|
512
|
+
const b = clone();
|
|
513
|
+
assert.equal((await cli(b, "move", keys.one, "done")).code, 0);
|
|
514
|
+
const blocked = await cliEnv(b, { PATH: path }, "sweep", "--json");
|
|
515
|
+
assert.equal(blocked.code, 1);
|
|
516
|
+
const { sweep } = JSON.parse(blocked.stdout);
|
|
517
|
+
assert.equal(sweep.swept.length, 0);
|
|
518
|
+
assert.equal(sweep.failed.length, 1);
|
|
519
|
+
assert.match(sweep.failed[0].error, /origin still has/);
|
|
520
|
+
// A capable runner heals it: the same sweep with real git succeeds.
|
|
521
|
+
const healed = await cli(b, "sweep");
|
|
522
|
+
assert.equal(healed.code, 0, healed.stderr);
|
|
523
|
+
assert.equal(sh(b, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.one}`), "");
|
|
524
|
+
});
|
|
339
525
|
//# sourceMappingURL=claim.test.js.map
|