@pmelab/gtd 58.1.0 → 59.0.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.
Files changed (142) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +30 -2
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/gherkin/git-control.feature +78 -0
  17. package/src/gherkin/index.ts +35 -1
  18. package/src/install/Install.test.ts +14 -11
  19. package/src/install/Install.ts +38 -15
  20. package/src/install/index.ts +1 -2
  21. package/src/replay/Replay.test.ts +1 -1
  22. package/src/replay/index.ts +1 -1
  23. package/src/step/planStart.ts +6 -1
  24. package/src/testkit/TestRepo.test.ts +118 -2
  25. package/src/testkit/TestRepo.ts +57 -6
  26. package/src/testkit/index.ts +1 -0
  27. package/src/twins/EmittedScriptRecognizer.ts +12 -14
  28. package/src/twins/FlowScripts.ts +10 -9
  29. package/src/twins/InMemRepo.ts +26 -1
  30. package/src/twins/Layers.ts +12 -1
  31. package/src/twins/index.ts +6 -1
  32. package/src/workflow/index.ts +1 -0
  33. package/src/workflow/load.test.ts +138 -113
  34. package/src/workflow/load.ts +1 -1
  35. package/src/workflow/vars.test.ts +30 -3
  36. package/src/workflow/vars.ts +34 -2
  37. package/src/workflow-kit/index.ts +1 -1
  38. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  39. package/dist/chunks/review-Bfaror38.mjs +0 -427
  40. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  41. package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
  42. package/dist/workflows/cleanup.mjs +0 -36
  43. package/dist/workflows/feature.mjs +0 -1018
  44. package/dist/workflows/fix.mjs +0 -41
  45. package/dist/workflows/review.mjs +0 -45
  46. package/dist/workflows/rollout.mjs +0 -48
  47. package/dist/workflows.mjs +0 -40651
  48. package/src/install/ConfigScaffold.ts +0 -22
  49. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  50. package/src/workflows/cleanup/cleanup.feature +0 -315
  51. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  52. package/src/workflows/cleanup/cleanup.ts +0 -52
  53. package/src/workflows/cleanup/index.ts +0 -6
  54. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  55. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  56. package/src/workflows/feature/criteria-coverage.feature +0 -102
  57. package/src/workflows/feature/criteria.test.ts +0 -147
  58. package/src/workflows/feature/criteria.ts +0 -267
  59. package/src/workflows/feature/fast-path.feature +0 -93
  60. package/src/workflows/feature/feature.test.ts +0 -338
  61. package/src/workflows/feature/feature.ts +0 -156
  62. package/src/workflows/feature/green.ts +0 -19
  63. package/src/workflows/feature/index.ts +0 -18
  64. package/src/workflows/feature/lens-models.feature +0 -69
  65. package/src/workflows/feature/lens-review.feature +0 -566
  66. package/src/workflows/feature/naive-acceptance.feature +0 -147
  67. package/src/workflows/feature/naive-test.feature +0 -263
  68. package/src/workflows/feature/one-step-land.feature +0 -93
  69. package/src/workflows/feature/planning.ts +0 -41
  70. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  71. package/src/workflows/feature/refactor-first.feature +0 -158
  72. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  73. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  74. package/src/workflows/feature/refactor-review.feature +0 -1528
  75. package/src/workflows/feature/refactor.test.ts +0 -1220
  76. package/src/workflows/feature/refactor.ts +0 -368
  77. package/src/workflows/feature/release-flag.feature +0 -167
  78. package/src/workflows/feature/release-flag.test.ts +0 -49
  79. package/src/workflows/feature/release-flag.ts +0 -46
  80. package/src/workflows/feature/review-gate.feature +0 -241
  81. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  82. package/src/workflows/feature/risk-tier.feature +0 -351
  83. package/src/workflows/feature/seam-rejection.feature +0 -146
  84. package/src/workflows/feature/seam.feature +0 -297
  85. package/src/workflows/feature/seam.test.ts +0 -527
  86. package/src/workflows/feature/seam.ts +0 -426
  87. package/src/workflows/feature/standards.feature +0 -354
  88. package/src/workflows/feature/steps.test.ts +0 -65
  89. package/src/workflows/feature/steps.ts +0 -45
  90. package/src/workflows/feature/tier-pin.feature +0 -66
  91. package/src/workflows/feature/tier.test.ts +0 -166
  92. package/src/workflows/feature/tier.ts +0 -56
  93. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  94. package/src/workflows/fix/fix-quality.feature +0 -212
  95. package/src/workflows/fix/fix.feature +0 -79
  96. package/src/workflows/fix/fix.test.ts +0 -74
  97. package/src/workflows/fix/fix.ts +0 -52
  98. package/src/workflows/fix/index.ts +0 -6
  99. package/src/workflows/index.ts +0 -47
  100. package/src/workflows/review/index.ts +0 -6
  101. package/src/workflows/review/review-door.feature +0 -199
  102. package/src/workflows/review/review.test.ts +0 -27
  103. package/src/workflows/review/review.ts +0 -60
  104. package/src/workflows/rollout/index.ts +0 -6
  105. package/src/workflows/rollout/rollout.feature +0 -510
  106. package/src/workflows/rollout/rollout.test.ts +0 -263
  107. package/src/workflows/rollout/rollout.ts +0 -82
  108. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  109. package/src/workflows/shared/access.test.ts +0 -62
  110. package/src/workflows/shared/access.ts +0 -24
  111. package/src/workflows/shared/config.fixture.ts +0 -12
  112. package/src/workflows/shared/fences.ts +0 -24
  113. package/src/workflows/shared/health.test.ts +0 -148
  114. package/src/workflows/shared/health.ts +0 -191
  115. package/src/workflows/shared/learn.test.ts +0 -140
  116. package/src/workflows/shared/learn.ts +0 -201
  117. package/src/workflows/shared/lenses.test.ts +0 -39
  118. package/src/workflows/shared/lenses.ts +0 -294
  119. package/src/workflows/shared/options.test.ts +0 -157
  120. package/src/workflows/shared/options.ts +0 -92
  121. package/src/workflows/shared/progress.feature +0 -124
  122. package/src/workflows/shared/prose.ts +0 -263
  123. package/src/workflows/shared/recheck.feature +0 -60
  124. package/src/workflows/shared/registry.test.ts +0 -345
  125. package/src/workflows/shared/registry.ts +0 -183
  126. package/src/workflows/shared/reserved.ts +0 -11
  127. package/src/workflows/shared/review.test.ts +0 -1062
  128. package/src/workflows/shared/review.ts +0 -446
  129. package/src/workflows/shared/skills.test.ts +0 -121
  130. package/src/workflows/shared/skills.ts +0 -49
  131. package/src/workflows/shared/standards.test.ts +0 -90
  132. package/src/workflows/shared/standards.ts +0 -156
  133. package/src/workflows/shared/steps.test.ts +0 -219
  134. package/src/workflows/shared/steps.ts +0 -201
  135. package/src/workflows/shared/tail.test.ts +0 -454
  136. package/src/workflows/shared/tail.ts +0 -264
  137. package/src/workflows/shared/text.fixture.ts +0 -128
  138. package/src/workflows/shared/text.test.ts +0 -1430
  139. package/src/workflows/shared/text.ts +0 -1700
  140. package/src/workflows/shared/threads.test.ts +0 -129
  141. package/src/workflows/shared/vars.ts +0 -47
  142. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,454 +0,0 @@
