@nickmeriano/task 0.9.0 → 0.11.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 +129 -0
- package/dist/asks.test.js +6 -0
- package/dist/asks.test.js.map +1 -1
- package/dist/claim-io.d.ts.map +1 -1
- package/dist/claim-io.js +4 -3
- package/dist/claim-io.js.map +1 -1
- package/dist/claim.d.ts +99 -1
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +271 -6
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.d.ts +8 -0
- package/dist/claim.test.d.ts.map +1 -1
- package/dist/claim.test.js +211 -2
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +193 -7
- package/dist/cli.js.map +1 -1
- package/dist/export.d.ts +132 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +199 -0
- package/dist/export.js.map +1 -0
- package/dist/export.test.d.ts +12 -0
- package/dist/export.test.d.ts.map +1 -0
- package/dist/export.test.js +185 -0
- package/dist/export.test.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +12 -3
- package/dist/server.js.map +1 -1
- package/dist/trailers.d.ts +51 -0
- package/dist/trailers.d.ts.map +1 -0
- package/dist/trailers.js +32 -0
- package/dist/trailers.js.map +1 -0
- package/package.json +1 -1
- package/skill/SKILL.md +34 -1
- package/src/asks.test.ts +15 -0
- package/src/claim-io.ts +12 -3
- package/src/claim.test.ts +286 -2
- package/src/claim.ts +375 -6
- package/src/cli.ts +208 -7
- package/src/export.test.ts +254 -0
- package/src/export.ts +276 -0
- package/src/server.ts +12 -3
- package/src/trailers.ts +65 -0
- package/ui/dist/assets/{index-CoKCUYic.css → index-eHsqltgs.css} +1 -1
- package/ui/dist/assets/{index-BjsorZOU.js → index-mJmm4sWq.js} +53 -53
- package/ui/dist/index.html +3 -3
package/src/claim.test.ts
CHANGED
|
@@ -10,12 +10,20 @@
|
|
|
10
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
|
|
|
15
23
|
import assert from "node:assert/strict"
|
|
16
24
|
import { test } from "node:test"
|
|
17
25
|
import { execFile, spawnSync } from "node:child_process"
|
|
18
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
26
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
19
27
|
import { readFileSync } from "node:fs"
|
|
20
28
|
import { tmpdir } from "node:os"
|
|
21
29
|
import { join } from "node:path"
|
|
@@ -46,11 +54,16 @@ interface CliResult {
|
|
|
46
54
|
|
|
47
55
|
/** The CLI as callers see it — a subprocess with an exit code. */
|
|
48
56
|
function cli(cwd: string, ...args: string[]): Promise<CliResult> {
|
|
57
|
+
return cliEnv(cwd, {}, ...args)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Like cli(), with extra env — the delete-dropping git shim rides in PATH. */
|
|
61
|
+
function cliEnv(cwd: string, env: Record<string, string>, ...args: string[]): Promise<CliResult> {
|
|
49
62
|
return new Promise((resolve) => {
|
|
50
63
|
execFile(
|
|
51
64
|
process.execPath,
|
|
52
65
|
["--experimental-strip-types", CLI, ...args],
|
|
53
|
-
{ cwd },
|
|
66
|
+
{ cwd, env: { ...process.env, ...env } },
|
|
54
67
|
(error, stdout, stderr) => {
|
|
55
68
|
resolve({ code: error ? ((error as { code?: number }).code ?? 1) : 0, stdout, stderr })
|
|
56
69
|
},
|
|
@@ -58,6 +71,26 @@ function cli(cwd: string, ...args: string[]): Promise<CliResult> {
|
|
|
58
71
|
})
|
|
59
72
|
}
|
|
60
73
|
|
|
74
|
+
/**
|
|
75
|
+
* A PATH whose `git` silently swallows branch-deletion pushes — exit 0,
|
|
76
|
+
* nothing deleted — and delegates everything else to the real git. This is
|
|
77
|
+
* the observed cloud-proxy behavior TAS-8b7s9 documents: the deletion "works"
|
|
78
|
+
* while the ref survives, which is exactly what verified release must catch.
|
|
79
|
+
* POSIX-only by construction, like the failure it reproduces.
|
|
80
|
+
*/
|
|
81
|
+
function deleteDroppingPath(): string {
|
|
82
|
+
const dir = mkdtempSync(join(tmpdir(), "task-fake-git-"))
|
|
83
|
+
process.on("exit", () => rmSync(dir, { recursive: true, force: true }))
|
|
84
|
+
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim()
|
|
85
|
+
const shim = join(dir, "git")
|
|
86
|
+
writeFileSync(
|
|
87
|
+
shim,
|
|
88
|
+
`#!/bin/sh\nfor a in "$@"; do case "$a" in --delete) exit 0;; esac; done\nexec "${real}" "$@"\n`,
|
|
89
|
+
)
|
|
90
|
+
chmodSync(shim, 0o755)
|
|
91
|
+
return `${dir}:${process.env.PATH ?? ""}`
|
|
92
|
+
}
|
|
93
|
+
|
|
61
94
|
interface Fixture {
|
|
62
95
|
bare: string
|
|
63
96
|
clone: () => string
|
|
@@ -150,9 +183,54 @@ test("two concurrent claims of one ticket: exactly one wins", async () => {
|
|
|
150
183
|
sh(winner, "git", "show", `origin/main:.task/tickets/${keys.one}/ticket.md`),
|
|
151
184
|
/status: todo/,
|
|
152
185
|
)
|
|
186
|
+
// The trailer contract (trailers.ts): the flip commit names its op and
|
|
187
|
+
// ticket in machine-readable trailers — pinned here because automation
|
|
188
|
+
// keys on them.
|
|
189
|
+
assert.equal(sh(winner, "git", "log", "-1", "--format=%(trailers:key=Task-Op,valueonly)"), "claim")
|
|
190
|
+
assert.equal(
|
|
191
|
+
sh(winner, "git", "log", "-1", "--format=%(trailers:key=Task-Id,valueonly)"),
|
|
192
|
+
`CLM-${keys.one}`,
|
|
193
|
+
)
|
|
153
194
|
void bare
|
|
154
195
|
})
|
|
155
196
|
|
|
197
|
+
test("claim --dry-run: the real gates, the real exit codes, zero writes", async () => {
|
|
198
|
+
const { clone, keys } = fixture()
|
|
199
|
+
const dir = clone()
|
|
200
|
+
const branch = `task/claim/clm-${keys.one}`
|
|
201
|
+
|
|
202
|
+
// Would claim: exit 0 — and nothing moved. No branch local or remote, the
|
|
203
|
+
// checkout still on main, the ticket still todo.
|
|
204
|
+
const yes = await cli(dir, "claim", keys.one, "--dry-run")
|
|
205
|
+
assert.equal(yes.code, 0, yes.stderr)
|
|
206
|
+
assert.match(yes.stdout, /would claim CLM-/)
|
|
207
|
+
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), "main")
|
|
208
|
+
assert.equal(sh(dir, "git", "branch", "--list", branch), "")
|
|
209
|
+
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", branch), "")
|
|
210
|
+
assert.match(readFileSync(join(dir, ".task", "tickets", keys.one, "ticket.md"), "utf8"), /status: todo/)
|
|
211
|
+
|
|
212
|
+
// The gates are the real command's gates: blocked exits 2, a ticket someone
|
|
213
|
+
// else claimed exits 1 — the codes a scheduler scripts against.
|
|
214
|
+
assert.equal((await cli(dir, "claim", keys.blocked, "--dry-run")).code, 2)
|
|
215
|
+
const other = clone()
|
|
216
|
+
assert.equal((await cli(other, "claim", keys.one)).code, 0)
|
|
217
|
+
assert.equal((await cli(dir, "claim", keys.one, "--dry-run")).code, 1)
|
|
218
|
+
|
|
219
|
+
// --next --dry-run names the top of the queue without taking it.
|
|
220
|
+
const next = await cli(dir, "claim", "--next", "--dry-run", "--json")
|
|
221
|
+
assert.equal(next.code, 0, next.stderr)
|
|
222
|
+
const picked = JSON.parse(next.stdout) as { task: { key: string }; dryRun: boolean }
|
|
223
|
+
assert.equal(picked.dryRun, true)
|
|
224
|
+
assert.equal(picked.task.key, keys.jumper)
|
|
225
|
+
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.jumper}`), "")
|
|
226
|
+
|
|
227
|
+
// A dry run reserves nothing — the real claim right after still works.
|
|
228
|
+
assert.equal((await cli(dir, "claim", keys.jumper)).code, 0)
|
|
229
|
+
|
|
230
|
+
// No stop-before-writing seam exists for --release or --lock-only.
|
|
231
|
+
assert.equal((await cli(other, "claim", keys.two, "--lock-only", "--dry-run")).code, 1)
|
|
232
|
+
})
|
|
233
|
+
|
|
156
234
|
test("unclaimable tickets exit 2, before anything touches origin", async () => {
|
|
157
235
|
const { clone, keys } = fixture()
|
|
158
236
|
const dir = clone()
|
|
@@ -470,3 +548,209 @@ test("release requires a reason and leaves it as a comment on the ticket", async
|
|
|
470
548
|
const after = await cli(dir, "show", keys.one, "--json")
|
|
471
549
|
assert.equal((JSON.parse(after.stdout) as { comments: unknown[] }).comments.length, 1)
|
|
472
550
|
})
|
|
551
|
+
|
|
552
|
+
test("--lock-only claims from a dirty, branch-pinned checkout without touching it", async () => {
|
|
553
|
+
const { clone, keys } = fixture()
|
|
554
|
+
const dir = clone()
|
|
555
|
+
const branch = `task/claim/clm-${keys.one}`
|
|
556
|
+
|
|
557
|
+
// The branch-pinned session: a provisioned branch, uncommitted tracked work.
|
|
558
|
+
sh(dir, "git", "checkout", "--quiet", "-b", "pinned")
|
|
559
|
+
const gitignore = join(dir, ".task", ".gitignore")
|
|
560
|
+
writeFileSync(gitignore, `${readFileSync(gitignore, "utf8")}\nwip\n`)
|
|
561
|
+
|
|
562
|
+
const claimed = await cli(dir, "claim", keys.one, "--lock-only", "--json")
|
|
563
|
+
assert.equal(claimed.code, 0, claimed.stderr)
|
|
564
|
+
const parsed = JSON.parse(claimed.stdout) as {
|
|
565
|
+
task: { status: string }
|
|
566
|
+
branch: string
|
|
567
|
+
deliveredBy: string
|
|
568
|
+
}
|
|
569
|
+
assert.equal(parsed.branch, branch)
|
|
570
|
+
assert.equal(parsed.deliveredBy, "pinned")
|
|
571
|
+
assert.equal(parsed.task.status, "in_progress")
|
|
572
|
+
|
|
573
|
+
// The checkout is exactly where it was: same branch, same dirty file, and
|
|
574
|
+
// the local ticket file never flipped — the claim lives on origin only.
|
|
575
|
+
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), "pinned")
|
|
576
|
+
assert.match(sh(dir, "git", "status", "--porcelain", "-uno"), /\.task\/\.gitignore/)
|
|
577
|
+
assert.match(readFileSync(join(dir, ".task", "tickets", keys.one, "ticket.md"), "utf8"), /status: todo/)
|
|
578
|
+
|
|
579
|
+
// On origin the claim is a real flip commit built on the default branch,
|
|
580
|
+
// carrying the delivery branch as a trailer.
|
|
581
|
+
assert.ok(sh(dir, "git", "ls-remote", "--heads", "origin", branch).includes(`refs/heads/${branch}`))
|
|
582
|
+
assert.equal(
|
|
583
|
+
sh(dir, "git", "rev-parse", `origin/${branch}~1`),
|
|
584
|
+
sh(dir, "git", "rev-parse", "origin/main"),
|
|
585
|
+
)
|
|
586
|
+
assert.match(sh(dir, "git", "show", `origin/${branch}:.task/tickets/${keys.one}/ticket.md`), /status: in_progress/)
|
|
587
|
+
const message = sh(dir, "git", "log", "-1", "--format=%B", `origin/${branch}`)
|
|
588
|
+
assert.match(message, /lock-only/)
|
|
589
|
+
assert.match(message, new RegExp(`Delivered-By: pinned`))
|
|
590
|
+
// Delivered-By shares one contiguous trailer block with the Task-Op/Task-Id
|
|
591
|
+
// contract (trailers.ts) — git must parse all three.
|
|
592
|
+
assert.equal(
|
|
593
|
+
sh(dir, "git", "log", "-1", "--format=%(trailers:key=Task-Op,valueonly)", `origin/${branch}`),
|
|
594
|
+
"claim",
|
|
595
|
+
)
|
|
596
|
+
assert.equal(
|
|
597
|
+
sh(dir, "git", "log", "-1", "--format=%(trailers:key=Delivered-By,valueonly)", `origin/${branch}`),
|
|
598
|
+
"pinned",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
// The lock is the same lock: both claim flavors lose to it from elsewhere.
|
|
602
|
+
const other = clone()
|
|
603
|
+
assert.equal((await cli(other, "claim", keys.one)).code, 1)
|
|
604
|
+
assert.equal((await cli(other, "claim", keys.one, "--lock-only")).code, 1)
|
|
605
|
+
|
|
606
|
+
// Release is symmetric: no local branch to delete, the origin one goes, and
|
|
607
|
+
// the pinned checkout still isn't touched.
|
|
608
|
+
const released = await cli(dir, "claim", "--release", keys.one, "--comment", "handing back a lock-only claim")
|
|
609
|
+
assert.equal(released.code, 0, released.stderr)
|
|
610
|
+
assert.equal(sh(dir, "git", "rev-parse", "--abbrev-ref", "HEAD"), "pinned")
|
|
611
|
+
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", branch), "")
|
|
612
|
+
assert.equal((await cli(other, "claim", keys.one)).code, 0)
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
test("--lock-only validates against origin's default branch, not the checkout", async () => {
|
|
616
|
+
const { clone, keys } = fixture()
|
|
617
|
+
const dir = clone()
|
|
618
|
+
|
|
619
|
+
// Not todo on the base tree → exit 2, and nothing minted on origin.
|
|
620
|
+
const parked = await cli(dir, "claim", keys.parked, "--lock-only")
|
|
621
|
+
assert.equal(parked.code, 2)
|
|
622
|
+
assert.match(parked.stderr, /is backlog on origin\/main/)
|
|
623
|
+
|
|
624
|
+
// A local promote that hasn't landed on the default branch doesn't count:
|
|
625
|
+
// the flip commit is built on origin/main, where the ticket is still backlog.
|
|
626
|
+
assert.equal((await cli(dir, "promote", keys.parked)).code, 0)
|
|
627
|
+
const promoted = await cli(dir, "claim", keys.parked, "--lock-only")
|
|
628
|
+
assert.equal(promoted.code, 2)
|
|
629
|
+
assert.match(promoted.stderr, /is backlog on origin\/main/)
|
|
630
|
+
assert.equal(sh(dir, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.parked}`), "")
|
|
631
|
+
|
|
632
|
+
// Blocked on the base tree refuses too.
|
|
633
|
+
const blocked = await cli(dir, "claim", keys.blocked, "--lock-only")
|
|
634
|
+
assert.equal(blocked.code, 2)
|
|
635
|
+
assert.match(blocked.stderr, new RegExp(`blocked by CLM-${keys.one}`))
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test("plain claim warns when it moves the checkout off a non-default branch", async () => {
|
|
639
|
+
const { clone, keys } = fixture()
|
|
640
|
+
const dir = clone()
|
|
641
|
+
sh(dir, "git", "checkout", "--quiet", "-b", "feature")
|
|
642
|
+
|
|
643
|
+
const claimed = await cli(dir, "claim", keys.one)
|
|
644
|
+
assert.equal(claimed.code, 0, claimed.stderr)
|
|
645
|
+
assert.match(claimed.stdout, /moved you off feature/)
|
|
646
|
+
assert.match(claimed.stdout, /--lock-only/)
|
|
647
|
+
|
|
648
|
+
// From the default branch there is nothing to warn about.
|
|
649
|
+
const other = clone()
|
|
650
|
+
const quiet = await cli(other, "claim", keys.two)
|
|
651
|
+
assert.equal(quiet.code, 0, quiet.stderr)
|
|
652
|
+
assert.doesNotMatch(quiet.stdout, /moved you off/)
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
test("sweep deletes done/canceled/missing claims and keeps live ones", async () => {
|
|
656
|
+
const { clone, keys } = fixture()
|
|
657
|
+
const a = clone()
|
|
658
|
+
assert.equal((await cli(a, "claim", keys.one)).code, 0)
|
|
659
|
+
assert.equal((await cli(a, "claim", keys.two)).code, 0)
|
|
660
|
+
assert.equal((await cli(a, "claim", keys.person)).code, 0)
|
|
661
|
+
|
|
662
|
+
// A second checkout is where the tickets' fates land (a merge, normally):
|
|
663
|
+
// one finishes, person is canceled, two stays live — plus a branch under
|
|
664
|
+
// the namespace whose ticket never existed.
|
|
665
|
+
const b = clone()
|
|
666
|
+
assert.equal((await cli(b, "move", keys.one, "done")).code, 0)
|
|
667
|
+
assert.equal((await cli(b, "move", keys.person, "canceled")).code, 0)
|
|
668
|
+
sh(b, "git", "push", "--quiet", "origin", "main:refs/heads/task/claim/clm-zz9zz")
|
|
669
|
+
// Namespace squatters sweep must NOT touch: a pre-migration numeric claim
|
|
670
|
+
// (its branch may head an open PR carrying unmerged work — the TAS-31 case)
|
|
671
|
+
// and a branch matching no board's prefix. Skipped and reported, never
|
|
672
|
+
// deleted — sweep's jurisdiction is exactly the names `claim` mints.
|
|
673
|
+
sh(b, "git", "push", "--quiet", "origin", "main:refs/heads/task/claim/clm-31")
|
|
674
|
+
sh(b, "git", "push", "--quiet", "origin", "main:refs/heads/task/claim/other-zz9zz")
|
|
675
|
+
|
|
676
|
+
const swept = await cli(b, "sweep", "--json")
|
|
677
|
+
assert.equal(swept.code, 0, swept.stderr)
|
|
678
|
+
const { sweep } = JSON.parse(swept.stdout) as {
|
|
679
|
+
sweep: {
|
|
680
|
+
swept: { branch: string; reason: string }[]
|
|
681
|
+
kept: { branch: string; status: string }[]
|
|
682
|
+
skipped: string[]
|
|
683
|
+
failed: unknown[]
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
assert.deepEqual(
|
|
687
|
+
sweep.swept.map((s) => [s.branch, s.reason]).sort(),
|
|
688
|
+
[
|
|
689
|
+
[`task/claim/clm-${keys.one}`, "done"],
|
|
690
|
+
[`task/claim/clm-${keys.person}`, "canceled"],
|
|
691
|
+
["task/claim/clm-zz9zz", "missing"],
|
|
692
|
+
].sort(),
|
|
693
|
+
)
|
|
694
|
+
assert.deepEqual(sweep.kept, [
|
|
695
|
+
{ branch: `task/claim/clm-${keys.two}`, ticket: `CLM-${keys.two}`, status: "todo" },
|
|
696
|
+
])
|
|
697
|
+
assert.deepEqual(sweep.skipped.sort(), ["task/claim/clm-31", "task/claim/other-zz9zz"])
|
|
698
|
+
assert.equal(sweep.failed.length, 0)
|
|
699
|
+
|
|
700
|
+
// Origin agrees: the live claim and the out-of-scheme branches survive,
|
|
701
|
+
// and a re-run is a no-op.
|
|
702
|
+
const remote = sh(b, "git", "ls-remote", "--heads", "origin", "task/claim/*")
|
|
703
|
+
assert.deepEqual(
|
|
704
|
+
remote.split("\n").map((l) => l.split("refs/heads/")[1]).sort(),
|
|
705
|
+
["task/claim/clm-31", `task/claim/clm-${keys.two}`, "task/claim/other-zz9zz"].sort(),
|
|
706
|
+
)
|
|
707
|
+
const again = await cli(b, "sweep", "--json")
|
|
708
|
+
assert.equal(again.code, 0)
|
|
709
|
+
assert.equal((JSON.parse(again.stdout) as { sweep: { swept: unknown[] } }).sweep.swept.length, 0)
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test(
|
|
713
|
+
"verified deletion: a remote that drops deletions makes release and sweep fail loudly",
|
|
714
|
+
{ skip: process.platform === "win32" },
|
|
715
|
+
async () => {
|
|
716
|
+
const { clone, keys } = fixture()
|
|
717
|
+
const path = deleteDroppingPath()
|
|
718
|
+
const dir = clone()
|
|
719
|
+
assert.equal((await cli(dir, "claim", keys.one)).code, 0)
|
|
720
|
+
|
|
721
|
+
// Release under the shim: git says the deletion worked, the ref survives —
|
|
722
|
+
// pre-TAS-8b7s9 this reported "released" and left a stale lock behind.
|
|
723
|
+
const released = await cliEnv(
|
|
724
|
+
dir, { PATH: path },
|
|
725
|
+
"claim", "--release", keys.one, "--comment", "should not land",
|
|
726
|
+
)
|
|
727
|
+
assert.notEqual(released.code, 0)
|
|
728
|
+
assert.match(released.stderr, /origin still has/)
|
|
729
|
+
assert.doesNotMatch(released.stdout, /released CLM/)
|
|
730
|
+
assert.ok(
|
|
731
|
+
sh(dir, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.one}`).includes(
|
|
732
|
+
`clm-${keys.one}`,
|
|
733
|
+
),
|
|
734
|
+
)
|
|
735
|
+
// No release, no comment: the failure must not read as a done release.
|
|
736
|
+
const shown = await cli(dir, "show", keys.one, "--json")
|
|
737
|
+
assert.equal((JSON.parse(shown.stdout) as { comments: unknown[] }).comments.length, 0)
|
|
738
|
+
|
|
739
|
+
// Sweep under the shim reports the failed deletion and exits non-zero.
|
|
740
|
+
const b = clone()
|
|
741
|
+
assert.equal((await cli(b, "move", keys.one, "done")).code, 0)
|
|
742
|
+
const blocked = await cliEnv(b, { PATH: path }, "sweep", "--json")
|
|
743
|
+
assert.equal(blocked.code, 1)
|
|
744
|
+
const { sweep } = JSON.parse(blocked.stdout) as {
|
|
745
|
+
sweep: { swept: unknown[]; failed: { branch: string; error: string }[] }
|
|
746
|
+
}
|
|
747
|
+
assert.equal(sweep.swept.length, 0)
|
|
748
|
+
assert.equal(sweep.failed.length, 1)
|
|
749
|
+
assert.match(sweep.failed[0].error, /origin still has/)
|
|
750
|
+
|
|
751
|
+
// A capable runner heals it: the same sweep with real git succeeds.
|
|
752
|
+
const healed = await cli(b, "sweep")
|
|
753
|
+
assert.equal(healed.code, 0, healed.stderr)
|
|
754
|
+
assert.equal(sh(b, "git", "ls-remote", "--heads", "origin", `task/claim/clm-${keys.one}`), "")
|
|
755
|
+
},
|
|
756
|
+
)
|