@pmelab/gtd 58.2.0 → 59.0.1

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 (150) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +120 -395
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-07Z_ENch.mjs} +13503 -3460
  4. package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
  5. package/dist/flows.mjs +1 -1
  6. package/dist/gtd.bundle.mjs +484 -497
  7. package/dist/testkit/gherkin.mjs +145 -65
  8. package/dist/testkit.mjs +1 -1
  9. package/dist/wire.mjs +1 -1
  10. package/package.json +3 -42
  11. package/skills/authoring/SKILL.md +34 -27
  12. package/src/cli/Cli.test.ts +148 -71
  13. package/src/cli/Cli.ts +384 -463
  14. package/src/cli/Help.test.ts +47 -0
  15. package/src/cli/Help.ts +70 -0
  16. package/src/cli/Program.test.ts +127 -33
  17. package/src/cli/Program.ts +70 -25
  18. package/src/edge/Edge.ts +4 -4
  19. package/src/emit/Emit.test.ts +1 -1
  20. package/src/emit/Emit.ts +1 -1
  21. package/src/flags/index.test.ts +0 -13
  22. package/src/flags/index.ts +1 -3
  23. package/src/flows/factory.ts +3 -1
  24. package/src/gherkin/index.ts +135 -71
  25. package/src/gherkin/messages.test.ts +90 -0
  26. package/src/gherkin/messages.ts +84 -0
  27. package/src/install/Install.test.ts +14 -11
  28. package/src/install/Install.ts +38 -15
  29. package/src/install/index.ts +1 -2
  30. package/src/replay/Replay.test.ts +1 -1
  31. package/src/replay/index.ts +1 -1
  32. package/src/step/planStart.ts +6 -1
  33. package/src/testkit/TestRepo.test.ts +47 -2
  34. package/src/testkit/TestRepo.ts +51 -6
  35. package/src/twins/EmittedScriptRecognizer.ts +5 -8
  36. package/src/twins/FlowScripts.ts +1 -1
  37. package/src/wire/Land.ts +1 -1
  38. package/src/workflow/index.ts +1 -0
  39. package/src/workflow/load.test.ts +138 -113
  40. package/src/workflow/load.ts +7 -7
  41. package/src/workflow/skills.test.ts +5 -5
  42. package/src/workflow/skills.ts +1 -1
  43. package/src/workflow/vars.test.ts +30 -3
  44. package/src/workflow/vars.ts +34 -2
  45. package/src/workflow-kit/index.ts +1 -1
  46. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  47. package/dist/chunks/review-Bfaror38.mjs +0 -427
  48. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  49. package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
  50. package/dist/workflows/cleanup.mjs +0 -36
  51. package/dist/workflows/feature.mjs +0 -1018
  52. package/dist/workflows/fix.mjs +0 -41
  53. package/dist/workflows/review.mjs +0 -45
  54. package/dist/workflows/rollout.mjs +0 -48
  55. package/dist/workflows.mjs +0 -40651
  56. package/src/install/ConfigScaffold.ts +0 -22
  57. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  58. package/src/workflows/cleanup/cleanup.feature +0 -315
  59. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  60. package/src/workflows/cleanup/cleanup.ts +0 -52
  61. package/src/workflows/cleanup/index.ts +0 -6
  62. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  63. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  64. package/src/workflows/feature/criteria-coverage.feature +0 -102
  65. package/src/workflows/feature/criteria.test.ts +0 -147
  66. package/src/workflows/feature/criteria.ts +0 -267
  67. package/src/workflows/feature/fast-path.feature +0 -93
  68. package/src/workflows/feature/feature.test.ts +0 -338
  69. package/src/workflows/feature/feature.ts +0 -156
  70. package/src/workflows/feature/green.ts +0 -19
  71. package/src/workflows/feature/index.ts +0 -18
  72. package/src/workflows/feature/lens-models.feature +0 -69
  73. package/src/workflows/feature/lens-review.feature +0 -566
  74. package/src/workflows/feature/naive-acceptance.feature +0 -147
  75. package/src/workflows/feature/naive-test.feature +0 -263
  76. package/src/workflows/feature/one-step-land.feature +0 -93
  77. package/src/workflows/feature/planning.ts +0 -41
  78. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  79. package/src/workflows/feature/refactor-first.feature +0 -158
  80. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  81. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  82. package/src/workflows/feature/refactor-review.feature +0 -1528
  83. package/src/workflows/feature/refactor.test.ts +0 -1220
  84. package/src/workflows/feature/refactor.ts +0 -368
  85. package/src/workflows/feature/release-flag.feature +0 -167
  86. package/src/workflows/feature/release-flag.test.ts +0 -49
  87. package/src/workflows/feature/release-flag.ts +0 -46
  88. package/src/workflows/feature/review-gate.feature +0 -241
  89. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  90. package/src/workflows/feature/risk-tier.feature +0 -351
  91. package/src/workflows/feature/seam-rejection.feature +0 -146
  92. package/src/workflows/feature/seam.feature +0 -297
  93. package/src/workflows/feature/seam.test.ts +0 -527
  94. package/src/workflows/feature/seam.ts +0 -426
  95. package/src/workflows/feature/standards.feature +0 -354
  96. package/src/workflows/feature/steps.test.ts +0 -65
  97. package/src/workflows/feature/steps.ts +0 -45
  98. package/src/workflows/feature/tier-pin.feature +0 -66
  99. package/src/workflows/feature/tier.test.ts +0 -166
  100. package/src/workflows/feature/tier.ts +0 -56
  101. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  102. package/src/workflows/fix/fix-quality.feature +0 -212
  103. package/src/workflows/fix/fix.feature +0 -79
  104. package/src/workflows/fix/fix.test.ts +0 -74
  105. package/src/workflows/fix/fix.ts +0 -52
  106. package/src/workflows/fix/index.ts +0 -6
  107. package/src/workflows/index.ts +0 -47
  108. package/src/workflows/review/index.ts +0 -6
  109. package/src/workflows/review/review-door.feature +0 -199
  110. package/src/workflows/review/review.test.ts +0 -27
  111. package/src/workflows/review/review.ts +0 -60
  112. package/src/workflows/rollout/index.ts +0 -6
  113. package/src/workflows/rollout/rollout.feature +0 -510
  114. package/src/workflows/rollout/rollout.test.ts +0 -263
  115. package/src/workflows/rollout/rollout.ts +0 -82
  116. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  117. package/src/workflows/shared/access.test.ts +0 -62
  118. package/src/workflows/shared/access.ts +0 -24
  119. package/src/workflows/shared/config.fixture.ts +0 -12
  120. package/src/workflows/shared/fences.ts +0 -24
  121. package/src/workflows/shared/health.test.ts +0 -148
  122. package/src/workflows/shared/health.ts +0 -191
  123. package/src/workflows/shared/learn.test.ts +0 -140
  124. package/src/workflows/shared/learn.ts +0 -201
  125. package/src/workflows/shared/lenses.test.ts +0 -39
  126. package/src/workflows/shared/lenses.ts +0 -294
  127. package/src/workflows/shared/options.test.ts +0 -157
  128. package/src/workflows/shared/options.ts +0 -92
  129. package/src/workflows/shared/progress.feature +0 -124
  130. package/src/workflows/shared/prose.ts +0 -263
  131. package/src/workflows/shared/recheck.feature +0 -60
  132. package/src/workflows/shared/registry.test.ts +0 -345
  133. package/src/workflows/shared/registry.ts +0 -183
  134. package/src/workflows/shared/reserved.ts +0 -11
  135. package/src/workflows/shared/review.test.ts +0 -1062
  136. package/src/workflows/shared/review.ts +0 -446
  137. package/src/workflows/shared/skills.test.ts +0 -121
  138. package/src/workflows/shared/skills.ts +0 -49
  139. package/src/workflows/shared/standards.test.ts +0 -90
  140. package/src/workflows/shared/standards.ts +0 -156
  141. package/src/workflows/shared/steps.test.ts +0 -219
  142. package/src/workflows/shared/steps.ts +0 -201
  143. package/src/workflows/shared/tail.test.ts +0 -454
  144. package/src/workflows/shared/tail.ts +0 -264
  145. package/src/workflows/shared/text.fixture.ts +0 -128
  146. package/src/workflows/shared/text.test.ts +0 -1430
  147. package/src/workflows/shared/text.ts +0 -1700
  148. package/src/workflows/shared/threads.test.ts +0 -129
  149. package/src/workflows/shared/vars.ts +0 -47
  150. 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
- })