1
- import { afterEach, describe, expect, it } from "vitest"
2
- import {
3
- installContext,
4
- type Change,
5
- type CodeThreadInfo,
6
- type StepRequest,
7
- } from "../../flows/index.js"
8
- import type { Guard } from "./health.js"
9
- import { handOffReview, qualityLap, REVIEW_ORIG, tail } from "./tail.js"
10
- import { fixtureContext } from "./text.fixture.js"
11
-
12
- afterEach(() => installContext(undefined))
13
-
14
- const REVIEW = ".gtd/REVIEW.md"
15
- const reviewDoc = (note: string): string =>
16
- [
17
- "# Review: abc1234",
18
- "",
19
- "<!-- base: 0000000 -->",
20
- "",
21
- "## calc",
22
- "- [ ] ./src/calc.ts#1",
23
- note,
24
- "",
25
- ].join("\n")
26
-
27
- /**
28
- * Run `handOffReview` with the human leaving `after` at the gate; `"deleted"`
29
- * has the human delete the review, `"unwritten"` has the reviewer write none.
30
- * Returns the steps and scripts, and the message of what it threw.
31
- */
32
- const handOff = async (
33
- after: string | "deleted" | "unwritten",
34
- extra: {
35
- paths?: readonly string[]
36
- codeThreads?: readonly CodeThreadInfo[]
37
- guard?: Guard
38
- } = {},
39
- ) => {
40
- const log: string[] = []
41
- const gates: boolean[] = []
42
- const scripts = new Map<string, string>()
43
- const files = new Map<string, string>()
44
- const onReviewing = () => {
45
- if (log.filter((n) => n === "review.reviewing").length > 1) throw new Error("re-review")
46
- if (after !== "unwritten") files.set(REVIEW, reviewDoc("new add function"))
47
- }
48
- const onGate = (request: StepRequest) => {
49
- if (request.kind === "human") gates.push(request.options.acceptClean === true)
50
- if (after === "deleted") files.delete(REVIEW)
51
- else if (after !== "unwritten") files.set(REVIEW, after)
52
- }
53
- installContext(
54
- fixtureContext(
55
- { codeThreads: extra.codeThreads ?? [] },
56
- {
57
- step: (request: StepRequest) => {
58
- if (request.kind === "restart") return Promise.resolve()
59
- log.push(request.name)
60
- if (request.kind === "run") scripts.set(request.name, request.body)
61
- if (request.name === "review.reviewing") onReviewing()
62
- if (request.name === "review.await-review") onGate(request)
63
- return Promise.resolve()
64
- },
65
- refuse: (message: string): never => {
66
- throw new Error(message)
67
- },
68
- pushScope: () => undefined,
69
- popScope: () => undefined,
70
- read: (path) => files.get(path),
71
- matches: (path, pattern) => path === pattern,
72
- changes: (): readonly Change[] =>
73
- after === "deleted"
74
- ? [
75
- {
76
- path: REVIEW,
77
- status: "deleted",
78
- before: reviewDoc("new add function"),
79
- after: undefined,
80
- },
81
- ]
82
- : [],
83
- changesSince: (): readonly Change[] => [
84
- {
85
- path: REVIEW,
86
- status: "modified",
87
- before: reviewDoc("new add function"),
88
- after: files.get(REVIEW),
89
- },
90
- ...(extra.paths ?? []).map(
91
- (path): Change => ({ path, status: "modified", before: "a", after: "b" }),
92
- ),
93
- ],
94
- head: () => "c1",
95
- start: () => "c0",
96
- },
97
- ),
98
- )
99
- const refusal = await handOffReview("base", { rounds: 0 }, extra.guard).then(
100
- () => undefined,
101
- (e: unknown) => (e as Error).message,
102
- )
103
- return { log, scripts, refusal, gates }
104
- }
105
-
106
- describe("the hand-off review", () => {
107
- it("no note signs off and closes the review", async () => {
108
- const run = await handOff(reviewDoc("new add function"))
109
- expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
110
- })
111
-
112
- it("the review gate accepts a clean landing", async () => {
113
- const run = await handOff(reviewDoc("new add function"))
114
- expect(run.gates).toEqual([true])
115
- })
116
-
117
- it("a note ends the round with the review kept and the reviewer's version copied beside it", async () => {
118
- const run = await handOff(reviewDoc("new add function — rename it to sum"))
119
- expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.handing-off"])
120
- expect(run.scripts.get("review.handing-off")).toContain(
121
- `git show 'c1:${REVIEW}' > '${REVIEW_ORIG}'`,
122
- )
123
- })
124
-
125
- it("an edit that parses to no note (a deleted chunk) is refused, not handed to a fix with nothing to read", async () => {
126
- const run = await handOff("# Review: abc1234\n\n<!-- base: 0000000 -->\n")
127
- expect(run.refusal).toMatch(/contains no note fix can read/)
128
- expect(run.log).toEqual(["review.reviewing", "review.await-review"])
129
- })
130
-
131
- it("a hand edit in a sign-off round keeps the suite green before closing", async () => {
132
- const run = await handOff(reviewDoc("new add function"), { paths: ["src/calc.ts"] })
133
- expect(run.log).toEqual([
134
- "review.reviewing",
135
- "review.await-review",
136
- "health.check",
137
- "review.closing",
138
- ])
139
- })
140
-
141
- it("a change under .gtd/ alone does not run the green check", async () => {
142
- const run = await handOff(reviewDoc("new add function"), { paths: [".gtd/OTHER.md"] })
143
- expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
144
- })
145
-
146
- it("a code thread waiting on the agent is refused, not signed off", async () => {
147
- const run = await handOff(reviewDoc("new add function"), {
148
- codeThreads: [
149
- { path: "src/calc.ts", line: 1, waitingOn: "agent", first: "why?", faults: [] },
150
- ],
151
- })
152
- expect(run.refusal).toMatch(/nothing answers it here/)
153
- })
154
-
155
- it("a thread waiting on the human at the gate is refused", async () => {
156
- const run = await handOff(reviewDoc("new add function"), {
157
- codeThreads: [
158
- { path: "src/calc.ts", line: 1, waitingOn: "human", first: "why?", faults: [] },
159
- ],
160
- })
161
- expect(run.refusal).toMatch(/wait on you/)
162
- })
163
-
164
- it("a round the reviewer left no review for rests at review-missing, then reviews again", async () => {
165
- const run = await handOff("unwritten")
166
- expect(run.log).toEqual([
167
- "review.reviewing",
168
- "review.await-review",
169
- "review.review-missing",
170
- "review.reviewing",
171
- ])
172
- })
173
-
174
- it("a review the human deleted is refused, not re-reviewed", async () => {
175
- const run = await handOff("deleted")
176
- expect(run.refusal).toMatch(/was deleted — restore it, or leave a note to hand to fix/)
177
- expect(run.log).toEqual(["review.reviewing", "review.await-review"])
178
- })
179
-
180
- describe("guard", () => {
181
- it("runs after the reviewing turn and never after the human's await-review landing", async () => {
182
- const seen: string[] = []
183
- const run = await handOff(reviewDoc("new add function"), {
184
- guard: () => seen.push("guard"),
185
- })
186
- expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
187
- expect(seen).toEqual(["guard"])
188
- })
189
-
190
- it("a refusal after reviewing stops the round before the human gate", async () => {
191
- const run = await handOff(reviewDoc("new add function"), {
192
- guard: () => {
193
- throw new Error("guarded: nope")
194
- },
195
- })
196
- expect(run.refusal).toBe("guarded: nope")
197
- expect(run.log).toEqual(["review.reviewing"])
198
- })
199
-
200
- it("does not run after review-missing", async () => {
201
- let calls = 0
202
- const run = await handOff("unwritten", { guard: () => void calls++ })
203
- expect(run.log).toContain("review.review-missing")
204
- // the fixture aborts the second reviewing turn: only the first was guarded
205
- expect(calls).toBe(1)
206
- })
207
- })
208
- })
209
-
210
- const prompts: string[] = []
211
- const models: (string | undefined)[] = []
212
-
213
- describe("the quality lap's lenses", () => {
214
- const lap = async (
215
- run: () => Promise<unknown>,
216
- vars: Record<string, string> = {},
217
- files: Record<string, string> = {},
218
- ) => {
219
- const log: string[] = []
220
- prompts.length = 0
221
- models.length = 0
222
- installContext(
223
- fixtureContext(
224
- { vars, read: (path) => files[path] },
225
- {
226
- step: (request: StepRequest) => {
227
- if (request.kind !== "restart") log.push(request.name)
228
- if (request.kind === "agent") {
229
- prompts.push(request.prompt)
230
- models.push(request.options.model)
231
- }
232
- return Promise.resolve()
233
- },
234
- pushScope: () => undefined,
235
- popScope: () => undefined,
236
- start: () => "START-SHA",
237
- },
238
- ),
239
- )
240
- await run()
241
- return log
242
- }
243
-
244
- it("default to the qualityReviews setting", async () => {
245
- const log = await lap(() => qualityLap(), { qualityReviews: "correctness, conventions" })
246
- expect(log).toEqual(["quality.correctness.reviewing", "quality.conventions.reviewing"])
247
- })
248
-
249
- it("lenses override qualityReviews", async () => {
250
- const log = await lap(() => qualityLap(undefined, ["ponytail-review"]), {
251
- qualityReviews: "correctness, conventions",
252
- })
253
- expect(log).toEqual(["quality.ponytail-review.reviewing"])
254
- })
255
-
256
- it("an empty list runs no lens turn", async () => {
257
- const log = await lap(() => qualityLap(undefined, []), { qualityReviews: "correctness" })
258
- expect(log).toEqual([])
259
- })
260
-
261
- it("lensBase reaches every lens turn's prompt; absent, they name start()", async () => {
262
- await lap(() => qualityLap(undefined, ["conventions", "correctness"], "BASE-SHA"))
263
- expect(prompts).toHaveLength(2)
264
- for (const prompt of prompts) expect(prompt).toContain("from `BASE-SHA`")
265
- await lap(() => qualityLap(undefined, ["conventions", "correctness"]))
266
- expect(prompts).toHaveLength(2)
267
- for (const prompt of prompts) expect(prompt).toContain("from `START-SHA`")
268
- })
269
-
270
- it("tail threads lensBase to every lens turn", async () => {
271
- await lap(() =>
272
- tail(() => Promise.resolve(), {
273
- green: true,
274
- lenses: ["conventions", "correctness"],
275
- lensBase: "BASE-SHA",
276
- }),
277
- )
278
- expect(prompts).toHaveLength(2)
279
- for (const prompt of prompts) expect(prompt).toContain("from `BASE-SHA`")
280
- })
281
-
282
- it("the deprecated form threads lensBase from its seventh slot", async () => {
283
- await lap(() =>
284
- tail(false, () => Promise.resolve(), true, undefined, ["conventions"], true, "BASE-SHA", "R"),
285
- )
286
- expect(prompts).toHaveLength(1)
287
- expect(prompts[0]).toContain("from `BASE-SHA`")
288
- expect(prompts[0]).not.toContain("from `R`")
289
- })
290
-
291
- it("each lens runs on its qualityReviews hint, else its default, under its plain name", async () => {
292
- const log = await lap(() => qualityLap(), {
293
- qualityReviews: "correctness=base, conventions, my-lens",
294
- })
295
- expect(log).toEqual([
296
- "quality.correctness.reviewing",
297
- "quality.conventions.reviewing",
298
- "quality.my-lens.reviewing",
299
- ])
300
- expect(models).toEqual(["base", "base", "smart"])
301
- })
302
-
303
- it("lenses passed in carry their own hints, never qualityReviews'", async () => {
304
- const log = await lap(
305
- () =>
306
- tail(() => Promise.resolve(), {
307
- green: true,
308
- lenses: ["correctness", "conventions=opus"],
309
- }),
310
- { qualityReviews: "correctness=base" },
311
- )
312
- expect(log).toEqual(["quality.correctness.reviewing", "quality.conventions.reviewing"])
313
- expect(models).toEqual(["smart", "opus"])
314
- })
315
-
316
- it("criteria-coverage runs only with requirements, passed or in .gtd/REQUIREMENTS.md, and quotes them", async () => {
317
- const lenses = ["criteria-coverage", "conventions"]
318
- expect(await lap(() => qualityLap(undefined, lenses))).toEqual([
319
- "quality.conventions.reviewing",
320
- ])
321
- await lap(() => qualityLap(undefined, lenses), {}, { ".gtd/REQUIREMENTS.md": "- AC-1: file" })
322
- expect(prompts[0]).toContain(" > - AC-1: file")
323
- expect(prompts[1]).not.toContain("AC-1")
324
- await lap(() =>
325
- tail(() => Promise.resolve(), { green: true, lenses, requirements: "- AC-1: passed" }),
326
- )
327
- expect(prompts[0]).toContain(" > - AC-1: passed")
328
- })
329
-
330
- it("tail passes lenses to its lap and a guard after each lens turn", async () => {
331
- let guards = 0
332
- const log = await lap(() =>
333
- tail(() => Promise.resolve(), {
334
- green: true,
335
- guard: () => void guards++,
336
- lenses: ["conventions"],
337
- }),
338
- )
339
- expect(log).toEqual(["quality.conventions.reviewing"])
340
- expect(guards).toBe(1)
341
- })
342
- })
343
-
344
- describe("tail's call forms", () => {
345
- class Stop extends Error {}
346
- const requestNames = async (
347
- call: () => Promise<unknown>,
348
- names: string[] = [],
349
- ): Promise<string[]> => {
350
- installContext(
351
- fixtureContext(
352
- { vars: { qualityReviews: "correctness" } },
353
- {
354
- step: (request: StepRequest) => {
355
- if (request.kind === "restart") return Promise.resolve()
356
- names.push(`${request.kind}:${request.name}`)
357
- if (request.kind === "agent") prompts.push(request.prompt)
358
- if (names.length >= 4) throw new Stop()
359
- return Promise.resolve()
360
- },
361
- pushScope: () => undefined,
362
- popScope: () => undefined,
363
- start: () => "START-SHA",
364
- },
365
- ),
366
- )
367
- await call().catch((error: unknown) => {
368
- if (!(error instanceof Stop)) throw error
369
- })
370
- return names
371
- }
372
- const finish = () => Promise.resolve()
373
-
374
- for (const [fixFirst, green] of [
375
- [true, false],
376
- [true, true],
377
- [false, true],
378
- [false, false],
379
- ] as const) {
380
- it(`the deprecated positional form matches the options form for fixFirst ${fixFirst}, green ${green}`, async () => {
381
- const modern = await requestNames(() => tail(finish, { fixFirst, green }))
382
- const legacy = await requestNames(() => tail(fixFirst, finish, green))
383
- expect(legacy).toEqual(modern)
384
- })
385
- }
386
-
387
- it("the deprecated form passes every positional on", async () => {
388
- let guards = 0
389
- const guard = () => void guards++
390
- const modern = await requestNames(() =>
391
- tail(finish, { green: true, guard, lenses: ["conventions"], guardRests: false }),
392
- )
393
- const modernGuards = guards
394
- guards = 0
395
- const legacy = await requestNames(() =>
396
- tail(false, finish, true, guard, ["conventions"], false),
397
- )
398
- expect(legacy).toEqual(modern)
399
- expect(guards).toBe(modernGuards)
400
- })
401
-
402
- it("the deprecated form threads rules from its eighth slot", async () => {
403
- prompts.length = 0
404
- await requestNames(() =>
405
- tail(true, finish, false, undefined, [], true, "BASE-SHA", "RULES-TEXT"),
406
- )
407
- expect(prompts.some((p) => p.includes("RULES-TEXT"))).toBe(true)
408
- expect(prompts.some((p) => p.includes("BASE-SHA"))).toBe(false)
409
- })
410
-
411
- it("the deprecated form still runs untyped flags as truthy or falsy", async () => {
412
- expect(await requestNames(() => tail(1 as never, finish, 0 as never))).toEqual(
413
- await requestNames(() => tail(finish, { fixFirst: true })),
414
- )
415
- })
416
-
417
- it("reads an explicit undefined in every option as absent", async () => {
418
- const absent = await requestNames(() => tail(finish))
419
- const explicit = await requestNames(() =>
420
- tail(finish, {
421
- fixFirst: undefined,
422
- green: undefined,
423
- guard: undefined,
424
- lenses: undefined,
425
- guardRests: undefined,
426
- lensBase: undefined,
427
- rules: undefined,
428
- }),
429
- )
430
- expect(explicit).toEqual(absent)
431
- })
432
-
433
- const fitsNeither: [string, () => Promise<unknown>][] = [
434
- ["no finish", () => (tail as (...args: unknown[]) => Promise<unknown>)(false)],
435
- ["a finish followed by a boolean", () => tail(finish, true as never)],
436
- ["a finish followed by a function", () => tail(finish, finish as never)],
437
- ["a finish followed by a Promise", () => tail(finish, Promise.resolve({}) as never)],
438
- [
439
- "a finish with a third argument",
440
- () => (tail as (...args: unknown[]) => Promise<unknown>)(finish, {}, true),
441
- ],
442
- ]
443
- for (const [label, call] of fitsNeither) {
444
- it(`rejects ${label} before any step runs`, async () => {
445
- const names: string[] = []
446
- const rejected = expect(requestNames(call, names)).rejects
447
- await rejected.toThrow("tail(finish, options?)")
448
- await rejected.toThrow(
449
- "tail(fixFirst, finish, green?, guard?, lenses?, guardRests?, lensBase?, rules?)",
450
- )
451
- expect(names).toEqual([])
452
- })
453
- }
454
- })