@pmelab/gtd 59.0.1 → 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 +37 -33
- 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-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +751 -645
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +245 -36
- 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 +66 -60
- package/src/cli/Cli.test.ts +183 -182
- package/src/cli/Cli.ts +164 -186
- 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 +65 -79
- package/src/cli/Program.ts +157 -118
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +3 -3
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +22 -5
- 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 +370 -51
- package/src/gherkin/messages.test.ts +38 -4
- package/src/gherkin/messages.ts +21 -4
- 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 +245 -7
- package/src/testkit/TestRepo.ts +271 -40
- 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 +2 -6
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- 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 +169 -146
- 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
package/dist/steering.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as parseThreads, C as
|
|
2
|
-
export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX,
|
|
1
|
+
import { A as parseThreads, C as FREE_TEXT_PLACEHOLDER, D as findingEntries, E as NOISE_HEADING, N as formatFinding, O as openThreadFindings, S as unansweredNotes, T as FIX_BOX, _ as reviewChunks, a as sections, b as reviewRisks, c as openThreads, d as freeFormFormat, f as applySteeringEdits, g as REVIEW_SECTIONS, h as ANSWER_WONT_FIX, i as sectionBodies, j as parseThreadsWithFindings, k as parseFootnotes, l as steeringFormatFor, m as ANSWER_LEAD, n as stripCodeThreads, o as threads, p as ANSWER_FIX, r as openQuestions, t as parseCodeThreads, v as reviewDecisions, w as isAnswered, x as reviewSections, y as reviewNotes } from "./chunks/steering-DF_BMadH.mjs";
|
|
2
|
+
export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, FIX_BOX, FREE_TEXT_PLACEHOLDER, NOISE_HEADING, REVIEW_SECTIONS, applySteeringEdits, findingEntries, formatFinding, freeFormFormat, isAnswered, openQuestions, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewChunks, reviewDecisions, reviewNotes, reviewRisks, reviewSections, sectionBodies, sections, steeringFormatFor, stripCodeThreads, threads, unansweredNotes };
|
package/dist/testkit/gherkin.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { n as runsItsScript, r as testRepo } from "../chunks/testkit-BvecVjQ0.mjs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import assert from "node:assert";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { Given, QuickPickleWorld, Then, When, setWorldConstructor } from "quickpickle";
|
|
5
|
+
import { After, AfterAll, Given, QuickPickleWorld, Then, When, setWorldConstructor } from "quickpickle";
|
|
6
6
|
//#region src/gherkin/messages.ts
|
|
7
7
|
const CONTENT_LINES = 5;
|
|
8
8
|
const reason = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -21,6 +21,7 @@ const beatLines = async (repo) => {
|
|
|
21
21
|
return [`unknown — gtd next failed: ${reason(error)}`];
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
+
const unreachedList = (steps) => steps.map(({ step, origin }) => `"${step}" (${origin})`).join(", ");
|
|
24
25
|
/** Where the process rests, for a failed outcome to end with. Never throws: it describes a failure. */
|
|
25
26
|
const restDescription = async (repo, neverUsed) => {
|
|
26
27
|
let lines;
|
|
@@ -34,7 +35,7 @@ const restDescription = async (repo, neverUsed) => {
|
|
|
34
35
|
} catch (error) {
|
|
35
36
|
lines = [`unknown — ${reason(error)}`];
|
|
36
37
|
}
|
|
37
|
-
if (neverUsed.length > 0) lines.push(`actors never used: ${neverUsed
|
|
38
|
+
if (neverUsed.length > 0) lines.push(`actors never used: ${unreachedList(neverUsed)}`);
|
|
38
39
|
return ["where the process rests:", ...lines.map((line) => ` ${line}`)].join("\n");
|
|
39
40
|
};
|
|
40
41
|
/** Both sides of a failed `contains`: the text looked for, and what was there. */
|
|
@@ -58,9 +59,14 @@ const assertContains = (where, text, content) => {
|
|
|
58
59
|
const assertLacks = (where, text, content) => {
|
|
59
60
|
if (content === void 0 || content.includes(text)) assert.fail(containsFailure(where, text, content, false));
|
|
60
61
|
};
|
|
62
|
+
/** A rest at `step`; a stall there is no rest, so it points to the step that asserts one. */
|
|
63
|
+
const assertRestsAt = (beat, step) => {
|
|
64
|
+
if (beat.kind === "stalled") assert.fail(`expected a rest at "${step}", but the process stalls at "${beat.state}" — assert a stall with: the process stalls at "${beat.state}"`);
|
|
65
|
+
assert.strictEqual(beat.state, step);
|
|
66
|
+
};
|
|
61
67
|
//#endregion
|
|
62
68
|
//#region src/gherkin/index.ts
|
|
63
|
-
const
|
|
69
|
+
const BACKGROUND_ACTORS = "gtdBackgroundActors";
|
|
64
70
|
var TestkitWorld = class extends QuickPickleWorld {
|
|
65
71
|
constructor(context, info) {
|
|
66
72
|
super(context, info);
|
|
@@ -68,11 +74,15 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
68
74
|
gtdConfig = void 0;
|
|
69
75
|
env = {};
|
|
70
76
|
commands = [];
|
|
71
|
-
|
|
77
|
+
modules = {};
|
|
72
78
|
actions = /* @__PURE__ */ new Map();
|
|
73
79
|
verdicts = /* @__PURE__ */ new Map();
|
|
74
|
-
/** Per judge step, a verdict for each text its questions may contain; the first match answers. */
|
|
75
80
|
conditionalVerdicts = /* @__PURE__ */ new Map();
|
|
81
|
+
prompts = /* @__PURE__ */ new Map();
|
|
82
|
+
used = /* @__PURE__ */ new Set();
|
|
83
|
+
origins = /* @__PURE__ */ new Map();
|
|
84
|
+
backgroundScripted = /* @__PURE__ */ new Set();
|
|
85
|
+
excused = /* @__PURE__ */ new Set();
|
|
76
86
|
created = void 0;
|
|
77
87
|
configure(config) {
|
|
78
88
|
this.assertNotCreated("a gtd config");
|
|
@@ -92,6 +102,19 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
92
102
|
this.assertNotCreated(`the environment variable ${name}`);
|
|
93
103
|
this.env[name] = value;
|
|
94
104
|
}
|
|
105
|
+
/** Unlike `setEnv`, also once the repository exists: gtd sees it from its next call on. */
|
|
106
|
+
changeEnv(name, value) {
|
|
107
|
+
if (this.created === void 0) this.env[name] = value;
|
|
108
|
+
else this.created.setEnv(name, value);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Lets a config given as source import `module` as `specifier`. A support
|
|
112
|
+
* file may call it for every scenario, e.g. from a `Before` hook.
|
|
113
|
+
*/
|
|
114
|
+
importable(specifier, module) {
|
|
115
|
+
this.assertNotCreated(`the module ${specifier}`);
|
|
116
|
+
this.modules[specifier] = module;
|
|
117
|
+
}
|
|
95
118
|
assertNotCreated(what) {
|
|
96
119
|
if (this.created !== void 0) throw new Error(`${what} must be set before any step that touches the repository`);
|
|
97
120
|
}
|
|
@@ -100,16 +123,70 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
100
123
|
return this.created ??= testRepo({
|
|
101
124
|
workflow: this.gtdConfig,
|
|
102
125
|
env: this.env,
|
|
103
|
-
commands: this.commands
|
|
126
|
+
commands: this.commands,
|
|
127
|
+
modules: this.modules
|
|
104
128
|
});
|
|
105
129
|
}
|
|
130
|
+
scripting(step, replace = false) {
|
|
131
|
+
const fromBackground = (this.info.stepIdx ?? 0) < 0;
|
|
132
|
+
if (fromBackground) this.backgroundScripted.add(step);
|
|
133
|
+
if (!fromBackground || replace || !this.origins.has(step)) this.origins.set(step, fromBackground ? "Background" : "scenario");
|
|
134
|
+
}
|
|
135
|
+
/** Adds `action` to what `step`'s actor does at every visit. */
|
|
106
136
|
act(step, action) {
|
|
137
|
+
this.scripting(step);
|
|
107
138
|
this.actions.set(step, [...this.actions.get(step) ?? [], action]);
|
|
108
139
|
}
|
|
109
|
-
|
|
140
|
+
/** Drops every action and prompt queued for `step`, leaving `action` alone. */
|
|
141
|
+
rescript(step, action) {
|
|
142
|
+
this.scripting(step, true);
|
|
143
|
+
this.actions.set(step, [action]);
|
|
144
|
+
this.prompts.delete(step);
|
|
145
|
+
}
|
|
146
|
+
/** Drops every action and prompt queued for `step`, so a drive rests there. */
|
|
147
|
+
unscript(step) {
|
|
148
|
+
this.actions.delete(step);
|
|
149
|
+
this.prompts.delete(step);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* `step`'s beat content must contain `text`. A scripted step is checked at
|
|
153
|
+
* every visit, before its actor acts; a step with nothing else scripted is
|
|
154
|
+
* not scripted at all, so a drive rests there and checks it at the rest.
|
|
155
|
+
*/
|
|
156
|
+
expectPrompt(step, text) {
|
|
157
|
+
this.scripting(step);
|
|
158
|
+
this.prompts.set(step, [...this.prompts.get(step) ?? [], text]);
|
|
159
|
+
}
|
|
160
|
+
/** Judge step `step` answers `verdict`, a JSON array. */
|
|
161
|
+
answer(step, verdict) {
|
|
162
|
+
this.scripting(step);
|
|
163
|
+
this.verdicts.set(step, verdict);
|
|
164
|
+
}
|
|
165
|
+
/** Judge step `step` answers `verdict` when its questions contain `text`; the first match answers. */
|
|
166
|
+
answerWhen(step, text, verdict) {
|
|
167
|
+
this.scripting(step);
|
|
168
|
+
this.conditionalVerdicts.set(step, [...this.conditionalVerdicts.get(step) ?? [], [text, verdict]]);
|
|
169
|
+
}
|
|
170
|
+
scripted() {
|
|
171
|
+
return /* @__PURE__ */ new Set([
|
|
172
|
+
...this.actions.keys(),
|
|
173
|
+
...this.verdicts.keys(),
|
|
174
|
+
...this.conditionalVerdicts.keys(),
|
|
175
|
+
...this.prompts.keys()
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
/** The steps the scenario scripted, or expects a prompt at, that no drive has reached. */
|
|
179
|
+
neverUsed() {
|
|
180
|
+
return [...this.scripted()].filter((step) => !this.used.has(step)).map((step) => ({
|
|
181
|
+
step,
|
|
182
|
+
origin: this.origins.get(step) ?? "scenario"
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
checkPrompts(step, content) {
|
|
186
|
+
for (const text of this.prompts.get(step) ?? []) if (!content.includes(text)) throw new Error(`step "${step}" was not prompted with "${text}":\n${content}`);
|
|
187
|
+
}
|
|
188
|
+
actors() {
|
|
110
189
|
const actors = {};
|
|
111
|
-
const used = usedActors.get(this) ?? /* @__PURE__ */ new Set();
|
|
112
|
-
usedActors.set(this, used);
|
|
113
190
|
for (const [step, actions] of this.actions) actors[step] = async (beat, repo) => {
|
|
114
191
|
for (const action of actions) await action(beat, repo);
|
|
115
192
|
};
|
|
@@ -126,32 +203,88 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
126
203
|
return hit[1];
|
|
127
204
|
};
|
|
128
205
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
206
|
+
return actors;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* `strict: false` lets what a strict drive refuses pass. Every drive leaves
|
|
210
|
+
* scripted steps it never reached to `assertReached`: an earlier step may
|
|
211
|
+
* script one for a later drive.
|
|
212
|
+
*/
|
|
213
|
+
async drive(options = {}) {
|
|
214
|
+
try {
|
|
215
|
+
const actors = this.actors();
|
|
216
|
+
const tracked = {};
|
|
217
|
+
for (const [step, actor] of Object.entries(actors)) tracked[step] = (beat, repo) => {
|
|
218
|
+
this.used.add(step);
|
|
219
|
+
this.checkPrompts(step, beat.content);
|
|
220
|
+
return actor(beat, repo);
|
|
221
|
+
};
|
|
222
|
+
const rest = await this.repo().drive(tracked, {
|
|
223
|
+
...options,
|
|
224
|
+
unreachedActors: "pass"
|
|
225
|
+
});
|
|
226
|
+
if (rest.kind !== "stalled" && this.prompts.has(rest.state) && !(rest.state in actors)) {
|
|
227
|
+
this.used.add(rest.state);
|
|
228
|
+
this.checkPrompts(rest.state, rest.content);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
for (const step of this.scripted()) this.excused.add(step);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Fails on a step a scenario step scripted, or expects a prompt at, that no
|
|
237
|
+
* drive reached. Run at scenario end. A Background's steps are left to the
|
|
238
|
+
* feature's end, and the steps a refused drive was handed are excused.
|
|
239
|
+
*/
|
|
240
|
+
async assertReached() {
|
|
241
|
+
const never = this.neverUsed();
|
|
242
|
+
const dead = never.filter(({ step, origin }) => origin === "scenario" && !this.excused.has(step));
|
|
243
|
+
if (dead.length === 0) return;
|
|
244
|
+
throw new Error(`at scenario end, no drive reached the steps scripted for ${unreachedList(dead)} — script a step only for a drive that reaches it, or in the Background when only some scenarios reach it\n\n${await restDescription(() => this.repo(), never)}`);
|
|
245
|
+
}
|
|
246
|
+
/** Records, for the feature's end, which Background-scripted steps this scenario reached. */
|
|
247
|
+
recordBackground() {
|
|
248
|
+
const record = this.common[BACKGROUND_ACTORS] ??= {
|
|
249
|
+
reached: /* @__PURE__ */ new Map(),
|
|
250
|
+
file: void 0
|
|
133
251
|
};
|
|
134
|
-
|
|
252
|
+
record.file ??= this.context?.task?.file;
|
|
253
|
+
const failed = this.info.errors.length > 0;
|
|
254
|
+
for (const step of this.backgroundScripted) record.reached.set(step, record.reached.get(step) === true || failed || this.used.has(step));
|
|
135
255
|
}
|
|
136
256
|
};
|
|
257
|
+
After(async (world) => {
|
|
258
|
+
if (!(world instanceof TestkitWorld)) return;
|
|
259
|
+
world.recordBackground();
|
|
260
|
+
if (world.info.errors.length === 0) await world.assertReached();
|
|
261
|
+
});
|
|
262
|
+
const allRan = (tasks) => tasks.every((task) => {
|
|
263
|
+
const t = task;
|
|
264
|
+
if (t.tasks !== void 0) return allRan(t.tasks);
|
|
265
|
+
return t.mode === "run" && t.result?.state !== "skip";
|
|
266
|
+
});
|
|
267
|
+
AfterAll((common) => {
|
|
268
|
+
const record = common[BACKGROUND_ACTORS];
|
|
269
|
+
if (record === void 0) return;
|
|
270
|
+
if (record.file?.tasks !== void 0 && !allRan(record.file.tasks)) return;
|
|
271
|
+
const dead = [...record.reached].filter(([, reached]) => !reached);
|
|
272
|
+
if (dead.length === 0) return;
|
|
273
|
+
throw new Error(`${common.info.feature}: no scenario reached the steps scripted for ${unreachedList(dead.map(([step]) => ({
|
|
274
|
+
step,
|
|
275
|
+
origin: "Background"
|
|
276
|
+
})))} — script a step in the Background only when some scenario reaches it`);
|
|
277
|
+
});
|
|
137
278
|
setWorldConstructor(TestkitWorld);
|
|
138
279
|
const withNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
139
|
-
const args = (list) => list.
|
|
140
|
-
const neverUsed = (world) => {
|
|
141
|
-
const used = usedActors.get(world) ?? /* @__PURE__ */ new Set();
|
|
142
|
-
return [.../* @__PURE__ */ new Set([
|
|
143
|
-
...world.actions.keys(),
|
|
144
|
-
...world.verdicts.keys(),
|
|
145
|
-
...world.conditionalVerdicts.keys()
|
|
146
|
-
])].filter((step) => !used.has(step));
|
|
147
|
-
};
|
|
280
|
+
const args = (list) => [...list.matchAll(/(["'])(.*?)\1(?=\s|$)|\S+/g)].map(([word, quote, inner]) => quote === void 0 ? word : inner);
|
|
148
281
|
/** An outcome step: a failure ends with where the process rests. */
|
|
149
282
|
const outcome = (check) => async (world, ...rest) => {
|
|
150
283
|
try {
|
|
151
284
|
await check(world, ...rest);
|
|
152
285
|
} catch (error) {
|
|
153
286
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
154
|
-
failure.message += `\n\n${await restDescription(() => world.repo(), neverUsed(
|
|
287
|
+
failure.message += `\n\n${await restDescription(() => world.repo(), world.neverUsed())}`;
|
|
155
288
|
throw failure;
|
|
156
289
|
}
|
|
157
290
|
};
|
|
@@ -164,9 +297,15 @@ Given("a gtd config from the module {string}", async (world, path) => {
|
|
|
164
297
|
Given("the gtd config ends with:", (world, source) => {
|
|
165
298
|
world.extendConfig(withNewline(source));
|
|
166
299
|
});
|
|
167
|
-
Given("
|
|
300
|
+
Given("the environment variable {string} is {string}", (world, name, value) => {
|
|
168
301
|
world.setEnv(name, value);
|
|
169
302
|
});
|
|
303
|
+
Given("from now on the environment variable {string} is {string}", (world, name, value) => {
|
|
304
|
+
world.changeEnv(name, value);
|
|
305
|
+
});
|
|
306
|
+
Given("the module {string} is importable as {string}", async (world, path, specifier) => {
|
|
307
|
+
world.importable(specifier, { ...await import(pathToFileURL(resolve(path)).href) });
|
|
308
|
+
});
|
|
170
309
|
Given("the command {string} exits {int}", (world, run, exit) => {
|
|
171
310
|
world.command({
|
|
172
311
|
run,
|
|
@@ -180,9 +319,25 @@ Given("the command {string} exits {int} with:", (world, run, exit, output) => {
|
|
|
180
319
|
output: withNewline(output)
|
|
181
320
|
});
|
|
182
321
|
});
|
|
322
|
+
Given("the command matching {string} exits {int}", (world, pattern, exit) => {
|
|
323
|
+
world.command({
|
|
324
|
+
run: new RegExp(pattern),
|
|
325
|
+
exit
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
Given("the command matching {string} exits {int} with:", (world, pattern, exit, output) => {
|
|
329
|
+
world.command({
|
|
330
|
+
run: new RegExp(pattern),
|
|
331
|
+
exit,
|
|
332
|
+
output: withNewline(output)
|
|
333
|
+
});
|
|
334
|
+
});
|
|
183
335
|
Given("a branch {string}", (world, name) => {
|
|
184
336
|
world.repo().branch(name);
|
|
185
337
|
});
|
|
338
|
+
Given("a branch {string} at {string}", (world, name, ref) => {
|
|
339
|
+
world.repo().branch(name, ref);
|
|
340
|
+
});
|
|
186
341
|
Given("{string} is checked out", (world, name) => {
|
|
187
342
|
world.repo().checkout(name);
|
|
188
343
|
});
|
|
@@ -200,7 +355,7 @@ Given("the text {string} in {string} is replaced with {string}", (world, text, p
|
|
|
200
355
|
assert.ok(content.includes(text), `${path}: ${content}`);
|
|
201
356
|
world.repo().write(path, content.replace(text, () => replacement));
|
|
202
357
|
});
|
|
203
|
-
Given("{string}
|
|
358
|
+
Given("text is appended to {string}:", (world, path, text) => {
|
|
204
359
|
const content = world.repo().read(path) ?? "";
|
|
205
360
|
world.repo().write(path, `${content.trimEnd()}\n\n${text}\n`);
|
|
206
361
|
});
|
|
@@ -214,10 +369,10 @@ Given("step {string} changes nothing", (world, step) => {
|
|
|
214
369
|
world.act(step, () => void 0);
|
|
215
370
|
});
|
|
216
371
|
Given("step {string} from now on changes nothing", (world, step) => {
|
|
217
|
-
world.
|
|
372
|
+
world.rescript(step, () => void 0);
|
|
218
373
|
});
|
|
219
374
|
Given("step {string} is no longer scripted", (world, step) => {
|
|
220
|
-
world.
|
|
375
|
+
world.unscript(step);
|
|
221
376
|
});
|
|
222
377
|
Given("step {string} writes {string} with:", (world, step, path, content) => {
|
|
223
378
|
world.act(step, (_, repo) => repo.write(path, withNewline(content)));
|
|
@@ -243,11 +398,9 @@ Given("step {string} deletes {string}", (world, step, path) => {
|
|
|
243
398
|
world.act(step, (_, repo) => repo.remove(path));
|
|
244
399
|
});
|
|
245
400
|
Given("step {string} is prompted with {string}", (world, step, text) => {
|
|
246
|
-
world.
|
|
247
|
-
if (!beat.content.includes(text)) throw new Error(`step "${step}" was not prompted with "${text}":\n${beat.content}`);
|
|
248
|
-
});
|
|
401
|
+
world.expectPrompt(step, text);
|
|
249
402
|
});
|
|
250
|
-
Given("step {string}
|
|
403
|
+
Given("step {string} overwrites {string}, if it exists, with:", (world, step, path, content) => {
|
|
251
404
|
world.act(step, (_, repo) => {
|
|
252
405
|
if (repo.read(path) !== void 0) repo.write(path, withNewline(content));
|
|
253
406
|
});
|
|
@@ -255,11 +408,30 @@ Given("step {string} rewrites {string} with:", (world, step, path, content) => {
|
|
|
255
408
|
Given("step {string} runs its script", (world, step) => {
|
|
256
409
|
world.act(step, runsItsScript);
|
|
257
410
|
});
|
|
411
|
+
/** A check step's script, every command it runs answering as `command` does. */
|
|
412
|
+
const checkRuns = (world, step, command) => {
|
|
413
|
+
world.act(step, (beat, repo) => {
|
|
414
|
+
if (beat.actor !== "check") throw new Error(`step "${step}" is no check step; its actor is ${beat.actor}`);
|
|
415
|
+
repo.runScript(beat.content, { commands: [{
|
|
416
|
+
...command,
|
|
417
|
+
run: /[\s\S]*/
|
|
418
|
+
}] });
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
Given("check step {string} passes", (world, step) => {
|
|
422
|
+
checkRuns(world, step, {});
|
|
423
|
+
});
|
|
424
|
+
Given("check step {string} fails with:", (world, step, output) => {
|
|
425
|
+
checkRuns(world, step, {
|
|
426
|
+
exit: 1,
|
|
427
|
+
output: withNewline(output)
|
|
428
|
+
});
|
|
429
|
+
});
|
|
258
430
|
Given("judge step {string} answers:", (world, step, verdict) => {
|
|
259
|
-
world.
|
|
431
|
+
world.answer(step, String(verdict));
|
|
260
432
|
});
|
|
261
433
|
Given("judge step {string}, when its questions contain {string}, answers:", (world, step, text, verdict) => {
|
|
262
|
-
world.
|
|
434
|
+
world.answerWhen(step, text, String(verdict));
|
|
263
435
|
});
|
|
264
436
|
When("gtd starts workflow {string}", async (world, workflow) => {
|
|
265
437
|
await world.repo().start(workflow);
|
|
@@ -279,6 +451,9 @@ When("gtd lands", async (world) => {
|
|
|
279
451
|
When("the process is driven", async (world) => {
|
|
280
452
|
await world.drive();
|
|
281
453
|
});
|
|
454
|
+
When("the process is driven leniently", async (world) => {
|
|
455
|
+
await world.drive({ strict: false });
|
|
456
|
+
});
|
|
282
457
|
Then("opening door {string} is refused with {string}", outcome(async (world, door, message) => {
|
|
283
458
|
await refusedWith(world.repo().door(door), message);
|
|
284
459
|
}));
|
|
@@ -296,7 +471,26 @@ Then("driving the process is refused without {string}", outcome(async (world, me
|
|
|
296
471
|
if (got.includes(message)) assert.fail(`expected a refusal without "${message}", but it was refused with:\n${got}`);
|
|
297
472
|
}));
|
|
298
473
|
Then("the process rests at {string}", outcome(async (world, step) => {
|
|
299
|
-
|
|
474
|
+
assertRestsAt(await world.repo().next(), step);
|
|
475
|
+
}));
|
|
476
|
+
Then("the process stalls at {string}", outcome(async (world, step) => {
|
|
477
|
+
const beat = await world.repo().next();
|
|
478
|
+
if (beat.kind !== "stalled" || beat.state !== step) assert.fail(`expected a stall at "${step}", but the process rests at "${beat.state}" (${beat.kind})`);
|
|
479
|
+
}));
|
|
480
|
+
/** A beat field by its dotted path (`model`, `access.write`), as text: a string as is, anything else as JSON. */
|
|
481
|
+
const beatField = async (world, path) => {
|
|
482
|
+
let value = await world.repo().next();
|
|
483
|
+
for (const key of path.split(".")) value = typeof value === "object" && value !== null ? value[key] : void 0;
|
|
484
|
+
return value;
|
|
485
|
+
};
|
|
486
|
+
const asText = (value) => typeof value === "string" ? value : JSON.stringify(value) ?? "undefined";
|
|
487
|
+
Then("the resting step's {word} is {string}", outcome(async (world, path, expected) => {
|
|
488
|
+
const value = asText(await beatField(world, path));
|
|
489
|
+
if (value !== expected) assert.fail(`the resting step's ${path} is not "${expected}"; it is:\n${value}`);
|
|
490
|
+
}));
|
|
491
|
+
Then("the resting step's {word} includes {string}", outcome(async (world, path, expected) => {
|
|
492
|
+
const value = await beatField(world, path);
|
|
493
|
+
if (!(Array.isArray(value) ? value.some((item) => asText(item) === expected) : typeof value === "string" && value.includes(expected))) assert.fail(`the resting step's ${path} does not include "${expected}"; it is:\n${asText(value)}`);
|
|
300
494
|
}));
|
|
301
495
|
Then("the resting step's content contains {string}", outcome(async (world, text) => {
|
|
302
496
|
assertContains("the resting step's content", text, (await world.repo().next()).content);
|
|
@@ -317,6 +511,18 @@ Then("the commit subjects end with:", outcome((world, list) => {
|
|
|
317
511
|
const expected = list.split("\n").filter((line) => line.trim() !== "");
|
|
318
512
|
assert.deepStrictEqual(world.repo().subjects().slice(-expected.length), expected);
|
|
319
513
|
}));
|
|
514
|
+
Then("the last commit body contains {string}", outcome((world, text) => {
|
|
515
|
+
assertContains("the last commit's message", text, world.repo().messages().at(-1));
|
|
516
|
+
}));
|
|
517
|
+
Then("the commit {string} has a body containing {string}", outcome((world, subject, text) => {
|
|
518
|
+
const matching = world.repo().commits().filter((commit) => commit.subject === subject);
|
|
519
|
+
assert.ok(matching.length > 0, `no commit "${subject}"`);
|
|
520
|
+
for (const { message } of matching) assertContains(`the commit "${subject}"`, text, message);
|
|
521
|
+
}));
|
|
522
|
+
Then("no commit body contains {string}", outcome((world, text) => {
|
|
523
|
+
const hit = world.repo().messages().find((message) => message.includes(text));
|
|
524
|
+
if (hit !== void 0) assert.fail(`a commit's message contains "${text}":\n${hit}`);
|
|
525
|
+
}));
|
|
320
526
|
const outputOf = async (world, list) => {
|
|
321
527
|
const result = await world.repo().gtd(...args(list));
|
|
322
528
|
assert.strictEqual(result.exitCode, 0, result.stderr);
|
|
@@ -347,6 +553,9 @@ Then("{string} contains the short hash of the commit {string}", outcome((world,
|
|
|
347
553
|
assert.ok(hash !== void 0, `no commit "${subject}"`);
|
|
348
554
|
assertContains(`"${path}"`, hash.slice(0, 7), repo.read(path));
|
|
349
555
|
}));
|
|
556
|
+
Then("{string} as committed at {string} contains {string}", outcome((world, path, ref, text) => {
|
|
557
|
+
assertContains(`"${path}" at ${ref}`, text, world.repo().show(ref, path));
|
|
558
|
+
}));
|
|
350
559
|
Then("{string} contains {string} exactly once", outcome((world, path, text) => {
|
|
351
560
|
const content = world.repo().read(path) ?? "";
|
|
352
561
|
const times = content.split(text).length - 1;
|
package/dist/testkit.mjs
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { H as installContext } from "./chunks/flows-DENt0uWV.mjs";
|
|
2
|
+
import { a as verdictWhen, i as verdict, n as runsItsScript, o as whenPrompted, r as testRepo, t as inTurn } from "./chunks/testkit-BvecVjQ0.mjs";
|
|
3
|
+
export { inTurn, installContext, runsItsScript, testRepo, verdict, verdictWhen, whenPrompted };
|