@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
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,9 +1,72 @@
|
|
|
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
|
+
//#region src/gherkin/messages.ts
|
|
7
|
+
const CONTENT_LINES = 5;
|
|
8
|
+
const reason = (error) => error instanceof Error ? error.message : String(error);
|
|
9
|
+
const beatLines = async (repo) => {
|
|
10
|
+
try {
|
|
11
|
+
const beat = await repo.next();
|
|
12
|
+
const content = beat.content.trimEnd().split("\n");
|
|
13
|
+
const more = content.length - CONTENT_LINES;
|
|
14
|
+
return [
|
|
15
|
+
`step "${beat.state}" (${beat.kind}, ${beat.actor}${beat.idle ? ", idle" : ""})`,
|
|
16
|
+
"content:",
|
|
17
|
+
...content.slice(0, CONTENT_LINES).map((line) => ` | ${line}`),
|
|
18
|
+
...more > 0 ? [` | … ${more} more lines`] : []
|
|
19
|
+
];
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return [`unknown — gtd next failed: ${reason(error)}`];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const unreachedList = (steps) => steps.map(({ step, origin }) => `"${step}" (${origin})`).join(", ");
|
|
25
|
+
/** Where the process rests, for a failed outcome to end with. Never throws: it describes a failure. */
|
|
26
|
+
const restDescription = async (repo, neverUsed) => {
|
|
27
|
+
let lines;
|
|
28
|
+
try {
|
|
29
|
+
const r = repo();
|
|
30
|
+
lines = [
|
|
31
|
+
...await beatLines(r),
|
|
32
|
+
"last commits:",
|
|
33
|
+
...r.subjects().slice(-5).map((subject) => ` ${subject}`)
|
|
34
|
+
];
|
|
35
|
+
} catch (error) {
|
|
36
|
+
lines = [`unknown — ${reason(error)}`];
|
|
37
|
+
}
|
|
38
|
+
if (neverUsed.length > 0) lines.push(`actors never used: ${unreachedList(neverUsed)}`);
|
|
39
|
+
return ["where the process rests:", ...lines.map((line) => ` ${line}`)].join("\n");
|
|
40
|
+
};
|
|
41
|
+
/** Both sides of a failed `contains`: the text looked for, and what was there. */
|
|
42
|
+
const containsFailure = (where, text, content, expected) => content === void 0 ? `${where} does not exist${expected ? `, so it cannot contain "${text}"` : ""}` : `${where} ${expected ? "does not contain" : "contains"} "${text}"; it holds:\n${content}`;
|
|
43
|
+
/** What `attempt` was refused with; fails when it was not refused at all. */
|
|
44
|
+
const refusal = async (attempt, expectation) => {
|
|
45
|
+
try {
|
|
46
|
+
await attempt;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return error instanceof Error ? error.message : String(error);
|
|
49
|
+
}
|
|
50
|
+
assert.fail(`expected a refusal ${expectation}, but nothing was refused`);
|
|
51
|
+
};
|
|
52
|
+
const refusedWith = async (attempt, message) => {
|
|
53
|
+
const got = await refusal(attempt, `with "${message}"`);
|
|
54
|
+
if (!got.includes(message)) assert.fail(`expected a refusal with "${message}", but it was refused with:\n${got}`);
|
|
55
|
+
};
|
|
56
|
+
const assertContains = (where, text, content) => {
|
|
57
|
+
if (content === void 0 || !content.includes(text)) assert.fail(containsFailure(where, text, content, true));
|
|
58
|
+
};
|
|
59
|
+
const assertLacks = (where, text, content) => {
|
|
60
|
+
if (content === void 0 || content.includes(text)) assert.fail(containsFailure(where, text, content, false));
|
|
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
|
+
};
|
|
67
|
+
//#endregion
|
|
6
68
|
//#region src/gherkin/index.ts
|
|
69
|
+
const BACKGROUND_ACTORS = "gtdBackgroundActors";
|
|
7
70
|
var TestkitWorld = class extends QuickPickleWorld {
|
|
8
71
|
constructor(context, info) {
|
|
9
72
|
super(context, info);
|
|
@@ -11,11 +74,15 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
11
74
|
gtdConfig = void 0;
|
|
12
75
|
env = {};
|
|
13
76
|
commands = [];
|
|
14
|
-
|
|
77
|
+
modules = {};
|
|
15
78
|
actions = /* @__PURE__ */ new Map();
|
|
16
79
|
verdicts = /* @__PURE__ */ new Map();
|
|
17
|
-
/** Per judge step, a verdict for each text its questions may contain; the first match answers. */
|
|
18
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();
|
|
19
86
|
created = void 0;
|
|
20
87
|
configure(config) {
|
|
21
88
|
this.assertNotCreated("a gtd config");
|
|
@@ -35,6 +102,19 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
35
102
|
this.assertNotCreated(`the environment variable ${name}`);
|
|
36
103
|
this.env[name] = value;
|
|
37
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
|
+
}
|
|
38
118
|
assertNotCreated(what) {
|
|
39
119
|
if (this.created !== void 0) throw new Error(`${what} must be set before any step that touches the repository`);
|
|
40
120
|
}
|
|
@@ -43,13 +123,69 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
43
123
|
return this.created ??= testRepo({
|
|
44
124
|
workflow: this.gtdConfig,
|
|
45
125
|
env: this.env,
|
|
46
|
-
commands: this.commands
|
|
126
|
+
commands: this.commands,
|
|
127
|
+
modules: this.modules
|
|
47
128
|
});
|
|
48
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. */
|
|
49
136
|
act(step, action) {
|
|
137
|
+
this.scripting(step);
|
|
50
138
|
this.actions.set(step, [...this.actions.get(step) ?? [], action]);
|
|
51
139
|
}
|
|
52
|
-
|
|
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() {
|
|
53
189
|
const actors = {};
|
|
54
190
|
for (const [step, actions] of this.actions) actors[step] = async (beat, repo) => {
|
|
55
191
|
for (const action of actions) await action(beat, repo);
|
|
@@ -67,12 +203,91 @@ var TestkitWorld = class extends QuickPickleWorld {
|
|
|
67
203
|
return hit[1];
|
|
68
204
|
};
|
|
69
205
|
}
|
|
70
|
-
|
|
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
|
|
251
|
+
};
|
|
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));
|
|
71
255
|
}
|
|
72
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
|
+
});
|
|
73
278
|
setWorldConstructor(TestkitWorld);
|
|
74
279
|
const withNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
75
|
-
const args = (list) => list.
|
|
280
|
+
const args = (list) => [...list.matchAll(/(["'])(.*?)\1(?=\s|$)|\S+/g)].map(([word, quote, inner]) => quote === void 0 ? word : inner);
|
|
281
|
+
/** An outcome step: a failure ends with where the process rests. */
|
|
282
|
+
const outcome = (check) => async (world, ...rest) => {
|
|
283
|
+
try {
|
|
284
|
+
await check(world, ...rest);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
287
|
+
failure.message += `\n\n${await restDescription(() => world.repo(), world.neverUsed())}`;
|
|
288
|
+
throw failure;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
76
291
|
Given("a gtd config with:", (world, source) => {
|
|
77
292
|
world.configure(withNewline(source));
|
|
78
293
|
});
|
|
@@ -82,9 +297,15 @@ Given("a gtd config from the module {string}", async (world, path) => {
|
|
|
82
297
|
Given("the gtd config ends with:", (world, source) => {
|
|
83
298
|
world.extendConfig(withNewline(source));
|
|
84
299
|
});
|
|
85
|
-
Given("
|
|
300
|
+
Given("the environment variable {string} is {string}", (world, name, value) => {
|
|
86
301
|
world.setEnv(name, value);
|
|
87
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
|
+
});
|
|
88
309
|
Given("the command {string} exits {int}", (world, run, exit) => {
|
|
89
310
|
world.command({
|
|
90
311
|
run,
|
|
@@ -98,9 +319,25 @@ Given("the command {string} exits {int} with:", (world, run, exit, output) => {
|
|
|
98
319
|
output: withNewline(output)
|
|
99
320
|
});
|
|
100
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
|
+
});
|
|
101
335
|
Given("a branch {string}", (world, name) => {
|
|
102
336
|
world.repo().branch(name);
|
|
103
337
|
});
|
|
338
|
+
Given("a branch {string} at {string}", (world, name, ref) => {
|
|
339
|
+
world.repo().branch(name, ref);
|
|
340
|
+
});
|
|
104
341
|
Given("{string} is checked out", (world, name) => {
|
|
105
342
|
world.repo().checkout(name);
|
|
106
343
|
});
|
|
@@ -118,7 +355,7 @@ Given("the text {string} in {string} is replaced with {string}", (world, text, p
|
|
|
118
355
|
assert.ok(content.includes(text), `${path}: ${content}`);
|
|
119
356
|
world.repo().write(path, content.replace(text, () => replacement));
|
|
120
357
|
});
|
|
121
|
-
Given("{string}
|
|
358
|
+
Given("text is appended to {string}:", (world, path, text) => {
|
|
122
359
|
const content = world.repo().read(path) ?? "";
|
|
123
360
|
world.repo().write(path, `${content.trimEnd()}\n\n${text}\n`);
|
|
124
361
|
});
|
|
@@ -132,10 +369,10 @@ Given("step {string} changes nothing", (world, step) => {
|
|
|
132
369
|
world.act(step, () => void 0);
|
|
133
370
|
});
|
|
134
371
|
Given("step {string} from now on changes nothing", (world, step) => {
|
|
135
|
-
world.
|
|
372
|
+
world.rescript(step, () => void 0);
|
|
136
373
|
});
|
|
137
374
|
Given("step {string} is no longer scripted", (world, step) => {
|
|
138
|
-
world.
|
|
375
|
+
world.unscript(step);
|
|
139
376
|
});
|
|
140
377
|
Given("step {string} writes {string} with:", (world, step, path, content) => {
|
|
141
378
|
world.act(step, (_, repo) => repo.write(path, withNewline(content)));
|
|
@@ -161,11 +398,9 @@ Given("step {string} deletes {string}", (world, step, path) => {
|
|
|
161
398
|
world.act(step, (_, repo) => repo.remove(path));
|
|
162
399
|
});
|
|
163
400
|
Given("step {string} is prompted with {string}", (world, step, text) => {
|
|
164
|
-
world.
|
|
165
|
-
if (!beat.content.includes(text)) throw new Error(`step "${step}" was not prompted with "${text}":\n${beat.content}`);
|
|
166
|
-
});
|
|
401
|
+
world.expectPrompt(step, text);
|
|
167
402
|
});
|
|
168
|
-
Given("step {string}
|
|
403
|
+
Given("step {string} overwrites {string}, if it exists, with:", (world, step, path, content) => {
|
|
169
404
|
world.act(step, (_, repo) => {
|
|
170
405
|
if (repo.read(path) !== void 0) repo.write(path, withNewline(content));
|
|
171
406
|
});
|
|
@@ -173,11 +408,30 @@ Given("step {string} rewrites {string} with:", (world, step, path, content) => {
|
|
|
173
408
|
Given("step {string} runs its script", (world, step) => {
|
|
174
409
|
world.act(step, runsItsScript);
|
|
175
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
|
+
});
|
|
176
430
|
Given("judge step {string} answers:", (world, step, verdict) => {
|
|
177
|
-
world.
|
|
431
|
+
world.answer(step, String(verdict));
|
|
178
432
|
});
|
|
179
433
|
Given("judge step {string}, when its questions contain {string}, answers:", (world, step, text, verdict) => {
|
|
180
|
-
world.
|
|
434
|
+
world.answerWhen(step, text, String(verdict));
|
|
181
435
|
});
|
|
182
436
|
When("gtd starts workflow {string}", async (world, workflow) => {
|
|
183
437
|
await world.repo().start(workflow);
|
|
@@ -197,87 +451,122 @@ When("gtd lands", async (world) => {
|
|
|
197
451
|
When("the process is driven", async (world) => {
|
|
198
452
|
await world.drive();
|
|
199
453
|
});
|
|
200
|
-
|
|
201
|
-
await
|
|
202
|
-
});
|
|
203
|
-
Then("
|
|
204
|
-
await
|
|
205
|
-
});
|
|
206
|
-
Then("
|
|
207
|
-
await
|
|
208
|
-
});
|
|
209
|
-
Then("
|
|
210
|
-
await
|
|
211
|
-
});
|
|
212
|
-
Then("driving the process is refused
|
|
213
|
-
await
|
|
214
|
-
});
|
|
215
|
-
Then("the process
|
|
216
|
-
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
});
|
|
222
|
-
Then("the
|
|
454
|
+
When("the process is driven leniently", async (world) => {
|
|
455
|
+
await world.drive({ strict: false });
|
|
456
|
+
});
|
|
457
|
+
Then("opening door {string} is refused with {string}", outcome(async (world, door, message) => {
|
|
458
|
+
await refusedWith(world.repo().door(door), message);
|
|
459
|
+
}));
|
|
460
|
+
Then("starting workflow {string} with {string} is refused with {string}", outcome(async (world, workflow, list, message) => {
|
|
461
|
+
await refusedWith(world.repo().start(workflow, ...args(list)), message);
|
|
462
|
+
}));
|
|
463
|
+
Then("landing is refused with {string}", outcome(async (world, message) => {
|
|
464
|
+
await refusedWith(world.repo().land(), message);
|
|
465
|
+
}));
|
|
466
|
+
Then("driving the process is refused with {string}", outcome(async (world, message) => {
|
|
467
|
+
await refusedWith(world.drive(), message);
|
|
468
|
+
}));
|
|
469
|
+
Then("driving the process is refused without {string}", outcome(async (world, message) => {
|
|
470
|
+
const got = await refusal(world.drive(), `without "${message}"`);
|
|
471
|
+
if (got.includes(message)) assert.fail(`expected a refusal without "${message}", but it was refused with:\n${got}`);
|
|
472
|
+
}));
|
|
473
|
+
Then("the process rests at {string}", outcome(async (world, step) => {
|
|
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)}`);
|
|
494
|
+
}));
|
|
495
|
+
Then("the resting step's content contains {string}", outcome(async (world, text) => {
|
|
496
|
+
assertContains("the resting step's content", text, (await world.repo().next()).content);
|
|
497
|
+
}));
|
|
498
|
+
Then("the resting step's content contains the hash of the commit {string}", outcome(async (world, subject) => {
|
|
223
499
|
const repo = world.repo();
|
|
224
500
|
const hash = repo.hashes()[repo.subjects().lastIndexOf(subject)];
|
|
225
501
|
assert.ok(hash !== void 0, `no commit "${subject}"`);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
233
|
-
Then("the last commit subject is {string}", (world, subject) => {
|
|
502
|
+
assertContains("the resting step's content", hash, (await repo.next()).content);
|
|
503
|
+
}));
|
|
504
|
+
Then("the resting step's content does not contain {string}", outcome(async (world, text) => {
|
|
505
|
+
assertLacks("the resting step's content", text, (await world.repo().next()).content);
|
|
506
|
+
}));
|
|
507
|
+
Then("the last commit subject is {string}", outcome((world, subject) => {
|
|
234
508
|
assert.strictEqual(world.repo().subjects().at(-1), subject);
|
|
235
|
-
});
|
|
236
|
-
Then("the commit subjects end with:", (world, list) => {
|
|
509
|
+
}));
|
|
510
|
+
Then("the commit subjects end with:", outcome((world, list) => {
|
|
237
511
|
const expected = list.split("\n").filter((line) => line.trim() !== "");
|
|
238
512
|
assert.deepStrictEqual(world.repo().subjects().slice(-expected.length), expected);
|
|
239
|
-
});
|
|
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
|
+
}));
|
|
240
526
|
const outputOf = async (world, list) => {
|
|
241
527
|
const result = await world.repo().gtd(...args(list));
|
|
242
528
|
assert.strictEqual(result.exitCode, 0, result.stderr);
|
|
243
529
|
return result.stdout;
|
|
244
530
|
};
|
|
245
|
-
Then("the output of gtd {string} contains {string}", async (world, list, text) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
});
|
|
253
|
-
Then("the output lines of gtd {string} start with:", async (world, list, prefixes) => {
|
|
531
|
+
Then("the output of gtd {string} contains {string}", outcome(async (world, list, text) => {
|
|
532
|
+
assertContains(`the output of gtd ${list}`, text, await outputOf(world, list));
|
|
533
|
+
}));
|
|
534
|
+
Then("the output of gtd {string} does not contain {string}", outcome(async (world, list, text) => {
|
|
535
|
+
assertLacks(`the output of gtd ${list}`, text, await outputOf(world, list));
|
|
536
|
+
}));
|
|
537
|
+
Then("the output lines of gtd {string} start with:", outcome(async (world, list, prefixes) => {
|
|
254
538
|
const out = (await outputOf(world, list)).split("\n").filter((line) => line !== "");
|
|
255
539
|
const expected = prefixes.split("\n").filter((line) => line.trim() !== "");
|
|
256
|
-
|
|
257
|
-
out.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
});
|
|
265
|
-
|
|
540
|
+
const sides = `expected lines starting with:\n${expected.join("\n")}\ngot:\n${out.join("\n")}`;
|
|
541
|
+
assert.strictEqual(out.length, expected.length, sides);
|
|
542
|
+
out.forEach((line, i) => assert.ok(line.startsWith(expected[i]), sides));
|
|
543
|
+
}));
|
|
544
|
+
Then("{string} contains {string}", outcome((world, path, text) => {
|
|
545
|
+
assertContains(`"${path}"`, text, world.repo().read(path));
|
|
546
|
+
}));
|
|
547
|
+
Then("{string} contains:", outcome((world, path, text) => {
|
|
548
|
+
assertContains(`"${path}"`, text, world.repo().read(path));
|
|
549
|
+
}));
|
|
550
|
+
Then("{string} contains the short hash of the commit {string}", outcome((world, path, subject) => {
|
|
266
551
|
const repo = world.repo();
|
|
267
552
|
const hash = repo.hashes()[repo.subjects().lastIndexOf(subject)];
|
|
268
553
|
assert.ok(hash !== void 0, `no commit "${subject}"`);
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
Then("{string}
|
|
554
|
+
assertContains(`"${path}"`, hash.slice(0, 7), repo.read(path));
|
|
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
|
+
}));
|
|
559
|
+
Then("{string} contains {string} exactly once", outcome((world, path, text) => {
|
|
272
560
|
const content = world.repo().read(path) ?? "";
|
|
273
|
-
|
|
274
|
-
});
|
|
275
|
-
|
|
561
|
+
const times = content.split(text).length - 1;
|
|
562
|
+
assert.strictEqual(times, 1, `"${path}" contains "${text}" ${times} times; it holds:\n${content}`);
|
|
563
|
+
}));
|
|
564
|
+
Then("{string} does not contain {string}", outcome((world, path, text) => {
|
|
565
|
+
assertLacks(`"${path}"`, text, world.repo().read(path));
|
|
566
|
+
}));
|
|
567
|
+
Then("{string} does not exist", outcome((world, path) => {
|
|
276
568
|
const content = world.repo().read(path);
|
|
277
|
-
assert.
|
|
278
|
-
});
|
|
279
|
-
Then("{string} does not exist", (world, path) => {
|
|
280
|
-
assert.strictEqual(world.repo().read(path), void 0);
|
|
281
|
-
});
|
|
569
|
+
assert.strictEqual(content, void 0, `"${path}" exists; it holds:\n${content}`);
|
|
570
|
+
}));
|
|
282
571
|
//#endregion
|
|
283
572
|
export { TestkitWorld };
|
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 };
|