@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +120 -395
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-07Z_ENch.mjs} +13503 -3460
- package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +484 -497
- package/dist/testkit/gherkin.mjs +145 -65
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +34 -27
- package/src/cli/Cli.test.ts +148 -71
- package/src/cli/Cli.ts +384 -463
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/Program.test.ts +127 -33
- package/src/cli/Program.ts +70 -25
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/flows/factory.ts +3 -1
- package/src/gherkin/index.ts +135 -71
- package/src/gherkin/messages.test.ts +90 -0
- package/src/gherkin/messages.ts +84 -0
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +47 -2
- package/src/testkit/TestRepo.ts +51 -6
- package/src/twins/EmittedScriptRecognizer.ts +5 -8
- package/src/twins/FlowScripts.ts +1 -1
- package/src/wire/Land.ts +1 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +7 -7
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- 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
|
-
})
|