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