@pmelab/gtd 59.0.0 → 60.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +42 -38
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
- package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +1101 -1025
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +371 -82
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +68 -61
- package/src/cli/Cli.test.ts +311 -229
- package/src/cli/Cli.ts +486 -585
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +131 -82
- package/src/cli/Program.ts +197 -111
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +25 -6
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +481 -98
- package/src/gherkin/messages.test.ts +124 -0
- package/src/gherkin/messages.ts +101 -0
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +289 -6
- package/src/testkit/TestRepo.ts +317 -41
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +7 -14
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Land.ts +1 -1
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +175 -152
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
|
@@ -3,25 +3,28 @@ import { describe, expect, it } from "vitest"
|
|
|
3
3
|
import { planStart, type StartOutcome } from "./planStart.js"
|
|
4
4
|
import { InMemRepo, testLayers } from "../twins/index.js"
|
|
5
5
|
import { ConfigService } from "../workflow/index.js"
|
|
6
|
+
import { GtdUsageError } from "../platform/index.js"
|
|
6
7
|
|
|
7
|
-
const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
|
|
8
|
+
const WORKFLOW = `import { agent, human, workflow } from "@pmelab/gtd/flows"
|
|
8
9
|
|
|
9
10
|
export default async () => {
|
|
10
11
|
await human("idle", { message: "hi" })
|
|
11
12
|
await agent("work", "work-prompt")
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export const working =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const defaults = { base: "" }
|
|
15
|
+
export const working = workflow(
|
|
16
|
+
async () => {
|
|
17
|
+
await agent("work", "work-prompt")
|
|
18
|
+
},
|
|
19
|
+
{ defaults: { base: "" } },
|
|
20
|
+
)
|
|
23
21
|
|
|
24
|
-
export const
|
|
22
|
+
export const reviewcheck = workflow(
|
|
23
|
+
async () => {
|
|
24
|
+
await agent("work", "work-prompt")
|
|
25
|
+
},
|
|
26
|
+
{ defaults: { base: "" }, base: (_workflow, vars) => vars.base ?? "" },
|
|
27
|
+
)
|
|
25
28
|
`
|
|
26
29
|
|
|
27
30
|
const repoAt = (): InMemRepo => {
|
|
@@ -85,15 +88,14 @@ describe("planStart", () => {
|
|
|
85
88
|
})
|
|
86
89
|
})
|
|
87
90
|
|
|
88
|
-
it("
|
|
91
|
+
it("fails an undeclared --var name as a usage error", async () => {
|
|
89
92
|
const repo = repoAt()
|
|
90
|
-
const
|
|
93
|
+
const failure = await start(repo, "idle", "human", {
|
|
91
94
|
workflow: "working",
|
|
92
95
|
commandLabel: "gtd test",
|
|
93
96
|
vars: { nope: "x" },
|
|
94
|
-
})
|
|
95
|
-
expect(
|
|
96
|
-
expect(plan.kind === "refusal" && plan.message).toContain("not declared by this workflow")
|
|
97
|
+
}).catch((e: unknown) => e)
|
|
98
|
+
expect(String(failure)).toContain(`${GtdUsageError.name}: gtd test: --var name(s) not declared`)
|
|
97
99
|
})
|
|
98
100
|
|
|
99
101
|
describe("a workflow's diff base", () => {
|
package/src/step/planStart.ts
CHANGED
|
@@ -40,7 +40,7 @@ const checkOverrides = (
|
|
|
40
40
|
varOverrides: Record<string, string>,
|
|
41
41
|
declaredNames: readonly string[],
|
|
42
42
|
envNames: readonly string[],
|
|
43
|
-
): Effect.Effect<
|
|
43
|
+
): Effect.Effect<void, GtdUsageError> =>
|
|
44
44
|
Effect.gen(function* () {
|
|
45
45
|
const environmentOnly = Object.keys(varOverrides).filter(
|
|
46
46
|
(n) => !declaredNames.includes(n) && envNames.includes(n),
|
|
@@ -56,17 +56,18 @@ const checkOverrides = (
|
|
|
56
56
|
}
|
|
57
57
|
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n))
|
|
58
58
|
if (undeclared.length > 0) {
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
return yield* Effect.fail(
|
|
60
|
+
new GtdUsageError(
|
|
61
|
+
`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(
|
|
62
|
+
", ",
|
|
63
|
+
)} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`,
|
|
64
|
+
),
|
|
63
65
|
)
|
|
64
66
|
}
|
|
65
|
-
return undefined
|
|
66
67
|
})
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
|
-
* `gtd
|
|
70
|
+
* `gtd start <name>`: start a brand NEW process on a workflow, writing an
|
|
70
71
|
* opening commit that names the workflow's first step and carries a
|
|
71
72
|
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
72
73
|
* a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
|
|
@@ -102,13 +103,12 @@ export const planStart = (
|
|
|
102
103
|
if ("refusal" in first) return refusal(`${commandLabel}: ${first.refusal}`)
|
|
103
104
|
const { firstStep } = first
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
yield* checkOverrides(
|
|
106
107
|
commandLabel,
|
|
107
108
|
varOverrides,
|
|
108
109
|
Object.keys(workflow.vars),
|
|
109
110
|
Object.keys(workflow.env),
|
|
110
111
|
)
|
|
111
|
-
if (overrideRefusal !== undefined) return overrideRefusal
|
|
112
112
|
|
|
113
113
|
const baseOf = workflow.def.base
|
|
114
114
|
const flags = yield* StartFlags
|
|
@@ -15,14 +15,27 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
agent,
|
|
17
17
|
check,
|
|
18
|
+
envSettings as env,
|
|
18
19
|
human,
|
|
20
|
+
judge,
|
|
19
21
|
moveScript,
|
|
20
22
|
read,
|
|
21
23
|
removeScript,
|
|
22
24
|
run,
|
|
23
25
|
showScript,
|
|
26
|
+
settings as vars,
|
|
27
|
+
workflow,
|
|
24
28
|
} from "../flows/index.js"
|
|
25
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
inTurn,
|
|
31
|
+
runsItsScript,
|
|
32
|
+
testRepo,
|
|
33
|
+
verdict,
|
|
34
|
+
verdictWhen,
|
|
35
|
+
whenPrompted,
|
|
36
|
+
type ScriptedActor,
|
|
37
|
+
type TestRepo,
|
|
38
|
+
} from "./TestRepo.js"
|
|
26
39
|
|
|
27
40
|
const tiny = async (): Promise<void> => {
|
|
28
41
|
await agent("plan", "Write PLAN.md.")
|
|
@@ -57,8 +70,8 @@ describe("testRepo", () => {
|
|
|
57
70
|
it("runs a workflow given as gtd.config.ts source", async () => {
|
|
58
71
|
const repo = testRepo({
|
|
59
72
|
workflow: [
|
|
60
|
-
`import { agent, human } from "@pmelab/gtd/flows"`,
|
|
61
|
-
`export const chores = async () => { await agent("sweep", "Sweep."); await human("done") }`,
|
|
73
|
+
`import { agent, human, workflow } from "@pmelab/gtd/flows"`,
|
|
74
|
+
`export const chores = workflow(async () => { await agent("sweep", "Sweep."); await human("done") })`,
|
|
62
75
|
`export default chores`,
|
|
63
76
|
].join("\n"),
|
|
64
77
|
})
|
|
@@ -75,7 +88,7 @@ describe("testRepo", () => {
|
|
|
75
88
|
await human("done")
|
|
76
89
|
}
|
|
77
90
|
const repo = testRepo({
|
|
78
|
-
workflow: { default: tidy, tidy },
|
|
91
|
+
workflow: { default: tidy, tidy: workflow(tidy) },
|
|
79
92
|
files: { "NOTES.md": "notes\n", "scratch.txt": "x\n" },
|
|
80
93
|
})
|
|
81
94
|
await repo.start("tidy")
|
|
@@ -201,12 +214,56 @@ describe("testRepo", () => {
|
|
|
201
214
|
let n = 0
|
|
202
215
|
await expect(
|
|
203
216
|
repo.drive({ spin: (_, r) => r.write("n", `${++n}\n`) }, { maxBeats: 5 }),
|
|
204
|
-
).rejects.toThrow(
|
|
217
|
+
).rejects.toThrow(
|
|
218
|
+
"did not rest within 5 beats; it passed through:\nspin → spin → spin → spin → spin",
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe("judge verdicts", () => {
|
|
223
|
+
const judged = async (): Promise<void> => {
|
|
224
|
+
await agent("plan", "Plan.")
|
|
225
|
+
const { answers } = await judge("ready", {
|
|
226
|
+
questions: [{ id: "ready", primitive: "noul", instructions: "Ready?", criteria: "" }],
|
|
227
|
+
evidence: { plan: "p" },
|
|
228
|
+
})
|
|
229
|
+
await human(answers.ready?.answer === "yes" ? "ship" : "rework")
|
|
230
|
+
}
|
|
231
|
+
const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
|
|
232
|
+
const template =
|
|
233
|
+
'expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:\n[{ "id": "ready", "answer": true, "p": 0.9 }]'
|
|
234
|
+
|
|
235
|
+
it("echoes a refused verdict and the shape it should have", async () => {
|
|
236
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
237
|
+
repo.write("go", "\n")
|
|
238
|
+
const error = await repo
|
|
239
|
+
.drive({ plan, ready: () => '{"ready": true}' })
|
|
240
|
+
.catch((e: Error) => e)
|
|
241
|
+
expect((error as Error).message).toContain(
|
|
242
|
+
`the actor for judge step "ready" sent a verdict gtd refused:\n{"ready": true}\n${template}\ngtd judge answer exited 2\n`,
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("says an actor returned an object rather than no verdict", async () => {
|
|
247
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
248
|
+
repo.write("go", "\n")
|
|
249
|
+
const actor = (() => [{ id: "ready", answer: true, p: 0.9 }]) as unknown as ScriptedActor
|
|
250
|
+
await expect(repo.drive({ plan, ready: actor })).rejects.toThrow(
|
|
251
|
+
`the actor for judge step "ready" returned an array, not a JSON string — JSON.stringify it\n${template}`,
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("says an actor returned nothing", async () => {
|
|
256
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
257
|
+
repo.write("go", "\n")
|
|
258
|
+
await expect(repo.drive({ plan, ready: () => undefined })).rejects.toThrow(
|
|
259
|
+
`the actor for judge step "ready" returned no verdict\n${template}`,
|
|
260
|
+
)
|
|
261
|
+
})
|
|
205
262
|
})
|
|
206
263
|
|
|
207
264
|
describe("canned commands", () => {
|
|
208
265
|
const checked = async (): Promise<void> => {
|
|
209
|
-
const green = await check("test", "npm test",
|
|
266
|
+
const green = await check("test", "npm test", ".gtd/FEEDBACK.md")
|
|
210
267
|
await human(green ? "sign-off" : "red")
|
|
211
268
|
}
|
|
212
269
|
|
|
@@ -310,4 +367,230 @@ describe("testRepo", () => {
|
|
|
310
367
|
expect(() => repo.ref("refs/tags/v1", "nowhere")).toThrow("resolves to no commit")
|
|
311
368
|
})
|
|
312
369
|
})
|
|
370
|
+
|
|
371
|
+
describe("modules a config imports", () => {
|
|
372
|
+
it("serves a specifier the config's source imports, read off the module the test imported", async () => {
|
|
373
|
+
const shipped = { tiny, chores: tiny }
|
|
374
|
+
const repo = testRepo({
|
|
375
|
+
workflow: [
|
|
376
|
+
`import { workflow } from "@pmelab/gtd/flows"`,
|
|
377
|
+
`import tidy, * as all from "#workflow"`,
|
|
378
|
+
`export { chores } from "#workflow"`,
|
|
379
|
+
`export const both = all.tiny === tidy ? workflow(() => tidy()) : undefined`,
|
|
380
|
+
`export default tidy`,
|
|
381
|
+
].join("\n"),
|
|
382
|
+
modules: { "#workflow": { ...shipped, default: tiny } },
|
|
383
|
+
})
|
|
384
|
+
await repo.start("both")
|
|
385
|
+
expect((await repo.drive({ plan: (_, r) => r.write("PLAN.md", "p\n") })).state).toBe("build")
|
|
386
|
+
expect(repo.read("gtd.config.ts")).not.toContain('from "#workflow"')
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
describe("strict drives", () => {
|
|
391
|
+
const signOff = async (): Promise<void> => {
|
|
392
|
+
await agent("plan", "Plan.")
|
|
393
|
+
await human("sign-off")
|
|
394
|
+
await agent("ship", "Ship.")
|
|
395
|
+
}
|
|
396
|
+
const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
|
|
397
|
+
|
|
398
|
+
it("fails on an actor whose step the drive never reached", async () => {
|
|
399
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
400
|
+
repo.write("go", "\n")
|
|
401
|
+
await expect(repo.drive({ plan, ship: () => undefined })).rejects.toThrow(
|
|
402
|
+
'strict drive: the process rests at "sign-off", and never reached the actors for "ship"',
|
|
403
|
+
)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it("leaves only the unreached-actor check out with unreachedActors: pass", async () => {
|
|
407
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
408
|
+
repo.write("go", "\n")
|
|
409
|
+
expect(
|
|
410
|
+
(await repo.drive({ plan, ship: () => undefined }, { unreachedActors: "pass" })).state,
|
|
411
|
+
).toBe("sign-off")
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("lets the same drive rest when it is not strict", async () => {
|
|
415
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
416
|
+
repo.write("go", "\n")
|
|
417
|
+
expect((await repo.drive({ plan, ship: () => undefined }, { strict: false })).state).toBe(
|
|
418
|
+
"sign-off",
|
|
419
|
+
)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it("counts a step an earlier drive reached as reached", async () => {
|
|
423
|
+
const twice = async (): Promise<void> => {
|
|
424
|
+
await signOff()
|
|
425
|
+
await human("done")
|
|
426
|
+
}
|
|
427
|
+
const repo = testRepo({ workflow: { default: twice } })
|
|
428
|
+
repo.write("go", "\n")
|
|
429
|
+
const actors: Record<string, ScriptedActor> = { plan }
|
|
430
|
+
expect((await repo.drive(actors)).state).toBe("sign-off")
|
|
431
|
+
repo.write("signed", "\n")
|
|
432
|
+
await repo.land()
|
|
433
|
+
actors["ship"] = (_, r) => r.write("shipped", "\n")
|
|
434
|
+
expect((await repo.drive(actors)).state).toBe("done")
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it("fails on a human step whose actor changes nothing, instead of repeating it", async () => {
|
|
438
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
439
|
+
repo.write("go", "\n")
|
|
440
|
+
await expect(repo.drive({ plan, "sign-off": () => undefined })).rejects.toThrow(
|
|
441
|
+
'strict drive: the actor for human step "sign-off" changed nothing, so nothing landed',
|
|
442
|
+
)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it("fails on a verdict returned at a step that is no judge", async () => {
|
|
446
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
447
|
+
repo.write("go", "\n")
|
|
448
|
+
await expect(repo.drive({ plan: (beat, r) => (plan(beat, r), "[]") })).rejects.toThrow(
|
|
449
|
+
'the actor for agent step "plan" returned a verdict',
|
|
450
|
+
)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it("fails on a check step without an actor, unless checks run themselves", async () => {
|
|
454
|
+
const checked = async (): Promise<void> => {
|
|
455
|
+
const green = await check("test", "npm test", ".gtd/FEEDBACK.md")
|
|
456
|
+
await human(green ? "sign-off" : "red")
|
|
457
|
+
}
|
|
458
|
+
const strictRepo = testRepo({ workflow: { default: checked } })
|
|
459
|
+
strictRepo.write("go", "\n")
|
|
460
|
+
await expect(strictRepo.drive({})).rejects.toThrow(
|
|
461
|
+
'strict drive: check step "test" has no actor',
|
|
462
|
+
)
|
|
463
|
+
const repo = testRepo({
|
|
464
|
+
workflow: { default: checked },
|
|
465
|
+
commands: [{ run: "npm test", exit: 1, output: "1 failing" }],
|
|
466
|
+
})
|
|
467
|
+
repo.write("go", "\n")
|
|
468
|
+
expect((await repo.drive({}, { checks: "run" })).state).toBe("red")
|
|
469
|
+
expect(repo.read(".gtd/FEEDBACK.md")).toContain("1 failing")
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
describe("actor helpers", () => {
|
|
474
|
+
const judged = async (): Promise<void> => {
|
|
475
|
+
await agent("plan", "Plan.")
|
|
476
|
+
const { answers } = await judge("ready", {
|
|
477
|
+
questions: [{ id: "ready", primitive: "noul", instructions: "Ready?", criteria: "" }],
|
|
478
|
+
evidence: { plan: "p" },
|
|
479
|
+
})
|
|
480
|
+
await human(answers.ready?.answer === "yes" ? "ship" : "rework")
|
|
481
|
+
}
|
|
482
|
+
const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
|
|
483
|
+
|
|
484
|
+
it("builds a verdict keyed by question id, p defaulting to 0.9", () => {
|
|
485
|
+
expect(verdict({ ready: true, tier: { answer: "low", p: 0.6 } })).toEqual({
|
|
486
|
+
answers: [
|
|
487
|
+
{ id: "ready", answer: true, p: 0.9 },
|
|
488
|
+
{ id: "tier", answer: "low", p: 0.6 },
|
|
489
|
+
],
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it("answers a judge with a verdict object", async () => {
|
|
494
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
495
|
+
repo.write("go", "\n")
|
|
496
|
+
expect((await repo.drive({ plan, ready: () => verdict({ ready: false }) })).state).toBe(
|
|
497
|
+
"rework",
|
|
498
|
+
)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it("answers by what the pending questions contain", async () => {
|
|
502
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
503
|
+
repo.write("go", "\n")
|
|
504
|
+
const ready = inTurn([
|
|
505
|
+
verdictWhen("Unrelated?", verdict({ ready: false })),
|
|
506
|
+
verdictWhen("Ready?", verdict({ ready: true })),
|
|
507
|
+
])
|
|
508
|
+
expect((await repo.drive({ plan, ready })).state).toBe("ship")
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it("runs an actor only at a visit prompted with the text, and every actor in turn", async () => {
|
|
512
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
513
|
+
repo.write("go", "\n")
|
|
514
|
+
const seen: string[] = []
|
|
515
|
+
await repo.drive({
|
|
516
|
+
plan: inTurn([
|
|
517
|
+
whenPrompted("Plan.", plan),
|
|
518
|
+
whenPrompted("Something else.", () => {
|
|
519
|
+
seen.push("else")
|
|
520
|
+
}),
|
|
521
|
+
() => {
|
|
522
|
+
seen.push("always")
|
|
523
|
+
},
|
|
524
|
+
]),
|
|
525
|
+
})
|
|
526
|
+
expect(repo.read("PLAN.md")).toBe("p\n")
|
|
527
|
+
expect(seen).toEqual(["always"])
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it("runs a script with commands of its own before the canned ones", () => {
|
|
531
|
+
const repo = testRepo({ workflow: { default: tiny }, commands: [{ run: /.*/, exit: 1 }] })
|
|
532
|
+
repo.runScript("make docs\n", {
|
|
533
|
+
commands: [{ run: "make docs", effect: (r) => r.write("docs.html", "<p>\n") }],
|
|
534
|
+
})
|
|
535
|
+
expect(repo.read("docs.html")).toBe("<p>\n")
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
describe("commits and the environment", () => {
|
|
540
|
+
it("lists every commit with its subject, message and trailers", async () => {
|
|
541
|
+
const repo = testRepo({ workflow: { default: tiny } })
|
|
542
|
+
repo.write("IDEA.md", "x\n")
|
|
543
|
+
repo.commit("feat: x\n\nA body.\n\nRefs: #1\nRefs: #2")
|
|
544
|
+
const [initial, feat] = repo.commits()
|
|
545
|
+
expect(initial).toMatchObject({ subject: "chore: initial commit", trailers: {} })
|
|
546
|
+
expect(feat).toEqual({
|
|
547
|
+
hash: repo.hashes()[1],
|
|
548
|
+
subject: "feat: x",
|
|
549
|
+
message: repo.messages()[1],
|
|
550
|
+
trailers: { Refs: ["#1", "#2"] },
|
|
551
|
+
})
|
|
552
|
+
await repo.drive({ plan: (_, r) => r.write("PLAN.md", "p\n") })
|
|
553
|
+
expect(repo.commits().at(-1)!.trailers["Gtd-Step"]).toEqual(["plan#1"])
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const greeter = {
|
|
557
|
+
default: workflow(
|
|
558
|
+
async (): Promise<void> => {
|
|
559
|
+
await agent("wake", "Wake up.")
|
|
560
|
+
await agent("greet", `Say ${env["greeting"]}, flag ${vars["flag_probe"]}.`)
|
|
561
|
+
},
|
|
562
|
+
{ envDefaults: { greeting: "hi" }, defaults: { flag_probe: "0" } },
|
|
563
|
+
),
|
|
564
|
+
}
|
|
565
|
+
const greet = async (repo: TestRepo): Promise<string> => {
|
|
566
|
+
const rest = await repo.drive({ wake: (_, r) => r.write("awake", "\n") })
|
|
567
|
+
return rest.content
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
it("changes a variable gtd sees from the next call on", async () => {
|
|
571
|
+
const repo = testRepo({ workflow: greeter })
|
|
572
|
+
repo.write("go", "\n")
|
|
573
|
+
expect(await greet(repo)).toContain("Say hi")
|
|
574
|
+
repo.setEnv("GTD_GREETING", "hello")
|
|
575
|
+
expect((await repo.next()).content).toContain("Say hello")
|
|
576
|
+
repo.setEnv("GTD_GREETING", undefined)
|
|
577
|
+
expect((await repo.next()).content).toContain("Say hi")
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it("keeps the runner's GTD_FLAG_* out when ambient flags are off", async () => {
|
|
581
|
+
const saved = process.env["GTD_FLAG_PROBE"]
|
|
582
|
+
process.env["GTD_FLAG_PROBE"] = "1"
|
|
583
|
+
try {
|
|
584
|
+
const ambient = testRepo({ workflow: greeter })
|
|
585
|
+
ambient.write("go", "\n")
|
|
586
|
+
expect(await greet(ambient)).toContain("flag 1")
|
|
587
|
+
const isolated = testRepo({ workflow: greeter, ambientFlags: false })
|
|
588
|
+
isolated.write("go", "\n")
|
|
589
|
+
expect(await greet(isolated)).toContain("flag 0")
|
|
590
|
+
} finally {
|
|
591
|
+
if (saved === undefined) delete process.env["GTD_FLAG_PROBE"]
|
|
592
|
+
else process.env["GTD_FLAG_PROBE"] = saved
|
|
593
|
+
}
|
|
594
|
+
})
|
|
595
|
+
})
|
|
313
596
|
})
|