@pmelab/gtd 59.0.1 → 60.1.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 +48 -32
- 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-BhTey1Do.mjs +30517 -0
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +11285 -19864
- package/dist/gtd.core.mjs +593 -0
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +246 -37
- 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 +7 -7
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +185 -184
- package/src/cli/Cli.ts +219 -224
- 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 +34 -3
- package/src/cli/Program.test.ts +78 -84
- package/src/cli/Program.ts +192 -128
- package/src/cli/ToolCommands.ts +2 -2
- package/src/edge/Edge.test.ts +72 -62
- package/src/edge/Edge.ts +6 -6
- 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 +189 -55
- 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/install/Version.ts +9 -3
- package/src/install/index.ts +1 -1
- package/src/platform/CommandRunner.ts +13 -11
- package/src/platform/Commentary.ts +2 -2
- package/src/platform/Git.test.ts +4 -5
- package/src/platform/Git.ts +42 -38
- package/src/platform/Host.ts +1 -1
- package/src/platform/NodeServices.ts +11 -0
- package/src/platform/Workspace.test.ts +4 -5
- package/src/platform/Workspace.ts +3 -3
- package/src/platform/WorktreeState.test.ts +2 -3
- package/src/platform/WorktreeState.ts +1 -1
- package/src/platform/index.ts +2 -1
- 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 +12 -12
- 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 +5 -3
- package/src/twins/GitTiers.ts +6 -7
- package/src/twins/InMemRepo.ts +1 -0
- package/src/twins/Layers.test.ts +1 -1
- package/src/twins/Layers.ts +10 -3
- package/src/twins/cliIo.ts +9 -4
- 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 +27 -10
- package/src/workflow/discovery.ts +5 -6
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +316 -119
- package/src/workflow/load.ts +478 -305
- package/src/workflow/pin.test.ts +159 -0
- package/src/workflow/pin.ts +72 -0
- package/src/workflow/vars.ts +3 -3
- 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/chunks/testkit-07Z_ENch.mjs +0 -32175
|
@@ -4,8 +4,7 @@ import { join } from "node:path"
|
|
|
4
4
|
import { tmpdir } from "node:os"
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
6
6
|
import { Cause, Effect, Exit, Layer } from "effect"
|
|
7
|
-
import {
|
|
8
|
-
import { Narrator, GitService, Host, Workspace } from "../platform/index.js"
|
|
7
|
+
import { Narrator, GitService, Host, Workspace, NodeServices } from "../platform/index.js"
|
|
9
8
|
import {
|
|
10
9
|
ConfigDiscovery,
|
|
11
10
|
ConfigService,
|
|
@@ -23,7 +22,7 @@ type Env = Readonly<Record<string, string>>
|
|
|
23
22
|
|
|
24
23
|
const baseLayer = (dir: string, env: Env = {}) => {
|
|
25
24
|
const hostLayer = Host.layer({ root: dir, home: homeDir, env })
|
|
26
|
-
const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer,
|
|
25
|
+
const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeServices.layer)))
|
|
27
26
|
const workspaceLayer = Workspace.Live.pipe(Layer.provide(Layer.merge(hostLayer, gitLayer)))
|
|
28
27
|
return Layer.mergeAll(hostLayer, gitLayer, workspaceLayer)
|
|
29
28
|
}
|
|
@@ -72,16 +71,11 @@ let projectDir: string
|
|
|
72
71
|
|
|
73
72
|
// A config whose default workflow rests at `idle`, declaring a process
|
|
74
73
|
// setting, an environment setting and the scopes its turns run under.
|
|
75
|
-
const BASE_CONFIG = `import { human } from "@pmelab/gtd/flows"
|
|
74
|
+
const BASE_CONFIG = `import { human, workflow } from "@pmelab/gtd/flows"
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
export const defaults = { qualityReviews: "" }
|
|
82
|
-
export const envDefaults = { testCommand: "npm test" }
|
|
83
|
-
|
|
84
|
-
export const skills = (vars) => ({
|
|
76
|
+
const defaults = { qualityReviews: "" }
|
|
77
|
+
const envDefaults = { testCommand: "npm test" }
|
|
78
|
+
const skills = (vars) => ({
|
|
85
79
|
build: [],
|
|
86
80
|
"build.fix.quality": [],
|
|
87
81
|
"build.review": [],
|
|
@@ -93,6 +87,11 @@ export const skills = (vars) => ({
|
|
|
93
87
|
.map((lens) => [\`build.quality.\${lens}\`, []]),
|
|
94
88
|
),
|
|
95
89
|
})
|
|
90
|
+
|
|
91
|
+
export default workflow(async () => {
|
|
92
|
+
await human("idle", { message: "hi" })
|
|
93
|
+
}, { defaults, envDefaults, skills })
|
|
94
|
+
|
|
96
95
|
`
|
|
97
96
|
|
|
98
97
|
// Workflow factories a config imports from a sibling module: string options
|
|
@@ -190,6 +189,17 @@ const minimalWorkflow = (first: string) =>
|
|
|
190
189
|
``,
|
|
191
190
|
].join("\n")
|
|
192
191
|
|
|
192
|
+
// The same one-step workflow, carrying `options` (source text) as its build options.
|
|
193
|
+
const optionedWorkflow = (first: string, options: string) =>
|
|
194
|
+
[
|
|
195
|
+
`import { human, workflow } from "@pmelab/gtd/flows"`,
|
|
196
|
+
``,
|
|
197
|
+
`export default workflow(async () => {`,
|
|
198
|
+
` await human("${first}")`,
|
|
199
|
+
`}, ${options})`,
|
|
200
|
+
``,
|
|
201
|
+
].join("\n")
|
|
202
|
+
|
|
193
203
|
const withConfig = (config: string) =>
|
|
194
204
|
writeFileSync(
|
|
195
205
|
join(projectDir, "gtd.config.ts"),
|
|
@@ -205,11 +215,11 @@ describe("ConfigService", () => {
|
|
|
205
215
|
expect(cfg.defaultWorkflow.vars["testCommand"]).toBeUndefined()
|
|
206
216
|
})
|
|
207
217
|
|
|
208
|
-
it("with no gtd.config.ts anywhere: fails, pointing at gtd
|
|
218
|
+
it("with no gtd.config.ts anywhere: fails, pointing at gtd guide", async () => {
|
|
209
219
|
rmSync(join(projectDir, "gtd.config.ts"))
|
|
210
220
|
|
|
211
221
|
await expect(getConfig()).rejects.toThrow(
|
|
212
|
-
/no workflows configured: no gtd\.config\.ts here or above — gtd ships no workflow; `gtd
|
|
222
|
+
/no workflows configured: no gtd\.config\.ts here or above — gtd ships no workflow; `gtd guide` says where to get one/,
|
|
213
223
|
)
|
|
214
224
|
})
|
|
215
225
|
|
|
@@ -307,7 +317,7 @@ describe("ConfigService", () => {
|
|
|
307
317
|
it("a workflow declaring a name in both defaults and envDefaults fails to load", async () => {
|
|
308
318
|
writeFileSync(
|
|
309
319
|
join(projectDir, "gtd.config.ts"),
|
|
310
|
-
|
|
320
|
+
optionedWorkflow("first", `{ defaults: { x: "1" }, envDefaults: { x: "2" } }`),
|
|
311
321
|
)
|
|
312
322
|
|
|
313
323
|
await expect(getConfig()).rejects.toThrow(`"x" declared in both "defaults" and "envDefaults"`)
|
|
@@ -375,16 +385,16 @@ describe("ConfigService", () => {
|
|
|
375
385
|
expect(cfg.workflow.initial).toBe("custom-idle")
|
|
376
386
|
})
|
|
377
387
|
|
|
378
|
-
it("reads `defaults`, `summary`, `base` and `steering` off
|
|
388
|
+
it("reads `defaults`, `summary`, `base` and `steering` off workflow()'s options and ignores other exports", async () => {
|
|
379
389
|
writeFileSync(
|
|
380
390
|
join(projectDir, "gtd.config.ts"),
|
|
381
391
|
[
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
`export const steering = { ".gtd/PLAN.md": "qa" }`,
|
|
392
|
+
optionedWorkflow(
|
|
393
|
+
"first",
|
|
394
|
+
`{ defaults: { greeting: "hi" }, summary: () => "sum", base: () => undefined, steering: { ".gtd/PLAN.md": "qa" } }`,
|
|
395
|
+
),
|
|
387
396
|
`export const helper = 42`,
|
|
397
|
+
`export const plain = async () => {}`,
|
|
388
398
|
``,
|
|
389
399
|
].join("\n"),
|
|
390
400
|
)
|
|
@@ -396,8 +406,23 @@ describe("ConfigService", () => {
|
|
|
396
406
|
expect(typeof cfg.workflow.summary).toBe("function")
|
|
397
407
|
expect(typeof cfg.workflow.base).toBe("function")
|
|
398
408
|
expect(cfg.workflow.steering).toEqual({ ".gtd/PLAN.md": "qa" })
|
|
409
|
+
expect(cfg.workflowNames).toEqual([])
|
|
399
410
|
})
|
|
400
411
|
|
|
412
|
+
it.each(["defaults", "envDefaults", "steering", "skills", "access", "doors", "summary", "base"])(
|
|
413
|
+
"refuses a file-level %s export, naming its replacement",
|
|
414
|
+
async (name) => {
|
|
415
|
+
writeFileSync(
|
|
416
|
+
join(projectDir, "gtd.config.ts"),
|
|
417
|
+
`${minimalWorkflow("first")}export const ${name} = {}\n`,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
await expect(getConfig()).rejects.toThrow(
|
|
421
|
+
`gtd.config.ts: ${name}: the "${name}" export is no longer read — pass it to the workflow it configures: \`workflow(flow, { ${name} })\`, or the build a workflow factory returns`,
|
|
422
|
+
)
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
|
|
401
426
|
it("the fingerprint covers gtd.config.ts AND a sibling module it imports from", async () => {
|
|
402
427
|
writeFileSync(
|
|
403
428
|
join(projectDir, "steps.ts"),
|
|
@@ -405,7 +430,9 @@ describe("ConfigService", () => {
|
|
|
405
430
|
)
|
|
406
431
|
writeFileSync(
|
|
407
432
|
join(projectDir, "gtd.config.ts"),
|
|
408
|
-
[
|
|
433
|
+
[`import { steering } from "./steps.js"`, optionedWorkflow("first", "{ steering }")].join(
|
|
434
|
+
"\n",
|
|
435
|
+
),
|
|
409
436
|
)
|
|
410
437
|
|
|
411
438
|
const cfg = await getConfig()
|
|
@@ -414,6 +441,64 @@ describe("ConfigService", () => {
|
|
|
414
441
|
expect(Object.keys(cfg.fingerprint).sort()).toEqual(["gtd.config.ts", "steps.ts"])
|
|
415
442
|
})
|
|
416
443
|
|
|
444
|
+
it("an ES module package gets the shipped gtd, a CommonJS one stays Node's, each module keeps its location", async () => {
|
|
445
|
+
const vendor = join(projectDir, "vendor", "wf")
|
|
446
|
+
const pkg = (name: string, manifest: object, file: string, source: string) => {
|
|
447
|
+
const dir = join(vendor, "node_modules", name)
|
|
448
|
+
mkdirSync(dir, { recursive: true })
|
|
449
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name, ...manifest }))
|
|
450
|
+
writeFileSync(join(dir, file), source)
|
|
451
|
+
}
|
|
452
|
+
// No @pmelab/gtd installed: only the shipped copy can serve this import.
|
|
453
|
+
pkg(
|
|
454
|
+
"dep",
|
|
455
|
+
{ type: "module", exports: { import: "./index.js" } },
|
|
456
|
+
"index.js",
|
|
457
|
+
`import { human } from "@pmelab/gtd/flows"\nexport const file = typeof human\n`,
|
|
458
|
+
)
|
|
459
|
+
pkg("ts-dep", { main: "index.ts" }, "index.ts", `export const suffix: string = "ts"\n`)
|
|
460
|
+
pkg("cjs-dep", {}, "index.js", `module.exports = require("node:path").basename(__dirname)\n`)
|
|
461
|
+
writeFileSync(
|
|
462
|
+
join(vendor, "index.ts"),
|
|
463
|
+
[
|
|
464
|
+
`import { basename, dirname } from "node:path"`,
|
|
465
|
+
`import { fileURLToPath } from "node:url"`,
|
|
466
|
+
`import { file as base } from "dep"`,
|
|
467
|
+
`import { suffix } from "ts-dep"`,
|
|
468
|
+
`import cjs from "cjs-dep"`,
|
|
469
|
+
`const where = basename(dirname(fileURLToPath(import.meta.url)))`,
|
|
470
|
+
`export const file = [base, suffix, cjs, where].join("-")`,
|
|
471
|
+
``,
|
|
472
|
+
].join("\n"),
|
|
473
|
+
)
|
|
474
|
+
writeFileSync(
|
|
475
|
+
join(projectDir, "gtd.config.ts"),
|
|
476
|
+
[
|
|
477
|
+
`import { file } from "./vendor/wf/index.js"`,
|
|
478
|
+
optionedWorkflow("first", "{ steering: { [`.gtd/${file}.md`]: 'qa' } }"),
|
|
479
|
+
].join("\n"),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
const cfg = await getConfig()
|
|
483
|
+
|
|
484
|
+
expect(cfg.workflow.steering[".gtd/function-ts-cjs-dep-wf.md"]).toBe("qa")
|
|
485
|
+
expect(Object.keys(cfg.fingerprint).sort()).toEqual(["gtd.config.ts", "vendor/wf/index.ts"])
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it("a config may require and require.resolve a local module", async () => {
|
|
489
|
+
writeFileSync(join(projectDir, "steps.ts"), `export const file = "required"\n`)
|
|
490
|
+
writeFileSync(
|
|
491
|
+
join(projectDir, "gtd.config.ts"),
|
|
492
|
+
[
|
|
493
|
+
`const { file } = require("./steps.js")`,
|
|
494
|
+
`const found = require("node:path").basename(require.resolve("./steps.ts"))`,
|
|
495
|
+
optionedWorkflow("first", "{ steering: { [`.gtd/${file}-${found}.md`]: 'qa' } }"),
|
|
496
|
+
].join("\n"),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
expect((await getConfig()).workflow.steering[".gtd/required-steps.ts.md"]).toBe("qa")
|
|
500
|
+
})
|
|
501
|
+
|
|
417
502
|
it("the fingerprint covers gtd.config.ts but no gtd module it imports", async () => {
|
|
418
503
|
const cfg = await getConfig()
|
|
419
504
|
|
|
@@ -421,21 +506,24 @@ describe("ConfigService", () => {
|
|
|
421
506
|
})
|
|
422
507
|
|
|
423
508
|
it.each([
|
|
424
|
-
[`
|
|
425
|
-
[`
|
|
509
|
+
[`{ defaults: { n: 1 } }`, `workflow "default"'s "defaults" is not a record of strings`],
|
|
510
|
+
[`{ summary: "text" }`, `workflow "default"'s "summary" is not a function`],
|
|
511
|
+
[`{ base: "main" }`, `workflow "default"'s "base" is not a function`],
|
|
426
512
|
[
|
|
427
|
-
`
|
|
428
|
-
|
|
513
|
+
`{ steering: { ".gtd/PLAN.md": 1 } }`,
|
|
514
|
+
`workflow "default"'s "steering" is not a record of strings`,
|
|
429
515
|
],
|
|
430
|
-
|
|
431
|
-
|
|
516
|
+
[`{ doors: [] }`, `workflow "default"'s "doors" is not a record of doors`],
|
|
517
|
+
[`{ defualts: {} }`, `workflow "default" has unknown option(s) "defualts" — known: defaults,`],
|
|
518
|
+
])("rejects malformed workflow() options: %s", async (options, message) => {
|
|
519
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), optionedWorkflow("first", options))
|
|
432
520
|
|
|
433
521
|
await expect(getConfig()).rejects.toThrow(message)
|
|
434
522
|
})
|
|
435
523
|
|
|
436
524
|
const firstStepWorkflow = (body: string) =>
|
|
437
525
|
[
|
|
438
|
-
`import { human, vars, env } from "@pmelab/gtd/flows"`,
|
|
526
|
+
`import { human, settings as vars, envSettings as env } from "@pmelab/gtd/flows"`,
|
|
439
527
|
`export default async () => {`,
|
|
440
528
|
` ${body}`,
|
|
441
529
|
`}`,
|
|
@@ -479,23 +567,19 @@ describe("ConfigService", () => {
|
|
|
479
567
|
it("refuses a workflow default whose name is not a valid setting name", async () => {
|
|
480
568
|
writeFileSync(
|
|
481
569
|
join(projectDir, "gtd.config.ts"),
|
|
482
|
-
|
|
483
|
-
minimalWorkflow("first"),
|
|
484
|
-
`export const defaults = { "a=b": "1" }`,
|
|
485
|
-
`export const envDefaults = { "c d": "1" }`,
|
|
486
|
-
].join("\n"),
|
|
570
|
+
optionedWorkflow("first", `{ defaults: { "a=b": "1" }, envDefaults: { "c d": "1" } }`),
|
|
487
571
|
)
|
|
488
572
|
await expect(getConfig()).rejects.toThrow(
|
|
489
|
-
'
|
|
573
|
+
'workflow "default" declares defaults "a=b", not a valid setting name — a setting name is a letter or "_", then letters, digits or "_"',
|
|
490
574
|
)
|
|
491
575
|
})
|
|
492
576
|
|
|
493
577
|
it("refuses an envDefaults name that is not a valid setting name", async () => {
|
|
494
578
|
writeFileSync(
|
|
495
579
|
join(projectDir, "gtd.config.ts"),
|
|
496
|
-
|
|
580
|
+
optionedWorkflow("first", `{ envDefaults: { "c d": "1" } }`),
|
|
497
581
|
)
|
|
498
|
-
await expect(getConfig()).rejects.toThrow('
|
|
582
|
+
await expect(getConfig()).rejects.toThrow('workflow "default" declares envDefaults "c d"')
|
|
499
583
|
})
|
|
500
584
|
|
|
501
585
|
it("refuses a flow whose first step depends on the repository's files", async () => {
|
|
@@ -559,7 +643,7 @@ describe("ConfigService", () => {
|
|
|
559
643
|
|
|
560
644
|
describe("named workflows", () => {
|
|
561
645
|
const flow = (first: string) => `async () => {\n await human("${first}")\n}`
|
|
562
|
-
const header = `import { human } from "@pmelab/gtd/flows"\n`
|
|
646
|
+
const header = `import { human, workflow } from "@pmelab/gtd/flows"\n`
|
|
563
647
|
|
|
564
648
|
it("re-exported factories are startable under their names, default is not", async () => {
|
|
565
649
|
withFactories(`export { feature as default, feature, review, fix } from "./workflows"\n`)
|
|
@@ -570,48 +654,93 @@ describe("ConfigService", () => {
|
|
|
570
654
|
expect(cfg.workflowNamed("fix")?.def.initial).toBe("idle")
|
|
571
655
|
})
|
|
572
656
|
|
|
573
|
-
it("
|
|
657
|
+
it("only workflow() exports and factories are workflows, named by their export; other functions are helpers", async () => {
|
|
574
658
|
writeFileSync(
|
|
575
659
|
join(projectDir, "gtd.config.ts"),
|
|
576
660
|
[
|
|
577
661
|
header,
|
|
578
662
|
`export default ${flow("main")}`,
|
|
579
|
-
`export const hotfix = ${flow("hot")}`,
|
|
580
|
-
`export const
|
|
581
|
-
`export const summary = () => "sum"`,
|
|
582
|
-
`export const base = () => undefined`,
|
|
583
|
-
`export const steering = {}`,
|
|
584
|
-
`export const skills = {}`,
|
|
663
|
+
`export const hotfix = workflow(${flow("hot")}, { defaults: { greeting: "hi" } })`,
|
|
664
|
+
`export const helper = ${flow("help")}`,
|
|
585
665
|
`export const notAFlow = 42`,
|
|
586
666
|
``,
|
|
587
667
|
].join("\n"),
|
|
588
668
|
)
|
|
589
669
|
const cfg = await getConfig()
|
|
590
670
|
expect(cfg.workflowNames).toEqual(["hotfix"])
|
|
671
|
+
expect(cfg.warnings).toEqual([])
|
|
591
672
|
expect(cfg.defaultWorkflow.name).toBe("default")
|
|
673
|
+
expect(cfg.defaultWorkflow.vars).toEqual({})
|
|
592
674
|
expect(cfg.workflow.initial).toBe("main")
|
|
593
675
|
expect(cfg.workflowNamed("hotfix")?.vars).toEqual({ greeting: "hi" })
|
|
594
676
|
})
|
|
595
677
|
|
|
678
|
+
it("an exported workflow re-wrapped with options of its own is a second workflow with only those", async () => {
|
|
679
|
+
writeFileSync(
|
|
680
|
+
join(projectDir, "gtd.config.ts"),
|
|
681
|
+
[
|
|
682
|
+
header,
|
|
683
|
+
`export const library = workflow(${flow("main")}, { defaults: { greeting: "hi" } })`,
|
|
684
|
+
`export const mine = workflow(library, { defaults: { tone: "dry" } })`,
|
|
685
|
+
`export default mine`,
|
|
686
|
+
``,
|
|
687
|
+
].join("\n"),
|
|
688
|
+
)
|
|
689
|
+
const cfg = await getConfig()
|
|
690
|
+
expect(cfg.workflowNames).toEqual(["library", "mine"])
|
|
691
|
+
expect(cfg.workflowNamed("library")?.vars).toEqual({ greeting: "hi" })
|
|
692
|
+
expect(cfg.workflowNamed("mine")?.vars).toEqual({ tone: "dry" })
|
|
693
|
+
expect(cfg.workflowNamed("mine")?.def.initial).toBe("main")
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
it("a plain default export is a workflow without options", async () => {
|
|
697
|
+
writeFileSync(
|
|
698
|
+
join(projectDir, "gtd.config.ts"),
|
|
699
|
+
[header, `export default ${flow("main")}`, ``].join("\n"),
|
|
700
|
+
)
|
|
701
|
+
const cfg = await getConfig()
|
|
702
|
+
expect(cfg.workflow.initial).toBe("main")
|
|
703
|
+
expect(cfg.defaultWorkflow.vars).toEqual({})
|
|
704
|
+
expect(cfg.workflow.summary).toBeUndefined()
|
|
705
|
+
})
|
|
706
|
+
|
|
596
707
|
it("a config with no default export fails, saying what to export", async () => {
|
|
597
708
|
writeFileSync(
|
|
598
709
|
join(projectDir, "gtd.config.ts"),
|
|
599
710
|
[header, `export const hotfix = ${flow("hot")}`, ``].join("\n"),
|
|
600
711
|
)
|
|
601
|
-
await expect(getConfig()).rejects.toThrow(
|
|
712
|
+
await expect(getConfig()).rejects.toThrow(
|
|
713
|
+
/no default export — export the workflow an ordinary start runs; `gtd guide` says where to get one, or write your own/,
|
|
714
|
+
)
|
|
602
715
|
})
|
|
603
716
|
|
|
604
717
|
it("a default that is also a named export goes by that name", async () => {
|
|
605
718
|
writeFileSync(
|
|
606
719
|
join(projectDir, "gtd.config.ts"),
|
|
607
|
-
[
|
|
720
|
+
[
|
|
721
|
+
header,
|
|
722
|
+
`export const hotfix = workflow(${flow("hot")})`,
|
|
723
|
+
`export default hotfix`,
|
|
724
|
+
``,
|
|
725
|
+
].join("\n"),
|
|
608
726
|
)
|
|
609
727
|
expect((await getConfig()).defaultWorkflow.name).toBe("hotfix")
|
|
610
728
|
})
|
|
611
729
|
|
|
612
|
-
it("a helper re-exported from a workflow module
|
|
730
|
+
it("a helper re-exported from a workflow module stays a helper; a marked one is a workflow", async () => {
|
|
613
731
|
withFactories(`export { feature as default, unwind } from "./workflows"\n`)
|
|
614
|
-
expect((await getConfig()).workflowNames).toContain("unwind")
|
|
732
|
+
expect((await getConfig()).workflowNames).not.toContain("unwind")
|
|
733
|
+
writeFileSync(
|
|
734
|
+
join(projectDir, "gtd.config.ts"),
|
|
735
|
+
[
|
|
736
|
+
`import { workflow } from "@pmelab/gtd/flows"`,
|
|
737
|
+
`import { unwind } from "./workflows"`,
|
|
738
|
+
`export { feature as default } from "./workflows"`,
|
|
739
|
+
`export const unwinding = workflow(unwind)`,
|
|
740
|
+
``,
|
|
741
|
+
].join("\n"),
|
|
742
|
+
)
|
|
743
|
+
expect((await getConfig()).workflowNames).toContain("unwinding")
|
|
615
744
|
})
|
|
616
745
|
|
|
617
746
|
it("a gtd subpath only a helper file imports resolves", async () => {
|
|
@@ -628,21 +757,19 @@ describe("ConfigService", () => {
|
|
|
628
757
|
[
|
|
629
758
|
header,
|
|
630
759
|
`import { kit } from "./kit"`,
|
|
631
|
-
`export default ${flow("main")}`,
|
|
632
|
-
'export const steering = { [`.gtd/${kit}.md`]: "qa" }',
|
|
760
|
+
`export default workflow(${flow("main")}, { steering: { [\`.gtd/\${kit}.md\`]: "qa" } })`,
|
|
633
761
|
``,
|
|
634
762
|
].join("\n"),
|
|
635
763
|
)
|
|
636
764
|
expect((await getConfig()).workflow.steering[".gtd/function.md"]).toBe("qa")
|
|
637
765
|
})
|
|
638
766
|
|
|
639
|
-
it("steering is the
|
|
767
|
+
it("steering is the workflow's own option", async () => {
|
|
640
768
|
writeFileSync(
|
|
641
769
|
join(projectDir, "gtd.config.ts"),
|
|
642
770
|
[
|
|
643
771
|
header,
|
|
644
|
-
`export default ${flow("main")}`,
|
|
645
|
-
`export const steering = { ".gtd/REVIEW.md": "qa", ".gtd/X.md": "qa" }`,
|
|
772
|
+
`export default workflow(${flow("main")}, { steering: { ".gtd/REVIEW.md": "qa", ".gtd/X.md": "qa" } })`,
|
|
646
773
|
``,
|
|
647
774
|
].join("\n"),
|
|
648
775
|
)
|
|
@@ -655,16 +782,16 @@ describe("ConfigService", () => {
|
|
|
655
782
|
|
|
656
783
|
describe("doors", () => {
|
|
657
784
|
const flow = (first: string) => `async () => {\n await human("${first}")\n}`
|
|
658
|
-
const header = `import { human } from "@pmelab/gtd/flows"\n`
|
|
785
|
+
const header = `import { human, workflow } from "@pmelab/gtd/flows"\n`
|
|
786
|
+
/** `hotfix` builds `doors`, a record of doors without their `workflow`. */
|
|
659
787
|
const withDoors = (doors: string, extra = ""): void =>
|
|
660
788
|
writeFileSync(
|
|
661
789
|
join(projectDir, "gtd.config.ts"),
|
|
662
790
|
[
|
|
663
791
|
header,
|
|
664
792
|
`export default ${flow("main")}`,
|
|
665
|
-
`export const hotfix = ${flow("hot")}`,
|
|
793
|
+
`export const hotfix = workflow(${flow("hot")}, { doors: ${doors} })`,
|
|
666
794
|
extra,
|
|
667
|
-
`export const doors = ${doors}`,
|
|
668
795
|
``,
|
|
669
796
|
].join("\n"),
|
|
670
797
|
)
|
|
@@ -701,26 +828,9 @@ describe("ConfigService", () => {
|
|
|
701
828
|
expect(audit?.def.base?.("audit", { reviewBase: "v1" })).toBe("v1")
|
|
702
829
|
})
|
|
703
830
|
|
|
704
|
-
it("a
|
|
705
|
-
withFactories(
|
|
706
|
-
[
|
|
707
|
-
`export { feature as default, review } from "./workflows"`,
|
|
708
|
-
`export const base = (w: string) => (w === "review" ? "develop" : undefined)`,
|
|
709
|
-
``,
|
|
710
|
-
].join("\n"),
|
|
711
|
-
)
|
|
712
|
-
expect((await getConfig()).workflowNamed("review")?.def.base?.("review", {})).toBe("develop")
|
|
713
|
-
})
|
|
714
|
-
|
|
715
|
-
it("a door several workflows build gets no door when none is exported under its name, until the doors export declares it", async () => {
|
|
716
|
-
const both = `export { feature as default, fix as fix2, fix as fix3 } from "./workflows"`
|
|
717
|
-
withFactories(`${both}\n`)
|
|
831
|
+
it("a door several workflows build gets no door when none is exported under its name", async () => {
|
|
832
|
+
withFactories(`export { feature as default, fix as fix2, fix as fix3 } from "./workflows"\n`)
|
|
718
833
|
expect((await getConfig()).doors.get("fix")).toBeUndefined()
|
|
719
|
-
writeFileSync(
|
|
720
|
-
join(projectDir, "gtd.config.ts"),
|
|
721
|
-
`${both}\nexport const doors = { fix: { workflow: "fix3" } }\n`,
|
|
722
|
-
)
|
|
723
|
-
expect((await getConfig()).doors.get("fix")?.workflow).toBe("fix3")
|
|
724
834
|
})
|
|
725
835
|
|
|
726
836
|
it("a door clash warns once at doors.<name>, builders sorted", async () => {
|
|
@@ -734,17 +844,15 @@ describe("ConfigService", () => {
|
|
|
734
844
|
severity: "warning",
|
|
735
845
|
path: ["doors", "fix"],
|
|
736
846
|
origin: join(projectDir, "gtd.config.ts"),
|
|
737
|
-
message: `door "fix" is built by workflows "fix2", "fix3" and "fix4" and none is exported under that name —
|
|
847
|
+
message: `door "fix" is built by workflows "fix2", "fix3" and "fix4" and none is exported under that name — rename the export that should own it to "fix"`,
|
|
738
848
|
},
|
|
739
849
|
])
|
|
740
850
|
})
|
|
741
851
|
|
|
742
|
-
it("no clash warning when an export is named after the door, one workflow builds it
|
|
743
|
-
const both = `export { feature as default, fix as fix2, fix as fix3 } from "./workflows"`
|
|
852
|
+
it("no clash warning when an export is named after the door, or one workflow builds it", async () => {
|
|
744
853
|
for (const body of [
|
|
745
854
|
`export { feature as default, fix as fix2, fix } from "./workflows"\n`,
|
|
746
855
|
`export { feature as default, fix } from "./workflows"\n`,
|
|
747
|
-
`${both}\nexport const doors = { fix: { workflow: "fix3" } }\n`,
|
|
748
856
|
]) {
|
|
749
857
|
withFactories(body)
|
|
750
858
|
expect((await getConfig()).warnings.filter((w) => w.path[0] === "doors")).toEqual([])
|
|
@@ -766,11 +874,8 @@ describe("ConfigService", () => {
|
|
|
766
874
|
}
|
|
767
875
|
})
|
|
768
876
|
|
|
769
|
-
it("a config's doors
|
|
770
|
-
withDoors(
|
|
771
|
-
`{ hot: { workflow: "hotfix", args: [{ name: "ticket" }] } }`,
|
|
772
|
-
'export { fix } from "./workflows"',
|
|
773
|
-
)
|
|
877
|
+
it("a config's doors are those of every workflow it exports, factory or workflow()", async () => {
|
|
878
|
+
withDoors(`{ hot: { args: [{ name: "ticket" }] } }`, 'export { fix } from "./workflows"')
|
|
774
879
|
writeFileSync(join(projectDir, "workflows.ts"), FACTORIES)
|
|
775
880
|
const { doors } = await getConfig()
|
|
776
881
|
expect([...doors.keys()].sort()).toEqual(["fix", "hot"])
|
|
@@ -779,39 +884,54 @@ describe("ConfigService", () => {
|
|
|
779
884
|
expect(doors.get("hot")?.args).toEqual([{ name: "ticket" }])
|
|
780
885
|
})
|
|
781
886
|
|
|
782
|
-
it("
|
|
783
|
-
|
|
784
|
-
|
|
887
|
+
it("a door named like a workflow it does not start warns that it hides that workflow", async () => {
|
|
888
|
+
const config = (fixDoors: string): void =>
|
|
889
|
+
writeFileSync(
|
|
890
|
+
join(projectDir, "gtd.config.ts"),
|
|
891
|
+
[
|
|
892
|
+
`import { human, workflow } from "@pmelab/gtd/flows"`,
|
|
893
|
+
`export default workflow(${flow("main")})`,
|
|
894
|
+
`export const fix = workflow(${flow("fix")}${fixDoors})`,
|
|
895
|
+
`export const hotfix = workflow(${flow("hot")}, { doors: { fix: {} } })`,
|
|
896
|
+
``,
|
|
897
|
+
].join("\n"),
|
|
898
|
+
)
|
|
899
|
+
config("")
|
|
900
|
+
const cfg = await getConfig()
|
|
901
|
+
expect(cfg.doors.get("fix")?.workflow).toBe("hotfix")
|
|
902
|
+
expect(cfg.warnings.filter((w) => w.path[0] === "doors")).toEqual([
|
|
903
|
+
{
|
|
904
|
+
severity: "warning",
|
|
905
|
+
path: ["doors", "fix"],
|
|
906
|
+
origin: join(projectDir, "gtd.config.ts"),
|
|
907
|
+
message:
|
|
908
|
+
'door "fix" starts workflow "hotfix" and hides the workflow of the same name from `gtd start fix` — rename one of them',
|
|
909
|
+
},
|
|
910
|
+
])
|
|
911
|
+
config(", { doors: { fix: {} } }")
|
|
912
|
+
expect((await getConfig()).warnings.filter((w) => w.path[0] === "doors")).toEqual([])
|
|
785
913
|
})
|
|
786
914
|
|
|
787
915
|
it("rejects a door name that is not lowercase-kebab", async () => {
|
|
788
|
-
withDoors(`{ Bad_Name: {
|
|
916
|
+
withDoors(`{ Bad_Name: {} }`)
|
|
789
917
|
await expect(getConfig()).rejects.toThrow(/door "Bad_Name".*\^\[a-z\]\[a-z0-9-\]\*\$/)
|
|
790
918
|
})
|
|
791
919
|
|
|
792
|
-
it("rejects a door whose workflow is not startable", async () => {
|
|
793
|
-
withDoors(`{ go: { workflow: "nope" } }`)
|
|
794
|
-
await expect(getConfig()).rejects.toThrow(/door "go".*workflow "nope"/)
|
|
795
|
-
})
|
|
796
|
-
|
|
797
920
|
it("rejects a required arg after an optional one", async () => {
|
|
798
|
-
withDoors(`{ go: {
|
|
921
|
+
withDoors(`{ go: { args: [{ name: "a", optional: true }, { name: "b" }] } }`)
|
|
799
922
|
await expect(getConfig()).rejects.toThrow(/door "go".*"b".*after an optional/)
|
|
800
923
|
})
|
|
801
924
|
|
|
802
925
|
it("rejects duplicate arg names", async () => {
|
|
803
|
-
withDoors(`{ go: {
|
|
926
|
+
withDoors(`{ go: { args: [{ name: "a" }, { name: "a" }] } }`)
|
|
804
927
|
await expect(getConfig()).rejects.toThrow(/door "go".*duplicate arg "a"/)
|
|
805
928
|
})
|
|
806
929
|
|
|
807
|
-
it("rejects
|
|
930
|
+
it("rejects doors that are not a record", async () => {
|
|
808
931
|
withDoors(`[1]`)
|
|
809
|
-
await expect(getConfig()).rejects.toThrow(
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
it("rejects a door that is not an object with a workflow name", async () => {
|
|
813
|
-
withDoors(`{ go: 3 }`)
|
|
814
|
-
await expect(getConfig()).rejects.toThrow(/door "go".*workflow/)
|
|
932
|
+
await expect(getConfig()).rejects.toThrow(
|
|
933
|
+
`workflow "hotfix"'s "doors" is not a record of doors`,
|
|
934
|
+
)
|
|
815
935
|
})
|
|
816
936
|
})
|
|
817
937
|
|
|
@@ -907,16 +1027,15 @@ describe("ConfigService", () => {
|
|
|
907
1027
|
)
|
|
908
1028
|
})
|
|
909
1029
|
|
|
910
|
-
it("loads a skills
|
|
911
|
-
const flow = minimalWorkflow("first")
|
|
1030
|
+
it("loads a skills option given as a record or as a function of the vars", async () => {
|
|
912
1031
|
writeFileSync(
|
|
913
1032
|
join(projectDir, "gtd.config.ts"),
|
|
914
|
-
|
|
1033
|
+
optionedWorkflow("first", `{ skills: { a: ["x"] } }`),
|
|
915
1034
|
)
|
|
916
1035
|
expect(Object.keys((await getConfig()).workflow.skills({}))).toEqual(["a"])
|
|
917
1036
|
writeFileSync(
|
|
918
1037
|
join(projectDir, "gtd.config.ts"),
|
|
919
|
-
|
|
1038
|
+
optionedWorkflow("first", `{ skills: (vars) => ({ [vars.k ?? "none"]: ["x"] }) }`),
|
|
920
1039
|
)
|
|
921
1040
|
expect(Object.keys((await getConfig()).workflow.skills({ k: "b" }))).toEqual(["b"])
|
|
922
1041
|
})
|
|
@@ -945,10 +1064,10 @@ describe("ConfigService", () => {
|
|
|
945
1064
|
expect(cfg.ui).toEqual({ format: "oxfmt --write $GTD_FILE" })
|
|
946
1065
|
})
|
|
947
1066
|
|
|
948
|
-
it("never interpolates the workflow's own `defaults`
|
|
1067
|
+
it("never interpolates the workflow's own `defaults`", async () => {
|
|
949
1068
|
writeFileSync(
|
|
950
1069
|
join(projectDir, "gtd.config.ts"),
|
|
951
|
-
|
|
1070
|
+
optionedWorkflow("first", `{ defaults: { greeting: "$LITERAL" } }`),
|
|
952
1071
|
)
|
|
953
1072
|
|
|
954
1073
|
const cfg = await getConfig(undefined, { LITERAL: "should-never-appear" })
|
|
@@ -1015,13 +1134,8 @@ describe("ConfigService — workflow factory options", () => {
|
|
|
1015
1134
|
expect(error).toMatch(/gtd\.config\.ts: testComand: workflow "default" option "testComand"/)
|
|
1016
1135
|
})
|
|
1017
1136
|
|
|
1018
|
-
it("a
|
|
1019
|
-
configWith(
|
|
1020
|
-
[
|
|
1021
|
-
`export default feature({ testComand: "x" } as never)`,
|
|
1022
|
-
`export const doors = { go: { workflow: "default" } }`,
|
|
1023
|
-
].join("\n"),
|
|
1024
|
-
)
|
|
1137
|
+
it("a rejected factory reports the option error, not the doors it would build", async () => {
|
|
1138
|
+
configWith(`export default feature({ testComand: "x" } as never)`)
|
|
1025
1139
|
|
|
1026
1140
|
const error = await getConfig().catch((e: Error) => e.message)
|
|
1027
1141
|
|
|
@@ -1049,7 +1163,7 @@ describe("ConfigService — a factory's built settings", () => {
|
|
|
1049
1163
|
await expect(getConfig()).rejects.toThrow(/workflow "audit" builds door "Bad_Name", which/)
|
|
1050
1164
|
})
|
|
1051
1165
|
|
|
1052
|
-
it("are held to the setting-name rule, like
|
|
1166
|
+
it("are held to the setting-name rule, like workflow()'s", async () => {
|
|
1053
1167
|
writeFileSync(
|
|
1054
1168
|
join(projectDir, "gtd.config.ts"),
|
|
1055
1169
|
[
|
|
@@ -1064,7 +1178,7 @@ describe("ConfigService — a factory's built settings", () => {
|
|
|
1064
1178
|
)
|
|
1065
1179
|
|
|
1066
1180
|
await expect(getConfig()).rejects.toThrow(
|
|
1067
|
-
/workflow "default"
|
|
1181
|
+
/workflow "default" declares defaults "x=y", not a valid setting name/,
|
|
1068
1182
|
)
|
|
1069
1183
|
})
|
|
1070
1184
|
|
|
@@ -1100,6 +1214,89 @@ describe("ConfigService — a factory's built settings", () => {
|
|
|
1100
1214
|
].join("\n"),
|
|
1101
1215
|
)
|
|
1102
1216
|
|
|
1103
|
-
await expect(getConfig()).rejects.toThrow(
|
|
1217
|
+
await expect(getConfig()).rejects.toThrow(
|
|
1218
|
+
`workflow "default"'s "defaults" is not a record of strings`,
|
|
1219
|
+
)
|
|
1220
|
+
})
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
describe("ConfigService — what a factory's build carries", () => {
|
|
1224
|
+
const factoryConfig = (build: string): void =>
|
|
1225
|
+
writeFileSync(
|
|
1226
|
+
join(projectDir, "gtd.config.ts"),
|
|
1227
|
+
[
|
|
1228
|
+
`import { defineWorkflow, human } from "@pmelab/gtd/flows"`,
|
|
1229
|
+
`const any = { "~standard": { version: 1, vendor: "t", validate: (value) => ({ value }) } }`,
|
|
1230
|
+
`export default defineWorkflow(any, () => ({`,
|
|
1231
|
+
` flow: async () => { await human("idle") },`,
|
|
1232
|
+
build,
|
|
1233
|
+
`}))`,
|
|
1234
|
+
``,
|
|
1235
|
+
].join("\n"),
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
it("skills, access, steering and summary reach the workflow", async () => {
|
|
1239
|
+
factoryConfig(
|
|
1240
|
+
[
|
|
1241
|
+
` skills: (vars) => ({ work: [vars.lens ?? "none"] }),`,
|
|
1242
|
+
` access: { work: { write: ["out/**"] } },`,
|
|
1243
|
+
` steering: { ".gtd/PLAN.md": "qa" },`,
|
|
1244
|
+
` summary: () => "built",`,
|
|
1245
|
+
].join("\n"),
|
|
1246
|
+
)
|
|
1247
|
+
const { workflow } = await getConfig()
|
|
1248
|
+
expect(workflow.skills({ lens: "x" })).toEqual({ work: ["x"] })
|
|
1249
|
+
expect(workflow.access({})).toEqual({ work: { write: ["out/**"] } })
|
|
1250
|
+
expect(workflow.steering).toEqual({ ".gtd/PLAN.md": "qa" })
|
|
1251
|
+
expect(workflow.summary?.({} as never)).toBe("built")
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
it("checks them as it checks workflow()'s options", async () => {
|
|
1255
|
+
factoryConfig(` skills: { work: "one" },`)
|
|
1256
|
+
await expect(getConfig()).rejects.toThrow(
|
|
1257
|
+
`workflow "default"'s "skills" is not a record of skill-name arrays`,
|
|
1258
|
+
)
|
|
1259
|
+
factoryConfig(` summary: "text",`)
|
|
1260
|
+
await expect(getConfig()).rejects.toThrow(`workflow "default"'s "summary" is not a function`)
|
|
1261
|
+
factoryConfig(` access: { work: { read: "src/**" } },`)
|
|
1262
|
+
await expect(getConfig()).rejects.toThrow(
|
|
1263
|
+
`workflow "default"'s "access": scope "work": access.read must be an array of glob strings`,
|
|
1264
|
+
)
|
|
1265
|
+
})
|
|
1266
|
+
})
|
|
1267
|
+
|
|
1268
|
+
describe("ConfigService — workflow()'s access option", () => {
|
|
1269
|
+
const withAccess = (access: string): void =>
|
|
1270
|
+
writeFileSync(
|
|
1271
|
+
join(projectDir, "gtd.config.ts"),
|
|
1272
|
+
optionedWorkflow("first", `{ access: ${access} }`),
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
it("accepts a record of { read?, write? } glob arrays, and a function", async () => {
|
|
1276
|
+
withAccess(`{ a: { read: ["x/**"], write: [] }, b: {}, c: { read: undefined } }`)
|
|
1277
|
+
expect((await getConfig()).workflow.access({})).toEqual({
|
|
1278
|
+
a: { read: ["x/**"], write: [] },
|
|
1279
|
+
b: {},
|
|
1280
|
+
c: { read: undefined },
|
|
1281
|
+
})
|
|
1282
|
+
withAccess(`(vars) => ({ [vars.k ?? "none"]: { write: "unchecked until resolved" } })`)
|
|
1283
|
+
expect(Object.keys((await getConfig()).workflow.access({ k: "b" }))).toEqual(["b"])
|
|
1284
|
+
})
|
|
1285
|
+
|
|
1286
|
+
it.each([
|
|
1287
|
+
[
|
|
1288
|
+
`{ a: { wirte: [] } }`,
|
|
1289
|
+
`workflow "default"'s "access": scope "a": unknown access key "wirte"`,
|
|
1290
|
+
],
|
|
1291
|
+
[
|
|
1292
|
+
`{ a: { read: [1] } }`,
|
|
1293
|
+
`workflow "default"'s "access": scope "a": access.read must be an array`,
|
|
1294
|
+
],
|
|
1295
|
+
[`{ a: ["x/**"] }`, `workflow "default"'s "access": scope "a": access must be an object`],
|
|
1296
|
+
[`["x/**"]`, `workflow "default"'s "access" is not a record of scope access`],
|
|
1297
|
+
[`"x/**"`, `workflow "default"'s "access" is not a record of scope access`],
|
|
1298
|
+
])("refuses %s at load", async (access, message) => {
|
|
1299
|
+
withAccess(access)
|
|
1300
|
+
await expect(getConfig()).rejects.toThrow(message)
|
|
1104
1301
|
})
|
|
1105
1302
|
})
|