@pmelab/gtd 23.2.0 → 24.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 +44 -11
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43830 -56044
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +1953 -1724
- package/package.json +70 -8
- package/skills/authoring/SKILL.md +5 -4
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +32 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +74 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +36 -2
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +99 -0
- package/src/workflows/review/review.ts +33 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/shared/workflows.steps.ts +7 -0
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
} from "node:fs"
|
|
11
|
+
import { join } from "node:path"
|
|
12
|
+
import { tmpdir } from "node:os"
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
14
|
+
import { Cause, Effect, Exit, Layer } from "effect"
|
|
15
|
+
import { NodeContext } from "@effect/platform-node"
|
|
16
|
+
import { Narrator, GitService, Host, Workspace } from "../platform/index.js"
|
|
17
|
+
import {
|
|
18
|
+
ConfigDiscovery,
|
|
19
|
+
ConfigService,
|
|
20
|
+
load,
|
|
21
|
+
refuseLegacyRc,
|
|
22
|
+
resolveScopeSkills,
|
|
23
|
+
resolveVars,
|
|
24
|
+
seededValidateCommand,
|
|
25
|
+
} from "./index.js"
|
|
26
|
+
import { CONFIG_SCAFFOLD } from "../install/index.js"
|
|
27
|
+
|
|
28
|
+
// Every real port `ConfigService.Live` needs, scoped to `dir` — `home` is a
|
|
29
|
+
// fresh directory above the project, so the walk stops there as it would in a
|
|
30
|
+
// real home, and no rc file of the machine running the suite is ever found.
|
|
31
|
+
type Env = Readonly<Record<string, string>>
|
|
32
|
+
|
|
33
|
+
const baseLayer = (dir: string, env: Env = {}) => {
|
|
34
|
+
const hostLayer = Host.layer({ root: dir, home: homeDir, env })
|
|
35
|
+
const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeContext.layer)))
|
|
36
|
+
const workspaceLayer = Workspace.Live.pipe(Layer.provide(Layer.merge(hostLayer, gitLayer)))
|
|
37
|
+
return Layer.mergeAll(hostLayer, gitLayer, workspaceLayer)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ConfigService.Live only loads/validates the config — it never writes.
|
|
41
|
+
// Narrator is a no-op here, MERGED (not just provided) so it stays in the
|
|
42
|
+
// output — these tests assert on the loaded config/failure, not on narration.
|
|
43
|
+
const layer = (dir: string, env: Env = {}) =>
|
|
44
|
+
Layer.mergeAll(
|
|
45
|
+
ConfigService.Live,
|
|
46
|
+
ConfigDiscovery.Live,
|
|
47
|
+
baseLayer(dir, env),
|
|
48
|
+
Narrator.layer(() => {}, false),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const run = <A>(
|
|
52
|
+
eff: Effect.Effect<A, Error, ConfigService | ConfigDiscovery | Narrator | Workspace | Host>,
|
|
53
|
+
dir: string = projectDir,
|
|
54
|
+
env: Env = {},
|
|
55
|
+
) => Effect.runPromise(eff.pipe(Effect.provide(layer(dir, env))))
|
|
56
|
+
|
|
57
|
+
const runExit = <A>(
|
|
58
|
+
eff: Effect.Effect<A, Error, ConfigService | ConfigDiscovery | Narrator | Workspace | Host>,
|
|
59
|
+
dir: string = projectDir,
|
|
60
|
+
) => Effect.runPromiseExit(eff.pipe(Effect.provide(layer(dir))))
|
|
61
|
+
|
|
62
|
+
const getConfig = (dir?: string, env: Env = {}) =>
|
|
63
|
+
run(
|
|
64
|
+
Effect.flatMap(ConfigService, (c) => c.load),
|
|
65
|
+
dir,
|
|
66
|
+
env,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// Config load no longer judges `skills:` keys; the resolver does, given vars.
|
|
70
|
+
const loadAndResolve = (dir: string = projectDir, vars: Env = {}) =>
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const c = yield* (yield* ConfigService).load
|
|
73
|
+
return yield* resolveScopeSkills(c.workflow, {
|
|
74
|
+
...resolveVars(c.defaultWorkflow.vars, {}, {}),
|
|
75
|
+
...vars,
|
|
76
|
+
})
|
|
77
|
+
}).pipe(Effect.provide(layer(dir)))
|
|
78
|
+
|
|
79
|
+
let homeDir: string
|
|
80
|
+
let projectDir: string
|
|
81
|
+
|
|
82
|
+
// Every test starts where `gtd install` leaves a repository.
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
homeDir = mkdtempSync(join(tmpdir(), "gtd-config-"))
|
|
85
|
+
projectDir = join(homeDir, "project")
|
|
86
|
+
mkdirSync(projectDir)
|
|
87
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), CONFIG_SCAFFOLD)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
rmSync(homeDir, { recursive: true, force: true })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// A one-step workflow whose only step is named `first`.
|
|
95
|
+
const minimalWorkflow = (first: string) =>
|
|
96
|
+
[
|
|
97
|
+
`import { human } from "@pmelab/gtd/flows"`,
|
|
98
|
+
``,
|
|
99
|
+
`export default async () => {`,
|
|
100
|
+
` await human("${first}")`,
|
|
101
|
+
`}`,
|
|
102
|
+
``,
|
|
103
|
+
].join("\n")
|
|
104
|
+
|
|
105
|
+
const withConfig = (config: string) =>
|
|
106
|
+
writeFileSync(
|
|
107
|
+
join(projectDir, "gtd.config.ts"),
|
|
108
|
+
`${CONFIG_SCAFFOLD}\nexport const config = ${config}\n`,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
describe("ConfigService", () => {
|
|
112
|
+
it("the scaffolded config runs the shipped feature workflow by default", async () => {
|
|
113
|
+
const cfg = await getConfig()
|
|
114
|
+
|
|
115
|
+
expect(cfg.workflow.initial).toBe("idle")
|
|
116
|
+
expect(cfg.defaultWorkflow.env["testCommand"]).toBe("npm test")
|
|
117
|
+
expect(cfg.defaultWorkflow.vars["testCommand"]).toBeUndefined()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("with no gtd.config.ts anywhere: fails, saying to run gtd install", async () => {
|
|
121
|
+
rmSync(join(projectDir, "gtd.config.ts"))
|
|
122
|
+
|
|
123
|
+
await expect(getConfig()).rejects.toThrow(
|
|
124
|
+
/no workflows configured: no gtd\.config\.ts here or above — run `gtd install`/,
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("refuses a leftover .gtdrc, naming the file and where each key went", async () => {
|
|
129
|
+
const file = join(projectDir, ".gtdrc.yaml")
|
|
130
|
+
writeFileSync(file, `env:\n testCommand: npm test\n`)
|
|
131
|
+
|
|
132
|
+
const error = await getConfig().catch((e: Error) => e.message)
|
|
133
|
+
|
|
134
|
+
expect(error).toContain(`${file}: no longer read — delete it`)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("links a migration anchor docs/configuration.md has a heading for", async () => {
|
|
138
|
+
writeFileSync(join(projectDir, ".gtdrc"), "{}\n")
|
|
139
|
+
const error = await getConfig().then(
|
|
140
|
+
() => "",
|
|
141
|
+
(e: Error) => e.message,
|
|
142
|
+
)
|
|
143
|
+
const anchor = error.match(/docs\/configuration\.md#([\w-]+)/)?.[1]
|
|
144
|
+
const doc = readFileSync(new URL("../../docs/configuration.md", import.meta.url), "utf8")
|
|
145
|
+
const slugs = [...doc.matchAll(/^#+ (.+)$/gm)].map(([, title]) =>
|
|
146
|
+
title!
|
|
147
|
+
.toLowerCase()
|
|
148
|
+
.replace(/[^\w\s-]/g, "")
|
|
149
|
+
.trim()
|
|
150
|
+
.replace(/\s+/g, "-"),
|
|
151
|
+
)
|
|
152
|
+
expect(slugs).toContain(anchor)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("only warns about a leftover rc file another user owns", async () => {
|
|
156
|
+
const warnings: string[] = []
|
|
157
|
+
const others = Layer.succeed(ConfigDiscovery, {
|
|
158
|
+
legacyRcFiles: () => Effect.succeed([{ path: "/shared/.gtdrc", owned: false }]),
|
|
159
|
+
workflowModule: () =>
|
|
160
|
+
Effect.succeed({ filepath: join(projectDir, "gtd.config.ts"), source: CONFIG_SCAFFOLD }),
|
|
161
|
+
})
|
|
162
|
+
const cfg = await Effect.runPromise(
|
|
163
|
+
Effect.flatMap(ConfigService, (c) => c.load).pipe(
|
|
164
|
+
Effect.provide(
|
|
165
|
+
Layer.mergeAll(
|
|
166
|
+
ConfigService.Live,
|
|
167
|
+
others,
|
|
168
|
+
baseLayer(projectDir),
|
|
169
|
+
Narrator.layer((m) => warnings.push(m), false),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
expect(cfg.workflow.initial).toBe("idle")
|
|
175
|
+
expect(warnings.join("\n")).toContain(
|
|
176
|
+
"/shared/.gtdrc is a removed .gtdrc file owned by another user",
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("refuses a user-level rc file in home, even when the walk never reaches home", async () => {
|
|
181
|
+
writeFileSync(join(homeDir, ".gtdrc.json"), `{ "$schema": "x" }\n`)
|
|
182
|
+
const outside = mkdtempSync(join(tmpdir(), "gtd-outside-"))
|
|
183
|
+
writeFileSync(join(outside, "gtd.config.ts"), CONFIG_SCAFFOLD)
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await expect(getConfig()).rejects.toThrow(`${join(homeDir, ".gtdrc.json")}: no longer read`)
|
|
187
|
+
await expect(getConfig(outside)).rejects.toThrow(
|
|
188
|
+
`${join(homeDir, ".gtdrc.json")}: no longer read`,
|
|
189
|
+
)
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(outside, { recursive: true, force: true })
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("refuses every rc file found, also without a gtd.config.ts", async () => {
|
|
196
|
+
rmSync(join(projectDir, "gtd.config.ts"))
|
|
197
|
+
writeFileSync(join(projectDir, ".gtdrc"), `{}\n`)
|
|
198
|
+
writeFileSync(join(projectDir, "gtd.config.json"), `{}\n`)
|
|
199
|
+
|
|
200
|
+
const error = await getConfig().catch((e: Error) => e.message)
|
|
201
|
+
|
|
202
|
+
expect(error).toContain(`${join(projectDir, ".gtdrc")}: no longer read`)
|
|
203
|
+
expect(error).toContain(`${join(projectDir, "gtd.config.json")}: no longer read`)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("ignores a directory merely named .gtdrc", async () => {
|
|
207
|
+
mkdirSync(join(projectDir, ".gtdrc"))
|
|
208
|
+
|
|
209
|
+
expect((await getConfig()).workflow.initial).toBe("idle")
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("refuseLegacyRc needs no gtd.config.ts", async () => {
|
|
213
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), `throw new Error("never evaluated")\n`)
|
|
214
|
+
await expect(run(refuseLegacyRc)).resolves.toBeUndefined()
|
|
215
|
+
writeFileSync(join(projectDir, ".gtdrc.yaml"), `judge:\n provider: fixed\n`)
|
|
216
|
+
await expect(run(refuseLegacyRc)).rejects.toThrow(/no longer read/)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it("a workflow declaring a name in both defaults and envDefaults fails to load", async () => {
|
|
220
|
+
writeFileSync(
|
|
221
|
+
join(projectDir, "gtd.config.ts"),
|
|
222
|
+
`${minimalWorkflow("first")}export const defaults = { x: "1" }\nexport const envDefaults = { x: "2" }\n`,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
await expect(getConfig()).rejects.toThrow(`"x" declared in both "defaults" and "envDefaults"`)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it("layers `config.modes` over the built-in modes, per half", async () => {
|
|
229
|
+
withConfig(`{ modes: { qa: { format: "adr-fmt $GTD_FILE" } } }`)
|
|
230
|
+
|
|
231
|
+
const cfg = await getConfig()
|
|
232
|
+
|
|
233
|
+
expect(cfg.workflow.modes["qa"]).toEqual({
|
|
234
|
+
format: "adr-fmt $GTD_FILE",
|
|
235
|
+
validate: seededValidateCommand("qa"),
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("accepts a config built with defineConfig", async () => {
|
|
240
|
+
writeFileSync(
|
|
241
|
+
join(projectDir, "gtd.config.ts"),
|
|
242
|
+
[
|
|
243
|
+
`import { defineConfig } from "@pmelab/gtd/flows"`,
|
|
244
|
+
CONFIG_SCAFFOLD,
|
|
245
|
+
`export const config = defineConfig({ ui: { port: 4173 } })`,
|
|
246
|
+
``,
|
|
247
|
+
].join("\n"),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
expect((await getConfig()).ui).toEqual({ port: 4173 })
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("defineConfig throws on a removed key, failing the load at gtd.config.ts", async () => {
|
|
254
|
+
writeFileSync(
|
|
255
|
+
join(projectDir, "gtd.config.ts"),
|
|
256
|
+
[
|
|
257
|
+
`import { defineConfig } from "@pmelab/gtd/flows"`,
|
|
258
|
+
CONFIG_SCAFFOLD,
|
|
259
|
+
`export const config = defineConfig({ env: { testCommand: "x" } })`,
|
|
260
|
+
``,
|
|
261
|
+
].join("\n"),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
await expect(getConfig()).rejects.toThrow("gtd.config.ts: config.env: is not a known key")
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("rejects an unknown key in a plain `config` object too", async () => {
|
|
268
|
+
withConfig(`{ vars: { greeting: "hi" }, ui: { bogus: true } }`)
|
|
269
|
+
|
|
270
|
+
const error = await getConfig().catch((e: Error) => e.message)
|
|
271
|
+
|
|
272
|
+
expect(error).toContain("gtd.config.ts: config.ui.bogus: is not a known key")
|
|
273
|
+
expect(error).toContain("gtd.config.ts: config.vars: is not a known key")
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it("the reserved `config` export is never a workflow", async () => {
|
|
277
|
+
withConfig(`{}`)
|
|
278
|
+
|
|
279
|
+
expect((await getConfig()).workflowNames).not.toContain("config")
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("reads a custom workflow from gtd.config.ts in cwd", async () => {
|
|
283
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), minimalWorkflow("custom-idle"))
|
|
284
|
+
|
|
285
|
+
const cfg = await getConfig()
|
|
286
|
+
|
|
287
|
+
expect(cfg.workflow.initial).toBe("custom-idle")
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it("reads `defaults`, `summary`, `base` and `steering` off the module and ignores its other exports", async () => {
|
|
291
|
+
writeFileSync(
|
|
292
|
+
join(projectDir, "gtd.config.ts"),
|
|
293
|
+
[
|
|
294
|
+
minimalWorkflow("first"),
|
|
295
|
+
`export const defaults = { greeting: "hi" }`,
|
|
296
|
+
`export const summary = () => "sum"`,
|
|
297
|
+
`export const base = () => undefined`,
|
|
298
|
+
`export const steering = { ".gtd/PLAN.md": "qa" }`,
|
|
299
|
+
`export const helper = 42`,
|
|
300
|
+
``,
|
|
301
|
+
].join("\n"),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
const cfg = await getConfig()
|
|
305
|
+
|
|
306
|
+
expect(cfg.workflow.initial).toBe("first")
|
|
307
|
+
expect(cfg.defaultWorkflow.vars).toEqual({ greeting: "hi" })
|
|
308
|
+
expect(typeof cfg.workflow.summary).toBe("function")
|
|
309
|
+
expect(typeof cfg.workflow.base).toBe("function")
|
|
310
|
+
expect(cfg.workflow.steering).toEqual({ ".gtd/PLAN.md": "qa" })
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("workflowFiles names gtd.config.ts AND a sibling module it imports from — a caller watching only gtd.config.ts's own path misses that module's edits", async () => {
|
|
314
|
+
writeFileSync(
|
|
315
|
+
join(projectDir, "steps.ts"),
|
|
316
|
+
[`export const steering = { ".gtd/PLAN.md": "qa" }`, ``].join("\n"),
|
|
317
|
+
)
|
|
318
|
+
writeFileSync(
|
|
319
|
+
join(projectDir, "gtd.config.ts"),
|
|
320
|
+
[minimalWorkflow("first"), `export { steering } from "./steps.js"`, ``].join("\n"),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const cfg = await getConfig()
|
|
324
|
+
|
|
325
|
+
expect(cfg.workflow.steering[".gtd/PLAN.md"]).toBe("qa")
|
|
326
|
+
expect(cfg.workflowFiles.map((f) => realpathSync(f))).toEqual(
|
|
327
|
+
expect.arrayContaining([
|
|
328
|
+
realpathSync(join(projectDir, "gtd.config.ts")),
|
|
329
|
+
realpathSync(join(projectDir, "steps.ts")),
|
|
330
|
+
]),
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it("workflowFiles names gtd.config.ts but no shipped module it imports", async () => {
|
|
335
|
+
const cfg = await getConfig()
|
|
336
|
+
|
|
337
|
+
expect(cfg.workflowFiles).toHaveLength(1)
|
|
338
|
+
expect(cfg.workflowFiles[0]).toMatch(/\/gtd\.config\.ts$/)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it.each([
|
|
342
|
+
[`export const defaults = { n: 1 }`, /"defaults" export is not a record of strings/],
|
|
343
|
+
[`export const summary = "text"`, /"summary" export is not a function/],
|
|
344
|
+
[
|
|
345
|
+
`export const steering = { ".gtd/PLAN.md": 1 }`,
|
|
346
|
+
/"steering" export is not a record of strings/,
|
|
347
|
+
],
|
|
348
|
+
])("rejects a malformed named export: %s", async (line, message) => {
|
|
349
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), `${minimalWorkflow("first")}${line}\n`)
|
|
350
|
+
|
|
351
|
+
await expect(getConfig()).rejects.toThrow(message)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const firstStepWorkflow = (body: string) =>
|
|
355
|
+
[
|
|
356
|
+
`import { human, vars, env } from "@pmelab/gtd/flows"`,
|
|
357
|
+
`export default async () => {`,
|
|
358
|
+
` ${body}`,
|
|
359
|
+
`}`,
|
|
360
|
+
``,
|
|
361
|
+
].join("\n")
|
|
362
|
+
|
|
363
|
+
it.each([
|
|
364
|
+
[
|
|
365
|
+
"a vars read",
|
|
366
|
+
`await human(vars.route === "x" ? "a" : "b")`,
|
|
367
|
+
'reads the process setting "route"',
|
|
368
|
+
],
|
|
369
|
+
["an env read", `await human(env.checker ?? "b")`, 'reads the environment setting "checker"'],
|
|
370
|
+
[
|
|
371
|
+
"several reads, sorted",
|
|
372
|
+
`await human(("z" in vars) || vars.b ? "a" : "b")`,
|
|
373
|
+
'reads the process settings "b", "z"',
|
|
374
|
+
],
|
|
375
|
+
[
|
|
376
|
+
"an enumeration",
|
|
377
|
+
`await human(Object.keys(vars).length > 0 ? "a" : "b")`,
|
|
378
|
+
"reads every process setting",
|
|
379
|
+
],
|
|
380
|
+
[
|
|
381
|
+
"an env enumeration",
|
|
382
|
+
`await human(Object.keys(env).length > 0 ? "a" : "b")`,
|
|
383
|
+
"reads every environment setting",
|
|
384
|
+
],
|
|
385
|
+
[
|
|
386
|
+
"a read only in the message",
|
|
387
|
+
`await human("x", { message: \`m \${vars.route}\` })`,
|
|
388
|
+
'reads the process setting "route"',
|
|
389
|
+
],
|
|
390
|
+
])("refuses a first step that depends on a setting: %s", async (_name, body, expected) => {
|
|
391
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), firstStepWorkflow(body))
|
|
392
|
+
|
|
393
|
+
await expect(getConfig()).rejects.toThrow(expected)
|
|
394
|
+
await expect(getConfig()).rejects.toThrow(/must not depend on a setting/)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it("refuses a workflow default whose name is not a valid setting name", async () => {
|
|
398
|
+
writeFileSync(
|
|
399
|
+
join(projectDir, "gtd.config.ts"),
|
|
400
|
+
[
|
|
401
|
+
minimalWorkflow("first"),
|
|
402
|
+
`export const defaults = { "a=b": "1" }`,
|
|
403
|
+
`export const envDefaults = { "c d": "1" }`,
|
|
404
|
+
].join("\n"),
|
|
405
|
+
)
|
|
406
|
+
await expect(getConfig()).rejects.toThrow(
|
|
407
|
+
'the "defaults" export declares "a=b", not a valid setting name — a setting name is a letter or "_", then letters, digits or "_"',
|
|
408
|
+
)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it("refuses an envDefaults name that is not a valid setting name", async () => {
|
|
412
|
+
writeFileSync(
|
|
413
|
+
join(projectDir, "gtd.config.ts"),
|
|
414
|
+
[minimalWorkflow("first"), `export const envDefaults = { "c d": "1" }`].join("\n"),
|
|
415
|
+
)
|
|
416
|
+
await expect(getConfig()).rejects.toThrow('the "envDefaults" export declares "c d"')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it("refuses a flow whose first step depends on the repository's files", async () => {
|
|
420
|
+
execSync("git init -q && git config user.email t@t && git config user.name T", {
|
|
421
|
+
cwd: projectDir,
|
|
422
|
+
})
|
|
423
|
+
writeFileSync(join(projectDir, "FIRST.md"), "from-head")
|
|
424
|
+
writeFileSync(
|
|
425
|
+
join(projectDir, "gtd.config.ts"),
|
|
426
|
+
[
|
|
427
|
+
`import { human, read } from "@pmelab/gtd/flows"`,
|
|
428
|
+
`export default async () => {`,
|
|
429
|
+
` await human(read("FIRST.md") ?? "no-file")`,
|
|
430
|
+
`}`,
|
|
431
|
+
``,
|
|
432
|
+
].join("\n"),
|
|
433
|
+
)
|
|
434
|
+
execSync("git add -A && git commit -q -m init", { cwd: projectDir })
|
|
435
|
+
|
|
436
|
+
await expect(getConfig()).rejects.toThrow(
|
|
437
|
+
/first step on an ordinary start depends on the repository's files \("no-file" without them, "from-head" at HEAD\)/,
|
|
438
|
+
)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it("accepts a first step that reads the repository for its content only", async () => {
|
|
442
|
+
execSync("git init -q && git config user.email t@t && git config user.name T", {
|
|
443
|
+
cwd: projectDir,
|
|
444
|
+
})
|
|
445
|
+
writeFileSync(join(projectDir, "NOTE.md"), "a note")
|
|
446
|
+
writeFileSync(
|
|
447
|
+
join(projectDir, "gtd.config.ts"),
|
|
448
|
+
[
|
|
449
|
+
`import { human, read } from "@pmelab/gtd/flows"`,
|
|
450
|
+
`export default async () => {`,
|
|
451
|
+
` await human("idle", { message: read("NOTE.md") ?? "" })`,
|
|
452
|
+
`}`,
|
|
453
|
+
``,
|
|
454
|
+
].join("\n"),
|
|
455
|
+
)
|
|
456
|
+
execSync("git add -A && git commit -q -m init", { cwd: projectDir })
|
|
457
|
+
|
|
458
|
+
expect((await getConfig()).workflow.initial).toBe("idle")
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it("takes the innermost gtd.config.ts — workflows are never merged", async () => {
|
|
462
|
+
const child = join(projectDir, "a", "b")
|
|
463
|
+
mkdirSync(child, { recursive: true })
|
|
464
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), minimalWorkflow("ancestor-idle"))
|
|
465
|
+
writeFileSync(join(child, "gtd.config.ts"), minimalWorkflow("child-idle"))
|
|
466
|
+
|
|
467
|
+
const cfg = await getConfig(child)
|
|
468
|
+
|
|
469
|
+
expect(cfg.workflow.initial).toBe("child-idle")
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it("rejects a gtd.config.ts whose default export is not a function", async () => {
|
|
473
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), `export default { nope: true }\n`)
|
|
474
|
+
|
|
475
|
+
await expect(getConfig()).rejects.toThrow(/the default export is not a function/)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
describe("named workflows", () => {
|
|
479
|
+
const flow = (first: string) => `async () => {\n await human("${first}")\n}`
|
|
480
|
+
const header = `import { human } from "@pmelab/gtd/flows"\n`
|
|
481
|
+
|
|
482
|
+
it("the scaffolded config: feature, review and fix are startable, default is not", async () => {
|
|
483
|
+
const cfg = await getConfig()
|
|
484
|
+
expect(cfg.workflowNames).toEqual(["feature", "fix", "review"])
|
|
485
|
+
expect(cfg.defaultWorkflow.name).toBe("feature")
|
|
486
|
+
expect(cfg.workflowNamed("default")).toBeUndefined()
|
|
487
|
+
expect(cfg.workflowNamed("fix")?.def.initial).toBe("idle")
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it("every exported function but the reserved names is a workflow, named by its export", async () => {
|
|
491
|
+
writeFileSync(
|
|
492
|
+
join(projectDir, "gtd.config.ts"),
|
|
493
|
+
[
|
|
494
|
+
header,
|
|
495
|
+
`export default ${flow("main")}`,
|
|
496
|
+
`export const hotfix = ${flow("hot")}`,
|
|
497
|
+
`export const defaults = { greeting: "hi" }`,
|
|
498
|
+
`export const summary = () => "sum"`,
|
|
499
|
+
`export const base = () => undefined`,
|
|
500
|
+
`export const steering = {}`,
|
|
501
|
+
`export const skills = {}`,
|
|
502
|
+
`export const notAFlow = 42`,
|
|
503
|
+
``,
|
|
504
|
+
].join("\n"),
|
|
505
|
+
)
|
|
506
|
+
const cfg = await getConfig()
|
|
507
|
+
expect(cfg.workflowNames).toEqual(["hotfix"])
|
|
508
|
+
expect(cfg.defaultWorkflow.name).toBe("default")
|
|
509
|
+
expect(cfg.workflow.initial).toBe("main")
|
|
510
|
+
expect(cfg.workflowNamed("hotfix")?.vars).toEqual({ greeting: "hi" })
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it("a config with no default export fails, saying what to export", async () => {
|
|
514
|
+
writeFileSync(
|
|
515
|
+
join(projectDir, "gtd.config.ts"),
|
|
516
|
+
[header, `export const hotfix = ${flow("hot")}`, ``].join("\n"),
|
|
517
|
+
)
|
|
518
|
+
await expect(getConfig()).rejects.toThrow(/no default export.*`gtd install`/)
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it("a default that is also a named export goes by that name", async () => {
|
|
522
|
+
writeFileSync(
|
|
523
|
+
join(projectDir, "gtd.config.ts"),
|
|
524
|
+
[header, `export const hotfix = ${flow("hot")}`, `export default hotfix`, ``].join("\n"),
|
|
525
|
+
)
|
|
526
|
+
expect((await getConfig()).defaultWorkflow.name).toBe("hotfix")
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it("a helper re-exported from the shipped module becomes a startable workflow", async () => {
|
|
530
|
+
writeFileSync(
|
|
531
|
+
join(projectDir, "gtd.config.ts"),
|
|
532
|
+
[`export { feature as default, unwind } from "@pmelab/gtd/workflows"`, ``].join("\n"),
|
|
533
|
+
)
|
|
534
|
+
expect((await getConfig()).workflowNames).toContain("unwind")
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it("the old @pmelab/gtd/workflow subpath serves the same module", async () => {
|
|
538
|
+
writeFileSync(
|
|
539
|
+
join(projectDir, "gtd.config.ts"),
|
|
540
|
+
[`export { feature as default, feature } from "@pmelab/gtd/workflow"`, ``].join("\n"),
|
|
541
|
+
)
|
|
542
|
+
expect((await getConfig()).defaultWorkflow.name).toBe("feature")
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it("each shipped workflow has its own subpath", async () => {
|
|
546
|
+
writeFileSync(
|
|
547
|
+
join(projectDir, "gtd.config.ts"),
|
|
548
|
+
[
|
|
549
|
+
`export { feature as default, feature } from "@pmelab/gtd/workflows/feature"`,
|
|
550
|
+
`export { review } from "@pmelab/gtd/workflows/review"`,
|
|
551
|
+
`export { fix } from "@pmelab/gtd/workflows/fix"`,
|
|
552
|
+
``,
|
|
553
|
+
].join("\n"),
|
|
554
|
+
)
|
|
555
|
+
const config = await getConfig()
|
|
556
|
+
expect(config.defaultWorkflow.name).toBe("feature")
|
|
557
|
+
expect(config.workflowNames).toEqual(["feature", "fix", "review"])
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it("a shipped subpath only a helper file imports resolves", async () => {
|
|
561
|
+
writeFileSync(
|
|
562
|
+
join(projectDir, "kit.ts"),
|
|
563
|
+
[
|
|
564
|
+
`import { lensesOf } from "@pmelab/gtd/workflow-kit"`,
|
|
565
|
+
`export const kit = typeof lensesOf`,
|
|
566
|
+
``,
|
|
567
|
+
].join("\n"),
|
|
568
|
+
)
|
|
569
|
+
writeFileSync(
|
|
570
|
+
join(projectDir, "gtd.config.ts"),
|
|
571
|
+
[
|
|
572
|
+
header,
|
|
573
|
+
`import { kit } from "./kit"`,
|
|
574
|
+
`export default ${flow("main")}`,
|
|
575
|
+
'export const steering = { [`.gtd/${kit}.md`]: "qa" }',
|
|
576
|
+
``,
|
|
577
|
+
].join("\n"),
|
|
578
|
+
)
|
|
579
|
+
expect((await getConfig()).workflow.steering[".gtd/function.md"]).toBe("qa")
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it("steering is the config's own export", async () => {
|
|
583
|
+
writeFileSync(
|
|
584
|
+
join(projectDir, "gtd.config.ts"),
|
|
585
|
+
[
|
|
586
|
+
header,
|
|
587
|
+
`export default ${flow("main")}`,
|
|
588
|
+
`export const steering = { ".gtd/REVIEW.md": "qa", ".gtd/X.md": "qa" }`,
|
|
589
|
+
``,
|
|
590
|
+
].join("\n"),
|
|
591
|
+
)
|
|
592
|
+
const steering = (await getConfig()).workflow.steering
|
|
593
|
+
expect(steering[".gtd/REVIEW.md"]).toBe("qa")
|
|
594
|
+
expect(steering[".gtd/X.md"]).toBe("qa")
|
|
595
|
+
expect(steering[".gtd/REQUIREMENTS.md"]).toBeUndefined()
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
describe("doors", () => {
|
|
600
|
+
const flow = (first: string) => `async () => {\n await human("${first}")\n}`
|
|
601
|
+
const header = `import { human } from "@pmelab/gtd/flows"\n`
|
|
602
|
+
const withDoors = (doors: string, extra = ""): void =>
|
|
603
|
+
writeFileSync(
|
|
604
|
+
join(projectDir, "gtd.config.ts"),
|
|
605
|
+
[
|
|
606
|
+
header,
|
|
607
|
+
`export default ${flow("main")}`,
|
|
608
|
+
`export const hotfix = ${flow("hot")}`,
|
|
609
|
+
extra,
|
|
610
|
+
`export const doors = ${doors}`,
|
|
611
|
+
``,
|
|
612
|
+
].join("\n"),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
it("the scaffolded config: the fix and review doors, review taking an optional base", async () => {
|
|
616
|
+
const { doors } = await getConfig()
|
|
617
|
+
expect([...doors.keys()].sort()).toEqual(["fix", "review"])
|
|
618
|
+
expect(doors.get("fix")?.workflow).toBe("fix")
|
|
619
|
+
expect(doors.get("review")?.args).toEqual([{ name: "base", optional: true }])
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it("a config exporting only fix gets only fix's door", async () => {
|
|
623
|
+
writeFileSync(
|
|
624
|
+
join(projectDir, "gtd.config.ts"),
|
|
625
|
+
[
|
|
626
|
+
header,
|
|
627
|
+
`export default ${flow("main")}`,
|
|
628
|
+
`export { fix } from "@pmelab/gtd/workflows"`,
|
|
629
|
+
``,
|
|
630
|
+
].join("\n"),
|
|
631
|
+
)
|
|
632
|
+
const { doors } = await getConfig()
|
|
633
|
+
expect([...doors.keys()]).toEqual(["fix"])
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it("a door follows the name its workflow is exported under", async () => {
|
|
637
|
+
writeFileSync(
|
|
638
|
+
join(projectDir, "gtd.config.ts"),
|
|
639
|
+
`export { feature as default, review as audit } from "@pmelab/gtd/workflows"\n`,
|
|
640
|
+
)
|
|
641
|
+
const door = (await getConfig()).doors.get("review")
|
|
642
|
+
expect(door?.workflow).toBe("audit")
|
|
643
|
+
expect(door?.vars?.({ base: "main" })).toEqual({ reviewBase: "main" })
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
it("a review factory exported as audit reviews the default branch, or an explicit base", async () => {
|
|
647
|
+
writeFileSync(
|
|
648
|
+
join(projectDir, "gtd.config.ts"),
|
|
649
|
+
`export { feature as default, review as audit } from "@pmelab/gtd/workflows"\n`,
|
|
650
|
+
)
|
|
651
|
+
const audit = (await getConfig()).workflowNamed("audit")
|
|
652
|
+
expect(audit?.def.base?.("audit", {})).toBe("")
|
|
653
|
+
expect(audit?.def.base?.("audit", { reviewBase: "v1" })).toBe("v1")
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
it("a config's own base export wins over the review factory's", async () => {
|
|
657
|
+
writeFileSync(
|
|
658
|
+
join(projectDir, "gtd.config.ts"),
|
|
659
|
+
[
|
|
660
|
+
`export { feature as default, review } from "@pmelab/gtd/workflows"`,
|
|
661
|
+
`export const base = (w: string) => (w === "review" ? "develop" : undefined)`,
|
|
662
|
+
``,
|
|
663
|
+
].join("\n"),
|
|
664
|
+
)
|
|
665
|
+
expect((await getConfig()).workflowNamed("review")?.def.base?.("review", {})).toBe("develop")
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it("a door two workflows build is a config error until the doors export settles it", async () => {
|
|
669
|
+
const both = `export { feature as default, fix, fix as fix2 } from "@pmelab/gtd/workflows"`
|
|
670
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), `${both}\n`)
|
|
671
|
+
await expect(getConfig()).rejects.toThrow(/door "fix" is built by workflows "fix" and "fix2"/)
|
|
672
|
+
writeFileSync(
|
|
673
|
+
join(projectDir, "gtd.config.ts"),
|
|
674
|
+
`${both}\nexport const doors = { fix: { workflow: "fix2" } }\n`,
|
|
675
|
+
)
|
|
676
|
+
expect((await getConfig()).doors.get("fix")?.workflow).toBe("fix2")
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it("a config's doors merge derived doors (exported workflows) with declared doors (doors export)", async () => {
|
|
680
|
+
withDoors(
|
|
681
|
+
`{ hot: { workflow: "hotfix", args: [{ name: "ticket" }] } }`,
|
|
682
|
+
'export { fix } from "@pmelab/gtd/workflows"',
|
|
683
|
+
)
|
|
684
|
+
const { doors } = await getConfig()
|
|
685
|
+
expect([...doors.keys()].sort()).toEqual(["fix", "hot"])
|
|
686
|
+
expect(doors.get("fix")).toBeDefined()
|
|
687
|
+
expect(doors.get("hot")?.workflow).toBe("hotfix")
|
|
688
|
+
expect(doors.get("hot")?.args).toEqual([{ name: "ticket" }])
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
it("the reserved `doors` export is never a workflow", async () => {
|
|
692
|
+
withDoors(`{}`)
|
|
693
|
+
expect((await getConfig()).workflowNames).toEqual(["hotfix"])
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
it("rejects a door name that is not lowercase-kebab", async () => {
|
|
697
|
+
withDoors(`{ Bad_Name: { workflow: "fix" } }`)
|
|
698
|
+
await expect(getConfig()).rejects.toThrow(/door "Bad_Name".*\^\[a-z\]\[a-z0-9-\]\*\$/)
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it("rejects a door whose workflow is not startable", async () => {
|
|
702
|
+
withDoors(`{ go: { workflow: "nope" } }`)
|
|
703
|
+
await expect(getConfig()).rejects.toThrow(/door "go".*workflow "nope"/)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
it("rejects a required arg after an optional one", async () => {
|
|
707
|
+
withDoors(`{ go: { workflow: "fix", args: [{ name: "a", optional: true }, { name: "b" }] } }`)
|
|
708
|
+
await expect(getConfig()).rejects.toThrow(/door "go".*"b".*after an optional/)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
it("rejects duplicate arg names", async () => {
|
|
712
|
+
withDoors(`{ go: { workflow: "fix", args: [{ name: "a" }, { name: "a" }] } }`)
|
|
713
|
+
await expect(getConfig()).rejects.toThrow(/door "go".*duplicate arg "a"/)
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it("rejects a `doors` export that is not a record", async () => {
|
|
717
|
+
withDoors(`[1]`)
|
|
718
|
+
await expect(getConfig()).rejects.toThrow(/the "doors" export is not a record/)
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
it("rejects a door that is not an object with a workflow name", async () => {
|
|
722
|
+
withDoors(`{ go: 3 }`)
|
|
723
|
+
await expect(getConfig()).rejects.toThrow(/door "go".*workflow/)
|
|
724
|
+
})
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
it("reads `config.ui` through as-is", async () => {
|
|
728
|
+
withConfig(`{ ui: { port: 4173, host: "0.0.0.0" } }`)
|
|
729
|
+
|
|
730
|
+
expect((await getConfig()).ui).toEqual({ port: 4173, host: "0.0.0.0" })
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
it("rejects a wrong-typed `config.ui` key", async () => {
|
|
734
|
+
withConfig(`{ ui: { port: "nope" } }`)
|
|
735
|
+
|
|
736
|
+
await expect(getConfig()).rejects.toThrow("config.ui.port: must be an integer")
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
it("lets `config.modes` define the mode a custom workflow's step names", async () => {
|
|
740
|
+
writeFileSync(
|
|
741
|
+
join(projectDir, "gtd.config.ts"),
|
|
742
|
+
[
|
|
743
|
+
`import { human } from "@pmelab/gtd/flows"`,
|
|
744
|
+
``,
|
|
745
|
+
`export default async () => {`,
|
|
746
|
+
` await human("idle", { message: "hi", file: "docs/adr.md", mode: "adr" })`,
|
|
747
|
+
`}`,
|
|
748
|
+
`export const config = { modes: { adr: { validate: "adr-lint $GTD_FILE" } } }`,
|
|
749
|
+
``,
|
|
750
|
+
].join("\n"),
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
const cfg = await getConfig()
|
|
754
|
+
|
|
755
|
+
expect(cfg.workflow.modes["adr"]).toEqual({ validate: "adr-lint $GTD_FILE" })
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it("rejects a default entry that never reaches a step", async () => {
|
|
759
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), `export default async () => {}\n`)
|
|
760
|
+
|
|
761
|
+
await expect(getConfig()).rejects.toThrow(/the flow returned without reaching any step/)
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
it("rejects a malformed `config.modes` entry", async () => {
|
|
765
|
+
withConfig(`{ modes: { adr: { lint: "adr-lint" } } }`)
|
|
766
|
+
|
|
767
|
+
await expect(getConfig()).rejects.toThrow("config.modes.adr.lint: is not a known key")
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
it("rejects a `config.skills` key that only matches an inherited object property, like `toString`", async () => {
|
|
771
|
+
withConfig(`{ skills: { toString: ["x"] } }`)
|
|
772
|
+
|
|
773
|
+
const exit = await Effect.runPromiseExit(loadAndResolve())
|
|
774
|
+
|
|
775
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
776
|
+
if (Exit.isFailure(exit)) {
|
|
777
|
+
expect(String(exit.cause)).toContain('"skills.toString" is not a scope that runs a turn')
|
|
778
|
+
}
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
describe("`config.skills` keys are scope names", () => {
|
|
782
|
+
const loadError = async (config: string): Promise<string> => {
|
|
783
|
+
withConfig(config)
|
|
784
|
+
const exit = await Effect.runPromiseExit(loadAndResolve())
|
|
785
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
786
|
+
return Exit.isFailure(exit) ? String(Cause.squash(exit.cause)) : ""
|
|
787
|
+
}
|
|
788
|
+
const keyed = (key: string): string => `{ skills: { "${key}": ["x"] } }`
|
|
789
|
+
|
|
790
|
+
it("names the key and lists every known scope", async () => {
|
|
791
|
+
const message = await loadError(keyed("nope"))
|
|
792
|
+
expect(message).toContain('"skills.nope" is not a scope that runs a turn — known scopes: ')
|
|
793
|
+
expect(message).toContain("architecture, architecture.decompose, build,")
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it("gives a group key the same plain error", async () => {
|
|
797
|
+
expect(await loadError(keyed("build.fix"))).toContain("known scopes: ")
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
it("accepts a scope key and keeps it in configuredSkills", async () => {
|
|
801
|
+
withConfig(keyed("build.review"))
|
|
802
|
+
expect((await getConfig()).workflow.configuredSkills).toEqual({ "build.review": ["x"] })
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
it("validates a lens key only while the lens is in qualityReviews", async () => {
|
|
806
|
+
withConfig(keyed("build.quality.my-lens"))
|
|
807
|
+
expect((await getConfig()).workflow.configuredSkills).toEqual({
|
|
808
|
+
"build.quality.my-lens": ["x"],
|
|
809
|
+
})
|
|
810
|
+
const withLens = await Effect.runPromise(
|
|
811
|
+
loadAndResolve(projectDir, { qualityReviews: "my-lens" }),
|
|
812
|
+
)
|
|
813
|
+
expect(Object.keys(withLens)).toContain("build.quality.my-lens")
|
|
814
|
+
expect(await loadError(keyed("build.quality.my-lens"))).toContain(
|
|
815
|
+
'"skills.build.quality.my-lens" is not a scope that runs a turn',
|
|
816
|
+
)
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
it("loads a skills export given as a record or as a function of the vars", async () => {
|
|
820
|
+
const flow = minimalWorkflow("first")
|
|
821
|
+
writeFileSync(
|
|
822
|
+
join(projectDir, "gtd.config.ts"),
|
|
823
|
+
`${flow}export const skills = { a: ["x"] }\n`,
|
|
824
|
+
)
|
|
825
|
+
expect(Object.keys((await getConfig()).workflow.skills({}))).toEqual(["a"])
|
|
826
|
+
writeFileSync(
|
|
827
|
+
join(projectDir, "gtd.config.ts"),
|
|
828
|
+
`${flow}export const skills = (vars) => ({ [vars.k ?? "none"]: ["x"] })\n`,
|
|
829
|
+
)
|
|
830
|
+
expect(Object.keys((await getConfig()).workflow.skills({ k: "b" }))).toEqual(["b"])
|
|
831
|
+
})
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
it("loading config never writes a file (ConfigService.Live is read-only)", async () => {
|
|
835
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), minimalWorkflow("x"))
|
|
836
|
+
|
|
837
|
+
const exit = await runExit(Effect.flatMap(ConfigService, (c) => c.load))
|
|
838
|
+
|
|
839
|
+
expect(Exit.isSuccess(exit)).toBe(true)
|
|
840
|
+
expect(existsSync(join(projectDir, ".gtd"))).toBe(false)
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
it("keeps `modes.*.format`/`validate` and `ui.format`'s `$GTD_FILE` untouched", async () => {
|
|
844
|
+
withConfig(
|
|
845
|
+
`{ modes: { adr: { format: "prettier --write $GTD_FILE", validate: "adr-lint $GTD_FILE" } }, ui: { format: "oxfmt --write $GTD_FILE" } }`,
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
const cfg = await getConfig(undefined, { GTD_FILE: "should-never-appear" })
|
|
849
|
+
|
|
850
|
+
expect(cfg.workflow.modes["adr"]).toEqual({
|
|
851
|
+
format: "prettier --write $GTD_FILE",
|
|
852
|
+
validate: "adr-lint $GTD_FILE",
|
|
853
|
+
})
|
|
854
|
+
expect(cfg.ui).toEqual({ format: "oxfmt --write $GTD_FILE" })
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
it("never interpolates the workflow's own `defaults` export", async () => {
|
|
858
|
+
writeFileSync(
|
|
859
|
+
join(projectDir, "gtd.config.ts"),
|
|
860
|
+
[minimalWorkflow("first"), `export const defaults = { greeting: "$LITERAL" }`, ``].join("\n"),
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
const cfg = await getConfig(undefined, { LITERAL: "should-never-appear" })
|
|
864
|
+
|
|
865
|
+
expect(cfg.defaultWorkflow.vars).toEqual({ greeting: "$LITERAL" })
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
it("`load` — the effectful half of the src/workflow/ boundary — is directly usable without going through ConfigService", async () => {
|
|
869
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), minimalWorkflow("direct-load"))
|
|
870
|
+
|
|
871
|
+
const result = await run(load, projectDir)
|
|
872
|
+
|
|
873
|
+
expect(result.workflow.initial).toBe("direct-load")
|
|
874
|
+
})
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
describe("ConfigService — workflow factory options", () => {
|
|
878
|
+
const configWith = (body: string) =>
|
|
879
|
+
writeFileSync(
|
|
880
|
+
join(projectDir, "gtd.config.ts"),
|
|
881
|
+
[`import { feature, fix, review } from "@pmelab/gtd/workflows"`, body, ``].join("\n"),
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
it("an option given in gtd.config.ts becomes the workflow's setting of its kind", async () => {
|
|
885
|
+
configWith(
|
|
886
|
+
`export default feature({ qualityReviews: "correctness", testCommand: "npm run ci" })`,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
const cfg = await getConfig()
|
|
890
|
+
|
|
891
|
+
expect(cfg.defaultWorkflow.name).toBe("default")
|
|
892
|
+
expect(cfg.defaultWorkflow.vars["qualityReviews"]).toBe("correctness")
|
|
893
|
+
expect(cfg.defaultWorkflow.env["testCommand"]).toBe("npm run ci")
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
it("an exported factory and its call are each a workflow, configured on their own", async () => {
|
|
897
|
+
configWith(
|
|
898
|
+
[
|
|
899
|
+
`export { feature as default, feature }`,
|
|
900
|
+
`export const strict = review({ reviewNoteActionable: "0.9" })`,
|
|
901
|
+
].join("\n"),
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
const cfg = await getConfig()
|
|
905
|
+
|
|
906
|
+
expect(cfg.defaultWorkflow.name).toBe("feature")
|
|
907
|
+
expect(cfg.workflowNames).toEqual(["feature", "strict"])
|
|
908
|
+
expect(cfg.workflowNamed("strict")?.vars["reviewNoteActionable"]).toBe("0.9")
|
|
909
|
+
expect(cfg.workflowNamed("feature")?.vars["reviewNoteActionable"]).toBe("0.7")
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it("an invalid option in gtd.config.ts names the workflow, the key path and the expectation", async () => {
|
|
913
|
+
configWith(`export default feature()\nexport const repair = fix({ judgeBudgetBytes: 65536 })`)
|
|
914
|
+
|
|
915
|
+
const error = await getConfig().catch((e: Error) => e.message)
|
|
916
|
+
|
|
917
|
+
expect(error).toContain(
|
|
918
|
+
`gtd.config.ts: judgeBudgetBytes: workflow "repair" option "judgeBudgetBytes"`,
|
|
919
|
+
)
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
it("a misspelled option fails the load, listing the options there are", async () => {
|
|
923
|
+
configWith(`export default feature({ testComand: "npm test" })`)
|
|
924
|
+
|
|
925
|
+
const error = await getConfig().catch((e: Error) => e.message)
|
|
926
|
+
|
|
927
|
+
expect(error).toMatch(/gtd\.config\.ts: testComand: workflow "default" option "testComand"/)
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
it("a door naming a rejected factory reports the option error alone", async () => {
|
|
931
|
+
configWith(
|
|
932
|
+
[
|
|
933
|
+
`export default feature({ testComand: "x" } as never)`,
|
|
934
|
+
`export const doors = { go: { workflow: "default" } }`,
|
|
935
|
+
].join("\n"),
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
const error = await getConfig().catch((e: Error) => e.message)
|
|
939
|
+
|
|
940
|
+
expect(error).toContain(`option "testComand"`)
|
|
941
|
+
expect(error).not.toContain("door")
|
|
942
|
+
})
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
describe("ConfigService — a factory's built settings", () => {
|
|
946
|
+
it("a door a factory builds is held to the door-name rule, naming workflow and door", async () => {
|
|
947
|
+
writeFileSync(
|
|
948
|
+
join(projectDir, "gtd.config.ts"),
|
|
949
|
+
[
|
|
950
|
+
`import { defineWorkflow, human } from "@pmelab/gtd/flows"`,
|
|
951
|
+
`const any = { "~standard": { version: 1, vendor: "t", validate: (value) => ({ value }) } }`,
|
|
952
|
+
`export const audit = defineWorkflow(any, () => ({`,
|
|
953
|
+
` flow: async () => { await human("idle") },`,
|
|
954
|
+
` doors: { Bad_Name: {} },`,
|
|
955
|
+
`}))`,
|
|
956
|
+
`export default audit`,
|
|
957
|
+
``,
|
|
958
|
+
].join("\n"),
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
await expect(getConfig()).rejects.toThrow(/workflow "audit" builds door "Bad_Name", which/)
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
it("are held to the setting-name rule, like a file's defaults", async () => {
|
|
965
|
+
writeFileSync(
|
|
966
|
+
join(projectDir, "gtd.config.ts"),
|
|
967
|
+
[
|
|
968
|
+
`import { defineWorkflow, human } from "@pmelab/gtd/flows"`,
|
|
969
|
+
`const any = { "~standard": { version: 1, vendor: "t", validate: (value) => ({ value }) } }`,
|
|
970
|
+
`export default defineWorkflow(any, () => ({`,
|
|
971
|
+
` flow: async () => { await human("idle") },`,
|
|
972
|
+
` defaults: { "x=y": "z" },`,
|
|
973
|
+
`}))`,
|
|
974
|
+
``,
|
|
975
|
+
].join("\n"),
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
await expect(getConfig()).rejects.toThrow(
|
|
979
|
+
/workflow "default" builds defaults "x=y", not a valid setting name/,
|
|
980
|
+
)
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
it("a build declaring one name as both a process and an environment setting is refused", async () => {
|
|
984
|
+
writeFileSync(
|
|
985
|
+
join(projectDir, "gtd.config.ts"),
|
|
986
|
+
[
|
|
987
|
+
`import { defineWorkflow, human } from "@pmelab/gtd/flows"`,
|
|
988
|
+
`const any = { "~standard": { version: 1, vendor: "t", validate: (value) => ({ value }) } }`,
|
|
989
|
+
`export default defineWorkflow(any, () => ({`,
|
|
990
|
+
` flow: async () => { await human("idle") },`,
|
|
991
|
+
` defaults: { x: "1" },`,
|
|
992
|
+
` envDefaults: { x: "2" },`,
|
|
993
|
+
`}))`,
|
|
994
|
+
``,
|
|
995
|
+
].join("\n"),
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
await expect(getConfig()).rejects.toThrow(`"x" declared in both "defaults" and "envDefaults"`)
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
it("a build declaring a setting that is no string is refused", async () => {
|
|
1002
|
+
writeFileSync(
|
|
1003
|
+
join(projectDir, "gtd.config.ts"),
|
|
1004
|
+
[
|
|
1005
|
+
`import { defineWorkflow, human } from "@pmelab/gtd/flows"`,
|
|
1006
|
+
`const any = { "~standard": { version: 1, vendor: "t", validate: (value) => ({ value }) } }`,
|
|
1007
|
+
`export default defineWorkflow(any, () => ({`,
|
|
1008
|
+
` flow: async () => { await human("idle") },`,
|
|
1009
|
+
` defaults: { x: 1 },`,
|
|
1010
|
+
`}))`,
|
|
1011
|
+
``,
|
|
1012
|
+
].join("\n"),
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
await expect(getConfig()).rejects.toThrow(/builds defaults "x", whose value is not a string/)
|
|
1016
|
+
})
|
|
1017
|
+
})
|