@bastani/atomic 0.5.34-0 → 0.6.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +329 -50
- package/dist/commands/cli/session.d.ts +67 -0
- package/dist/commands/cli/session.d.ts.map +1 -0
- package/dist/commands/cli/workflow-status.d.ts +63 -0
- package/dist/commands/cli/workflow-status.d.ts.map +1 -0
- package/dist/sdk/commander.d.ts +74 -0
- package/dist/sdk/commander.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/define-workflow.d.ts +18 -9
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +4 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/management-commands.d.ts +42 -0
- package/dist/sdk/management-commands.d.ts.map +1 -0
- package/dist/sdk/registry.d.ts +27 -0
- package/dist/sdk/registry.d.ts.map +1 -0
- package/dist/sdk/runtime/attached-footer.d.ts +1 -1
- package/dist/sdk/runtime/executor-env.d.ts +20 -0
- package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +61 -10
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +147 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/worker-shared.d.ts +42 -0
- package/dist/sdk/worker-shared.d.ts.map +1 -0
- package/dist/sdk/workflow-cli.d.ts +103 -0
- package/dist/sdk/workflow-cli.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
- package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +5 -5
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/cli.ts +85 -144
- package/src/commands/cli/chat/index.ts +10 -0
- package/src/commands/cli/workflow-command.test.ts +279 -938
- package/src/commands/cli/workflow-inputs.test.ts +41 -11
- package/src/commands/cli/workflow-inputs.ts +47 -12
- package/src/commands/cli/workflow-list.test.ts +234 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow.ts +11 -798
- package/src/scripts/constants.ts +2 -1
- package/src/sdk/commander.ts +161 -0
- package/src/sdk/components/workflow-picker-panel.tsx +78 -258
- package/src/sdk/define-workflow.test.ts +104 -11
- package/src/sdk/define-workflow.ts +47 -11
- package/src/sdk/errors.test.ts +16 -0
- package/src/sdk/index.ts +8 -8
- package/src/sdk/management-commands.ts +151 -0
- package/src/sdk/registry.ts +132 -0
- package/src/sdk/runtime/attached-footer.ts +1 -1
- package/src/sdk/runtime/executor-env.ts +45 -0
- package/src/sdk/runtime/executor.test.ts +37 -0
- package/src/sdk/runtime/executor.ts +147 -68
- package/src/sdk/types.ts +169 -4
- package/src/sdk/worker-shared.test.ts +163 -0
- package/src/sdk/worker-shared.ts +155 -0
- package/src/sdk/workflow-cli.ts +409 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin-registry.ts +23 -0
- package/src/sdk/workflows/index.ts +10 -20
- package/src/services/system/auth.test.ts +63 -1
- package/.agents/skills/workflow-creator/SKILL.md +0 -334
- package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
- package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
- package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
- package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
- package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
- package/.agents/skills/workflow-creator/references/session-config.md +0 -384
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
- package/.agents/skills/workflow-creator/references/user-input.md +0 -234
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
- package/dist/sdk/runtime/discovery.d.ts +0 -132
- package/dist/sdk/runtime/discovery.d.ts.map +0 -1
- package/dist/sdk/runtime/executor-entry.d.ts +0 -11
- package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
- package/dist/sdk/runtime/loader.d.ts +0 -70
- package/dist/sdk/runtime/loader.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/src/commands/cli/workflow.test.ts +0 -317
- package/src/sdk/runtime/discovery.ts +0 -368
- package/src/sdk/runtime/executor-entry.ts +0 -18
- package/src/sdk/runtime/loader.ts +0 -267
|
@@ -1,154 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* the workflows SDK (executor + tmux probe + discovery), the system detector
|
|
5
|
-
* (command-presence checks), and the spawn helpers (best-effort installers).
|
|
6
|
-
* Every one of these is a side-effectful dependency — tmux spawn, disk I/O,
|
|
7
|
-
* agent CLI spawn — and replacing them with controlled fakes lets us hit the
|
|
8
|
-
* CLI's error/success branches without actually touching the real system.
|
|
2
|
+
* Tests for `workflowCommand` — the Commander Command returned by
|
|
3
|
+
* `createWorkflowCli(createBuiltinRegistry()).command("workflow")`.
|
|
9
4
|
*
|
|
10
|
-
*
|
|
5
|
+
* Mocking strategy: mock.module("../../sdk/runtime/executor.ts") replaces
|
|
6
|
+
* executeWorkflow with a spy BEFORE the dynamic import of workflow.ts.
|
|
11
7
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* Module load order:
|
|
9
|
+
* 1. Static imports execute first (hoisted by ES module semantics) —
|
|
10
|
+
* this loads registry.ts → providers/claude.ts → executor.ts (REAL),
|
|
11
|
+
* so `escBash` and all other executor exports are cached before the mock.
|
|
12
|
+
* 2. `mock.module` replaces executor.ts for SUBSEQUENT imports — only
|
|
13
|
+
* `worker.ts` picks up the mocked executeWorkflow/runOrchestrator.
|
|
14
|
+
* 3. Dynamic import of workflow.ts uses the mocked executor via worker.ts.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* without touching the repo's own `.atomic/workflows` tree.
|
|
16
|
+
* Commander error handling: `exitOverride()` is called on the command before
|
|
17
|
+
* tests that expect rejection, converting process.exit(1) into a thrown Error.
|
|
20
18
|
*/
|
|
21
19
|
|
|
22
20
|
import {
|
|
23
21
|
describe,
|
|
24
22
|
test,
|
|
25
23
|
expect,
|
|
26
|
-
beforeAll,
|
|
27
|
-
afterAll,
|
|
28
24
|
beforeEach,
|
|
29
25
|
afterEach,
|
|
30
26
|
mock,
|
|
31
27
|
} from "bun:test";
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
import
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Capture original function references BEFORE `mock.module` replaces the
|
|
47
|
-
// module exports. `import * as realWorkflows` gives a LIVE namespace — after
|
|
48
|
-
// mock.module rebinds the exports, `realWorkflows.discoverWorkflows` would
|
|
49
|
-
// resolve to our own mock and a pass-through would recurse infinitely. These
|
|
50
|
-
// constants lock in the real implementations so pass-through defaults work.
|
|
51
|
-
const realDiscoverWorkflows = realWorkflows.discoverWorkflows;
|
|
52
|
-
const realLoadWorkflowsMetadata = realWorkflows.loadWorkflowsMetadata;
|
|
53
|
-
const realIsCommandInstalled = realDetect.isCommandInstalled;
|
|
54
|
-
// Snapshot the real `auth.ts` exports before `mock.module` rebinds them.
|
|
55
|
-
// Bun 1.3.13 canonicalizes `mock.module` specifiers to an absolute path, so
|
|
56
|
-
// our registration for `"../../services/system/auth.ts"` now shares a key with
|
|
57
|
-
// `auth.test.ts`'s `"./auth.ts"` — the mock leaks across files unless we
|
|
58
|
-
// explicitly restore on teardown. See `afterAll` at the bottom of this file.
|
|
59
|
-
const realAuthSnapshot = { ...realAuth };
|
|
60
|
-
|
|
61
|
-
// ─── Dependency mocks ───────────────────────────────────────────────────────
|
|
62
|
-
// Every mock is a wrapper around the real implementation by default so
|
|
63
|
-
// unrelated tests that don't care about a given mock still see the real
|
|
64
|
-
// behaviour. Tests override specific mocks via `mockImplementationOnce` (or a
|
|
65
|
-
// longer-lived `mockImplementation` inside a describe block) to exercise
|
|
66
|
-
// failure branches. `beforeEach` resets everything to the default pass-through.
|
|
67
|
-
|
|
68
|
-
const executeWorkflowMock =
|
|
69
|
-
mock<(opts: WorkflowRunOptions) => Promise<void>>(async () => {});
|
|
70
|
-
|
|
71
|
-
// Default: real discovery so the filesystem-level branches still work.
|
|
72
|
-
const discoverWorkflowsMock = mock<typeof realWorkflows.discoverWorkflows>(
|
|
73
|
-
(...args) => realDiscoverWorkflows(...args),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// Default: real metadata load — supports the picker branches that need
|
|
77
|
-
// compiled metadata from a real workflow on disk.
|
|
78
|
-
const loadWorkflowsMetadataMock = mock<
|
|
79
|
-
typeof realWorkflows.loadWorkflowsMetadata
|
|
80
|
-
>((...args) => realLoadWorkflowsMetadata(...args));
|
|
81
|
-
|
|
82
|
-
// Default: pretend tmux is installed. The test env has it, but we want the
|
|
83
|
-
// coverage test to be deterministic regardless of host config — if the host
|
|
84
|
-
// removed tmux we'd still want these tests to cover the happy path.
|
|
85
|
-
const isTmuxInstalledMock =
|
|
86
|
-
mock<typeof realWorkflows.isTmuxInstalled>(() => true);
|
|
87
|
-
|
|
88
|
-
// Default: delegate to the real check, but pretend agent CLIs are installed.
|
|
89
|
-
// CI runners won't have copilot/opencode/claude on PATH; without this
|
|
90
|
-
// override every test that passes through runPrereqChecks would bail early.
|
|
91
|
-
// Non-agent commands still hit the real check so mock.module doesn't break
|
|
92
|
-
// detect.test.ts (Bun shares one process across test files).
|
|
93
|
-
const AGENT_CMDS = new Set(Object.values(AGENT_CONFIG).map((c) => c.cmd));
|
|
94
|
-
const defaultIsCommandInstalled = (cmd: string) =>
|
|
95
|
-
AGENT_CMDS.has(cmd) || realIsCommandInstalled(cmd);
|
|
96
|
-
const isCommandInstalledMock = mock<typeof realDetect.isCommandInstalled>(
|
|
97
|
-
defaultIsCommandInstalled,
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Default: no-op so the best-effort installer branch in runPrereqChecks
|
|
101
|
-
// doesn't try to actually install tmux/bun on the test machine.
|
|
102
|
-
const ensureTmuxInstalledMock = mock<typeof realSpawn.ensureTmuxInstalled>(
|
|
103
|
-
async () => {},
|
|
104
|
-
);
|
|
105
|
-
const ensureBunInstalledMock = mock<typeof realSpawn.ensureBunInstalled>(
|
|
106
|
-
async () => {},
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Default: pretend auth succeeded. Real probes spawn the agent CLI via
|
|
110
|
-
// its SDK to talk to an auth RPC — that's network/process-heavy and
|
|
111
|
-
// non-deterministic on CI runners, so the auth branch is faked here and
|
|
112
|
-
// exercised directly by `auth.test.ts`.
|
|
113
|
-
const checkAgentAuthMock = mock<typeof realAuth.checkAgentAuth>(async () => ({
|
|
114
|
-
loggedIn: true,
|
|
115
|
-
}));
|
|
28
|
+
import type { WorkflowRunOptions } from "../../sdk/runtime/executor.ts";
|
|
29
|
+
// Static import — loads providers/claude.ts → real executor.ts into module cache
|
|
30
|
+
// BEFORE mock.module replaces it for subsequent imports.
|
|
31
|
+
import "../../sdk/registry.ts";
|
|
32
|
+
|
|
33
|
+
// ─── Module-level mock ────────────────────────────────────────────────────────
|
|
34
|
+
// Must be declared AFTER the static imports above (which load the real executor)
|
|
35
|
+
// but BEFORE the dynamic import of workflow.ts below (which uses worker.ts → mock).
|
|
36
|
+
|
|
37
|
+
const executeWorkflowCalls: WorkflowRunOptions[] = [];
|
|
38
|
+
const executeWorkflowMock = mock(async (opts: WorkflowRunOptions): Promise<void> => {
|
|
39
|
+
executeWorkflowCalls.push(opts);
|
|
40
|
+
});
|
|
116
41
|
|
|
117
|
-
|
|
118
|
-
|
|
42
|
+
// Spread real module to preserve all exports (escBash, discoverCopilotBinary, etc.)
|
|
43
|
+
// so this mock doesn't break other test files that import those exports.
|
|
44
|
+
const realExecutor = await import("../../sdk/runtime/executor.ts");
|
|
45
|
+
await mock.module("../../sdk/runtime/executor.ts", () => ({
|
|
46
|
+
...realExecutor,
|
|
119
47
|
executeWorkflow: executeWorkflowMock,
|
|
120
|
-
|
|
121
|
-
loadWorkflowsMetadata: loadWorkflowsMetadataMock,
|
|
122
|
-
isTmuxInstalled: isTmuxInstalledMock,
|
|
123
|
-
}));
|
|
124
|
-
mock.module("../../services/system/detect.ts", () => ({
|
|
125
|
-
...realDetect,
|
|
126
|
-
isCommandInstalled: isCommandInstalledMock,
|
|
127
|
-
}));
|
|
128
|
-
mock.module("../../services/system/auth.ts", () => ({
|
|
129
|
-
...realAuth,
|
|
130
|
-
checkAgentAuth: checkAgentAuthMock,
|
|
131
|
-
}));
|
|
132
|
-
mock.module("../../lib/spawn.ts", () => ({
|
|
133
|
-
...realSpawn,
|
|
134
|
-
ensureTmuxInstalled: ensureTmuxInstalledMock,
|
|
135
|
-
ensureBunInstalled: ensureBunInstalledMock,
|
|
48
|
+
runOrchestrator: async () => {},
|
|
136
49
|
}));
|
|
137
50
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
51
|
+
// Build a fresh workflowCommand using the real builtin registry directly.
|
|
52
|
+
// This avoids stale-cache issues when workflow.ts was previously loaded by
|
|
53
|
+
// cli.ts with a mocked (fake) builtin-registry in earlier test files.
|
|
54
|
+
const { createWorkflowCli } = await import("../../sdk/workflow-cli.ts");
|
|
55
|
+
const { toCommand } = await import("../../sdk/commander.ts");
|
|
56
|
+
const { createBuiltinRegistry } = await import("../../sdk/workflows/builtin-registry.ts");
|
|
57
|
+
const workflowCommand = toCommand(
|
|
58
|
+
createWorkflowCli(createBuiltinRegistry()),
|
|
59
|
+
"workflow",
|
|
60
|
+
);
|
|
147
61
|
|
|
148
|
-
// ─── Output capture
|
|
149
|
-
// The CLI writes error banners to stderr via `console.error`, success content
|
|
150
|
-
// to stdout via `process.stdout.write`. Wrap both so tests can snapshot the
|
|
151
|
-
// emitted text without leaking it into the test runner's own output.
|
|
62
|
+
// ─── Output capture ──────────────────────────────────────────────────────────
|
|
152
63
|
|
|
153
64
|
interface CapturedOutput {
|
|
154
65
|
stdout: string;
|
|
@@ -157,887 +68,317 @@ interface CapturedOutput {
|
|
|
157
68
|
}
|
|
158
69
|
|
|
159
70
|
function captureOutput(): CapturedOutput {
|
|
160
|
-
const captured: CapturedOutput = {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
166
|
-
const originalConsoleError = console.error;
|
|
167
|
-
const originalConsoleLog = console.log;
|
|
168
|
-
const originalConsoleWarn = console.warn;
|
|
71
|
+
const captured: CapturedOutput = { stdout: "", stderr: "", restore: () => {} };
|
|
72
|
+
const origStdout = process.stdout.write.bind(process.stdout);
|
|
73
|
+
const origConsoleLog = console.log;
|
|
74
|
+
const origConsoleError = console.error;
|
|
75
|
+
const origConsoleWarn = console.warn;
|
|
169
76
|
|
|
170
|
-
// Typed as never so the loose commander signature doesn't widen.
|
|
171
77
|
process.stdout.write = ((chunk: string | Uint8Array): boolean => {
|
|
172
|
-
captured.stdout +=
|
|
173
|
-
typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
78
|
+
captured.stdout += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
174
79
|
return true;
|
|
175
80
|
}) as typeof process.stdout.write;
|
|
176
|
-
console.error = (...args: unknown[]) => {
|
|
177
|
-
captured.stderr += args.map((a) => String(a)).join(" ") + "\n";
|
|
178
|
-
};
|
|
179
81
|
console.log = (...args: unknown[]) => {
|
|
180
|
-
captured.stdout += args.map(
|
|
82
|
+
captured.stdout += args.map(String).join(" ") + "\n";
|
|
83
|
+
};
|
|
84
|
+
console.error = (...args: unknown[]) => {
|
|
85
|
+
captured.stderr += args.map(String).join(" ") + "\n";
|
|
181
86
|
};
|
|
182
87
|
console.warn = (...args: unknown[]) => {
|
|
183
|
-
captured.stderr += args.map(
|
|
88
|
+
captured.stderr += args.map(String).join(" ") + "\n";
|
|
184
89
|
};
|
|
185
90
|
|
|
186
91
|
captured.restore = () => {
|
|
187
|
-
process.stdout.write =
|
|
188
|
-
console.
|
|
189
|
-
console.
|
|
190
|
-
console.warn =
|
|
92
|
+
process.stdout.write = origStdout;
|
|
93
|
+
console.log = origConsoleLog;
|
|
94
|
+
console.error = origConsoleError;
|
|
95
|
+
console.warn = origConsoleWarn;
|
|
191
96
|
};
|
|
192
97
|
return captured;
|
|
193
98
|
}
|
|
194
99
|
|
|
195
|
-
// ─── Colour
|
|
196
|
-
// `NO_COLOR` flips both COLORS (module load time) and createPainter (call
|
|
197
|
-
// time) into plain-text mode so assertions can match against readable
|
|
198
|
-
// substrings rather than SGR escape noise. COLORS is baked at module load
|
|
199
|
-
// so the env var must already be set by the time workflow.ts gets imported.
|
|
100
|
+
// ─── Colour suppression ──────────────────────────────────────────────────────
|
|
200
101
|
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
originalNoColor = process.env.NO_COLOR;
|
|
102
|
+
let savedNoColor: string | undefined;
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
savedNoColor = process.env.NO_COLOR;
|
|
205
105
|
process.env.NO_COLOR = "1";
|
|
206
|
-
|
|
207
|
-
// leaking into unrelated suites in the same bun-test process.
|
|
208
|
-
originalAtomicAgent = process.env.ATOMIC_AGENT;
|
|
209
|
-
});
|
|
210
|
-
afterAll(() => {
|
|
211
|
-
if (originalNoColor === undefined) delete process.env.NO_COLOR;
|
|
212
|
-
else process.env.NO_COLOR = originalNoColor;
|
|
213
|
-
if (originalAtomicAgent === undefined) delete process.env.ATOMIC_AGENT;
|
|
214
|
-
else process.env.ATOMIC_AGENT = originalAtomicAgent;
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// ─── Temp workspace plumbing ────────────────────────────────────────────────
|
|
218
|
-
// Each test gets a fresh cwd so one test's workflows can't leak into another.
|
|
219
|
-
// The actual workflow files live under `.atomic/workflows/<name>/<agent>/index.ts`
|
|
220
|
-
// — matching the layout that `discoverWorkflows` scans.
|
|
221
|
-
|
|
222
|
-
let tempDir: string;
|
|
223
|
-
|
|
224
|
-
beforeEach(async () => {
|
|
225
|
-
tempDir = await mkdtemp(join(tmpdir(), "atomic-workflow-cmd-test-"));
|
|
226
|
-
// Clear ATOMIC_AGENT by default — `workflowCommand` falls back to this
|
|
227
|
-
// env var when `-a` is omitted, and we don't want the ambient env (e.g.
|
|
228
|
-
// a developer running tests from inside an atomic chat pane) to silently
|
|
229
|
-
// change the agent any given test sees. Tests that need it explicitly
|
|
230
|
-
// set it themselves.
|
|
231
|
-
delete process.env.ATOMIC_AGENT;
|
|
232
|
-
// Reset every mock to its default pass-through / no-op so tests are
|
|
233
|
-
// independent — no leftover state from prior overrides. `mockClear` wipes
|
|
234
|
-
// call history; `mockImplementation` replaces the queued implementation
|
|
235
|
-
// (including anything set via `mockImplementationOnce`) with the default.
|
|
106
|
+
executeWorkflowCalls.length = 0;
|
|
236
107
|
executeWorkflowMock.mockClear();
|
|
237
|
-
executeWorkflowMock.mockImplementation(async () => {
|
|
238
|
-
|
|
239
|
-
discoverWorkflowsMock.mockImplementation((...args) =>
|
|
240
|
-
realDiscoverWorkflows(...args),
|
|
241
|
-
);
|
|
242
|
-
loadWorkflowsMetadataMock.mockClear();
|
|
243
|
-
loadWorkflowsMetadataMock.mockImplementation((...args) =>
|
|
244
|
-
realLoadWorkflowsMetadata(...args),
|
|
245
|
-
);
|
|
246
|
-
isTmuxInstalledMock.mockClear();
|
|
247
|
-
isTmuxInstalledMock.mockImplementation(() => true);
|
|
248
|
-
isCommandInstalledMock.mockClear();
|
|
249
|
-
isCommandInstalledMock.mockImplementation(defaultIsCommandInstalled);
|
|
250
|
-
ensureTmuxInstalledMock.mockClear();
|
|
251
|
-
ensureTmuxInstalledMock.mockImplementation(async () => {});
|
|
252
|
-
ensureBunInstalledMock.mockClear();
|
|
253
|
-
ensureBunInstalledMock.mockImplementation(async () => {});
|
|
254
|
-
checkAgentAuthMock.mockClear();
|
|
255
|
-
checkAgentAuthMock.mockImplementation(async () => ({ loggedIn: true }));
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
afterEach(async () => {
|
|
259
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Write a real workflow file that compiles through `defineWorkflow()`.
|
|
264
|
-
* Tests import a real SDK so the module under test sees a live
|
|
265
|
-
* `WorkflowDefinition`, not a mock shape — this keeps the coverage
|
|
266
|
-
* line-level on `runNamedMode`'s resolution of the compiled definition.
|
|
267
|
-
*/
|
|
268
|
-
async function writeCompiledWorkflow(
|
|
269
|
-
opts: {
|
|
270
|
-
name: string;
|
|
271
|
-
agent: "claude" | "copilot" | "opencode";
|
|
272
|
-
source?: string;
|
|
273
|
-
},
|
|
274
|
-
): Promise<string> {
|
|
275
|
-
const dir = join(tempDir, ".atomic", "workflows", opts.name, opts.agent);
|
|
276
|
-
await mkdir(dir, { recursive: true });
|
|
277
|
-
const filePath = join(dir, "index.ts");
|
|
278
|
-
const defaultBody =
|
|
279
|
-
opts.source ??
|
|
280
|
-
`
|
|
281
|
-
import { defineWorkflow } from "${join(process.cwd(), "src/sdk/workflows/index.ts")}";
|
|
282
|
-
|
|
283
|
-
export default defineWorkflow({ name: "${opts.name}" })
|
|
284
|
-
.run(async () => {})
|
|
285
|
-
.compile();
|
|
286
|
-
`;
|
|
287
|
-
await writeFile(filePath, defaultBody);
|
|
288
|
-
return filePath;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── List mode ──────────────────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
describe("workflowCommand --list", () => {
|
|
294
|
-
test("prints the rendered list and returns 0", async () => {
|
|
295
|
-
await writeCompiledWorkflow({ name: "alpha", agent: "copilot" });
|
|
296
|
-
|
|
297
|
-
const cap = captureOutput();
|
|
298
|
-
const code = await workflowCommand({
|
|
299
|
-
list: true,
|
|
300
|
-
agent: "copilot",
|
|
301
|
-
cwd: tempDir,
|
|
302
|
-
});
|
|
303
|
-
cap.restore();
|
|
304
|
-
|
|
305
|
-
expect(code).toBe(0);
|
|
306
|
-
// Singular noun because only our one workflow is filtered in, and builtins
|
|
307
|
-
// discovered via `{ merge: false }` may still show up — so assert on the
|
|
308
|
-
// name we wrote instead of a count.
|
|
309
|
-
expect(cap.stdout).toContain("alpha");
|
|
310
|
-
expect(cap.stdout).toContain("run: atomic workflow -n <name> -a <agent>");
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("filters by the provided agent", async () => {
|
|
314
|
-
await writeCompiledWorkflow({ name: "claude-only", agent: "claude" });
|
|
315
|
-
await writeCompiledWorkflow({ name: "copilot-only", agent: "copilot" });
|
|
316
|
-
|
|
317
|
-
const cap = captureOutput();
|
|
318
|
-
const code = await workflowCommand({
|
|
319
|
-
list: true,
|
|
320
|
-
agent: "claude",
|
|
321
|
-
cwd: tempDir,
|
|
322
|
-
});
|
|
323
|
-
cap.restore();
|
|
324
|
-
|
|
325
|
-
expect(code).toBe(0);
|
|
326
|
-
expect(cap.stdout).toContain("claude-only");
|
|
327
|
-
expect(cap.stdout).not.toContain("copilot-only");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("shows workflows missing .compile() with a broken marker", async () => {
|
|
331
|
-
// Broken workflows used to vanish from the list silently. Surfacing
|
|
332
|
-
// them with a visible "✗ broken" badge is the remediation for the
|
|
333
|
-
// "my workflow disappeared after upgrading" class of bug reports —
|
|
334
|
-
// the user can now see the file still exists and needs a fix.
|
|
335
|
-
await writeCompiledWorkflow({ name: "good", agent: "copilot" });
|
|
336
|
-
await writeCompiledWorkflow({
|
|
337
|
-
name: "not-compiled",
|
|
338
|
-
agent: "copilot",
|
|
339
|
-
source: `
|
|
340
|
-
import { defineWorkflow } from "${join(process.cwd(), "src/sdk/workflows/index.ts")}";
|
|
341
|
-
|
|
342
|
-
export default defineWorkflow({ name: "not-compiled" })
|
|
343
|
-
.run(async () => {});
|
|
344
|
-
// intentionally missing .compile()
|
|
345
|
-
`,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const cap = captureOutput();
|
|
349
|
-
const code = await workflowCommand({
|
|
350
|
-
list: true,
|
|
351
|
-
agent: "copilot",
|
|
352
|
-
cwd: tempDir,
|
|
353
|
-
});
|
|
354
|
-
cap.restore();
|
|
355
|
-
|
|
356
|
-
expect(code).toBe(0);
|
|
357
|
-
expect(cap.stdout).toContain("good");
|
|
358
|
-
expect(cap.stdout).toContain("not-compiled");
|
|
359
|
-
expect(cap.stdout).toContain("✗ broken");
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
test("shows workflows with type errors with a broken marker", async () => {
|
|
363
|
-
await writeCompiledWorkflow({ name: "valid", agent: "copilot" });
|
|
364
|
-
await writeCompiledWorkflow({
|
|
365
|
-
name: "broken-syntax",
|
|
366
|
-
agent: "copilot",
|
|
367
|
-
source: `this is not valid typescript }{}{`,
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
const cap = captureOutput();
|
|
371
|
-
const code = await workflowCommand({
|
|
372
|
-
list: true,
|
|
373
|
-
agent: "copilot",
|
|
374
|
-
cwd: tempDir,
|
|
375
|
-
});
|
|
376
|
-
cap.restore();
|
|
377
|
-
|
|
378
|
-
expect(code).toBe(0);
|
|
379
|
-
expect(cap.stdout).toContain("valid");
|
|
380
|
-
expect(cap.stdout).toContain("broken-syntax");
|
|
381
|
-
expect(cap.stdout).toContain("✗ broken");
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test("renders the empty state when no workflows exist and no agent filter is set", async () => {
|
|
385
|
-
// No agent filter + a fresh tempdir means `discoverWorkflows` only
|
|
386
|
-
// returns builtins for whichever agents exist on disk; to exercise
|
|
387
|
-
// the real empty-state branch we filter to an agent with no builtin
|
|
388
|
-
// coverage for the tempdir — `opencode` has builtins too, so instead
|
|
389
|
-
// point at an empty workflows directory.
|
|
390
|
-
const cap = captureOutput();
|
|
391
|
-
const code = await workflowCommand({
|
|
392
|
-
list: true,
|
|
393
|
-
agent: "copilot",
|
|
394
|
-
cwd: tempDir,
|
|
395
|
-
});
|
|
396
|
-
cap.restore();
|
|
397
|
-
|
|
398
|
-
expect(code).toBe(0);
|
|
399
|
-
// Either the builtin ralph shows up or we get the "no workflows" banner.
|
|
400
|
-
// We only need to verify the code path completes and writes *something*.
|
|
401
|
-
expect(cap.stdout.length).toBeGreaterThan(0);
|
|
108
|
+
executeWorkflowMock.mockImplementation(async (opts) => {
|
|
109
|
+
executeWorkflowCalls.push(opts);
|
|
402
110
|
});
|
|
403
111
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
describe("workflowCommand agent validation", () => {
|
|
408
|
-
test("missing agent returns 1 and logs a targeted error", async () => {
|
|
409
|
-
const cap = captureOutput();
|
|
410
|
-
const code = await workflowCommand({ cwd: tempDir });
|
|
411
|
-
cap.restore();
|
|
412
|
-
|
|
413
|
-
expect(code).toBe(1);
|
|
414
|
-
expect(cap.stderr).toContain("Missing agent");
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("unknown agent returns 1 and lists valid agents", async () => {
|
|
418
|
-
const cap = captureOutput();
|
|
419
|
-
const code = await workflowCommand({
|
|
420
|
-
agent: "bogus-agent",
|
|
421
|
-
cwd: tempDir,
|
|
422
|
-
});
|
|
423
|
-
cap.restore();
|
|
424
|
-
|
|
425
|
-
expect(code).toBe(1);
|
|
426
|
-
expect(cap.stderr).toContain("Unknown agent");
|
|
427
|
-
// Error helper lists valid agents — spot-check one.
|
|
428
|
-
expect(cap.stderr).toContain("claude");
|
|
429
|
-
});
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// ─── Picker mode error paths ───────────────────────────────────────────────
|
|
433
|
-
|
|
434
|
-
describe("workflowCommand picker mode", () => {
|
|
435
|
-
test("rejects passthrough args in picker mode", async () => {
|
|
436
|
-
// No `-n` means picker mode; any extra args are ambiguous (would the
|
|
437
|
-
// user want them fed into the picker's form, or straight through?), so
|
|
438
|
-
// the command bails early rather than guessing.
|
|
439
|
-
const cap = captureOutput();
|
|
440
|
-
const code = await workflowCommand({
|
|
441
|
-
agent: "copilot",
|
|
442
|
-
passthroughArgs: ["oops", "--mode=fast"],
|
|
443
|
-
cwd: tempDir,
|
|
444
|
-
});
|
|
445
|
-
cap.restore();
|
|
446
|
-
|
|
447
|
-
expect(code).toBe(1);
|
|
448
|
-
expect(cap.stderr).toContain("unexpected arguments");
|
|
449
|
-
// The hint points the user at the right place.
|
|
450
|
-
expect(cap.stderr).toContain("-n <name>");
|
|
451
|
-
});
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
if (savedNoColor === undefined) delete process.env.NO_COLOR;
|
|
114
|
+
else process.env.NO_COLOR = savedNoColor;
|
|
452
115
|
});
|
|
453
116
|
|
|
454
|
-
// ───
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
test("unknown workflow name returns 1 and lists available options", async () => {
|
|
458
|
-
// Seed one workflow so the "Available" section renders.
|
|
459
|
-
await writeCompiledWorkflow({ name: "real-one", agent: "copilot" });
|
|
460
|
-
|
|
461
|
-
const cap = captureOutput();
|
|
462
|
-
const code = await workflowCommand({
|
|
463
|
-
name: "does-not-exist",
|
|
464
|
-
agent: "copilot",
|
|
465
|
-
cwd: tempDir,
|
|
466
|
-
});
|
|
467
|
-
cap.restore();
|
|
468
|
-
|
|
469
|
-
expect(code).toBe(1);
|
|
470
|
-
expect(cap.stderr).toContain("does-not-exist");
|
|
471
|
-
expect(cap.stderr).toContain("not found");
|
|
472
|
-
// Lists the real workflow we wrote so users can copy-paste a valid name.
|
|
473
|
-
expect(cap.stderr).toContain("real-one");
|
|
474
|
-
// executeWorkflow should never be called on the error path.
|
|
475
|
-
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test("available workflows hint excludes uncompiled workflows", async () => {
|
|
479
|
-
await writeCompiledWorkflow({ name: "real-one", agent: "copilot" });
|
|
480
|
-
await writeCompiledWorkflow({
|
|
481
|
-
name: "uncompiled",
|
|
482
|
-
agent: "copilot",
|
|
483
|
-
source: `
|
|
484
|
-
import { defineWorkflow } from "${join(process.cwd(), "src/sdk/workflows/index.ts")}";
|
|
485
|
-
|
|
486
|
-
export default defineWorkflow({ name: "uncompiled" })
|
|
487
|
-
.run(async () => {});
|
|
488
|
-
`,
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
const cap = captureOutput();
|
|
492
|
-
const code = await workflowCommand({
|
|
493
|
-
name: "does-not-exist",
|
|
494
|
-
agent: "copilot",
|
|
495
|
-
cwd: tempDir,
|
|
496
|
-
});
|
|
497
|
-
cap.restore();
|
|
498
|
-
|
|
499
|
-
expect(code).toBe(1);
|
|
500
|
-
expect(cap.stderr).toContain("real-one");
|
|
501
|
-
expect(cap.stderr).not.toContain("uncompiled");
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
test("parse errors in passthrough args abort before loading", async () => {
|
|
505
|
-
await writeCompiledWorkflow({ name: "parse-err", agent: "copilot" });
|
|
506
|
-
|
|
507
|
-
const cap = captureOutput();
|
|
508
|
-
const code = await workflowCommand({
|
|
509
|
-
name: "parse-err",
|
|
510
|
-
agent: "copilot",
|
|
511
|
-
// Trailing --flag with no value is the canonical parse error.
|
|
512
|
-
passthroughArgs: ["--orphan"],
|
|
513
|
-
cwd: tempDir,
|
|
514
|
-
});
|
|
515
|
-
cap.restore();
|
|
516
|
-
|
|
517
|
-
expect(code).toBe(1);
|
|
518
|
-
expect(cap.stderr).toContain("--orphan");
|
|
519
|
-
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
test("load errors from WorkflowLoader surface cleanly", async () => {
|
|
523
|
-
// Write a workflow file that lacks `.compile()` — the loader treats
|
|
524
|
-
// this as a hard error and the CLI must return 1 rather than crash.
|
|
525
|
-
await writeCompiledWorkflow({
|
|
526
|
-
name: "broken",
|
|
527
|
-
agent: "copilot",
|
|
528
|
-
source: `
|
|
529
|
-
import { defineWorkflow } from "${join(process.cwd(), "src/sdk/workflows/index.ts")}";
|
|
530
|
-
|
|
531
|
-
export default defineWorkflow({ name: "broken" })
|
|
532
|
-
.run(async () => {});
|
|
533
|
-
// intentionally missing .compile()
|
|
534
|
-
`,
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
const cap = captureOutput();
|
|
538
|
-
const code = await workflowCommand({
|
|
539
|
-
name: "broken",
|
|
540
|
-
agent: "copilot",
|
|
541
|
-
cwd: tempDir,
|
|
542
|
-
});
|
|
543
|
-
cap.restore();
|
|
544
|
-
|
|
545
|
-
expect(code).toBe(1);
|
|
546
|
-
expect(cap.stderr).toContain("not compiled");
|
|
547
|
-
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
test("free-form workflow rejects stray --flags", async () => {
|
|
551
|
-
// A workflow with no declared `inputs` takes a positional prompt; any
|
|
552
|
-
// `--<name>` flag is definitionally wrong because there's nothing for
|
|
553
|
-
// it to bind to.
|
|
554
|
-
await writeCompiledWorkflow({ name: "free-form", agent: "copilot" });
|
|
555
|
-
|
|
556
|
-
const cap = captureOutput();
|
|
557
|
-
const code = await workflowCommand({
|
|
558
|
-
name: "free-form",
|
|
559
|
-
agent: "copilot",
|
|
560
|
-
passthroughArgs: ["--mode=fast"],
|
|
561
|
-
cwd: tempDir,
|
|
562
|
-
});
|
|
563
|
-
cap.restore();
|
|
564
|
-
|
|
565
|
-
expect(code).toBe(1);
|
|
566
|
-
expect(cap.stderr).toContain("no declared inputs");
|
|
567
|
-
expect(cap.stderr).toContain("--mode");
|
|
568
|
-
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
test("structured workflow rejects positional prompt tokens", async () => {
|
|
572
|
-
await writeCompiledWorkflow({
|
|
573
|
-
name: "structured",
|
|
574
|
-
agent: "copilot",
|
|
575
|
-
source: `
|
|
576
|
-
import { defineWorkflow } from "${join(process.cwd(), "src/sdk/workflows/index.ts")}";
|
|
577
|
-
|
|
578
|
-
export default defineWorkflow({
|
|
579
|
-
name: "structured",
|
|
580
|
-
inputs: [
|
|
581
|
-
{ name: "topic", type: "string", required: true },
|
|
582
|
-
],
|
|
583
|
-
})
|
|
584
|
-
.run(async () => {})
|
|
585
|
-
.compile();
|
|
586
|
-
`,
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
const cap = captureOutput();
|
|
590
|
-
const code = await workflowCommand({
|
|
591
|
-
name: "structured",
|
|
592
|
-
agent: "copilot",
|
|
593
|
-
// Positional-only invocation is ambiguous against a structured
|
|
594
|
-
// schema, so the command refuses to guess.
|
|
595
|
-
passthroughArgs: ["just", "a", "prompt"],
|
|
596
|
-
cwd: tempDir,
|
|
597
|
-
});
|
|
598
|
-
cap.restore();
|
|
599
|
-
|
|
600
|
-
expect(code).toBe(1);
|
|
601
|
-
expect(cap.stderr).toContain("structured inputs");
|
|
602
|
-
expect(cap.stderr).toContain("--topic");
|
|
603
|
-
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
test("structured workflow surfaces schema validation errors", async () => {
|
|
607
|
-
await writeCompiledWorkflow({
|
|
608
|
-
name: "validated",
|
|
609
|
-
agent: "copilot",
|
|
610
|
-
source: `
|
|
611
|
-
import { defineWorkflow } from "${join(process.cwd(), "src/sdk/workflows/index.ts")}";
|
|
117
|
+
// ─── exitOverride helper ──────────────────────────────────────────────────────
|
|
118
|
+
// Calling exitOverride() converts Commander's process.exit(1) into a thrown
|
|
119
|
+
// Error so tests can assert on rejection without killing the process.
|
|
612
120
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
{ name: "topic", type: "string", required: true },
|
|
617
|
-
],
|
|
618
|
-
})
|
|
619
|
-
.run(async () => {})
|
|
620
|
-
.compile();
|
|
621
|
-
`,
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const cap = captureOutput();
|
|
625
|
-
const code = await workflowCommand({
|
|
626
|
-
name: "validated",
|
|
627
|
-
agent: "copilot",
|
|
628
|
-
// Empty flag set — required `topic` is missing.
|
|
629
|
-
passthroughArgs: [],
|
|
630
|
-
cwd: tempDir,
|
|
631
|
-
});
|
|
632
|
-
cap.restore();
|
|
121
|
+
function enableExitOverride(): void {
|
|
122
|
+
workflowCommand.exitOverride();
|
|
123
|
+
}
|
|
633
124
|
|
|
634
|
-
|
|
635
|
-
|
|
125
|
+
// ─── Listing removed from dispatcher flags ──────────────────────────────────
|
|
126
|
+
//
|
|
127
|
+
// `--list` / `-l` used to live on the dispatcher command as a flag. It's
|
|
128
|
+
// since moved to a dedicated `atomic workflow list` subcommand (registered
|
|
129
|
+
// in src/cli.ts, implemented in ./workflow-list.ts) because the flag form
|
|
130
|
+
// had confusing interactions with argv parsing. The dispatcher itself no
|
|
131
|
+
// longer accepts the flag.
|
|
132
|
+
|
|
133
|
+
describe("workflowCommand: --list flag removed", () => {
|
|
134
|
+
test("--list is not a recognised dispatcher option", async () => {
|
|
135
|
+
enableExitOverride();
|
|
136
|
+
let threw = false;
|
|
137
|
+
const cap = captureOutput();
|
|
138
|
+
try {
|
|
139
|
+
await workflowCommand.parseAsync(["node", "cli", "--list"]);
|
|
140
|
+
} catch {
|
|
141
|
+
threw = true;
|
|
142
|
+
} finally {
|
|
143
|
+
cap.restore();
|
|
144
|
+
}
|
|
145
|
+
expect(threw).toBe(true);
|
|
636
146
|
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
637
147
|
});
|
|
638
148
|
});
|
|
639
149
|
|
|
640
|
-
// ─── Named mode success
|
|
150
|
+
// ─── Named mode success ───────────────────────────────────────────────────────
|
|
641
151
|
|
|
642
|
-
describe("workflowCommand named
|
|
643
|
-
test("
|
|
644
|
-
await
|
|
152
|
+
describe("workflowCommand named mode — success", () => {
|
|
153
|
+
test("dispatches ralph/claude with prompt to executor", async () => {
|
|
154
|
+
await workflowCommand.parseAsync([
|
|
155
|
+
"node", "cli",
|
|
156
|
+
"-n", "ralph",
|
|
157
|
+
"-a", "claude",
|
|
158
|
+
"--prompt", "fix the auth bug",
|
|
159
|
+
]);
|
|
645
160
|
|
|
646
|
-
const cap = captureOutput();
|
|
647
|
-
const code = await workflowCommand({
|
|
648
|
-
name: "runs",
|
|
649
|
-
agent: "copilot",
|
|
650
|
-
passthroughArgs: ["fix", "the", "bug"],
|
|
651
|
-
cwd: tempDir,
|
|
652
|
-
});
|
|
653
|
-
cap.restore();
|
|
654
|
-
|
|
655
|
-
expect(code).toBe(0);
|
|
656
161
|
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
657
|
-
const call =
|
|
658
|
-
expect(call.agent).toBe("
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
expect(call.inputs).toEqual({ prompt: "fix the bug" });
|
|
662
|
-
expect((call.definition as WorkflowDefinition).name).toBe("runs");
|
|
162
|
+
const call = executeWorkflowCalls[0]!;
|
|
163
|
+
expect(call.agent).toBe("claude");
|
|
164
|
+
expect(call.inputs?.["prompt"]).toBe("fix the auth bug");
|
|
165
|
+
expect(call.workflowKey).toBe("claude/ralph");
|
|
663
166
|
});
|
|
664
167
|
|
|
665
|
-
test("
|
|
666
|
-
await
|
|
168
|
+
test("dispatches ralph/copilot successfully", async () => {
|
|
169
|
+
await workflowCommand.parseAsync([
|
|
170
|
+
"node", "cli",
|
|
171
|
+
"-n", "ralph",
|
|
172
|
+
"-a", "copilot",
|
|
173
|
+
"--prompt", "review this PR",
|
|
174
|
+
]);
|
|
667
175
|
|
|
668
|
-
const cap = captureOutput();
|
|
669
|
-
const code = await workflowCommand({
|
|
670
|
-
name: "silent",
|
|
671
|
-
agent: "copilot",
|
|
672
|
-
passthroughArgs: [],
|
|
673
|
-
cwd: tempDir,
|
|
674
|
-
});
|
|
675
|
-
cap.restore();
|
|
676
|
-
|
|
677
|
-
expect(code).toBe(0);
|
|
678
176
|
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
679
|
-
|
|
177
|
+
const call = executeWorkflowCalls[0]!;
|
|
178
|
+
expect(call.agent).toBe("copilot");
|
|
179
|
+
expect(call.inputs?.["prompt"]).toBe("review this PR");
|
|
680
180
|
});
|
|
681
181
|
|
|
682
|
-
test("
|
|
683
|
-
await
|
|
182
|
+
test("dispatches ralph/opencode successfully", async () => {
|
|
183
|
+
await workflowCommand.parseAsync([
|
|
184
|
+
"node", "cli",
|
|
185
|
+
"-n", "ralph",
|
|
186
|
+
"-a", "opencode",
|
|
187
|
+
"--prompt", "refactor the service layer",
|
|
188
|
+
]);
|
|
684
189
|
|
|
685
|
-
const cap = captureOutput();
|
|
686
|
-
const code = await workflowCommand({
|
|
687
|
-
name: "detached",
|
|
688
|
-
agent: "copilot",
|
|
689
|
-
detach: true,
|
|
690
|
-
passthroughArgs: ["run", "in", "bg"],
|
|
691
|
-
cwd: tempDir,
|
|
692
|
-
});
|
|
693
|
-
cap.restore();
|
|
694
|
-
|
|
695
|
-
expect(code).toBe(0);
|
|
696
190
|
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
697
|
-
expect(
|
|
191
|
+
expect(executeWorkflowCalls[0]!.agent).toBe("opencode");
|
|
698
192
|
});
|
|
699
193
|
|
|
700
|
-
test("
|
|
701
|
-
await
|
|
194
|
+
test("dispatches deep-research-codebase/claude with prompt", async () => {
|
|
195
|
+
await workflowCommand.parseAsync([
|
|
196
|
+
"node", "cli",
|
|
197
|
+
"-n", "deep-research-codebase",
|
|
198
|
+
"-a", "claude",
|
|
199
|
+
"--prompt", "how does auth work",
|
|
200
|
+
]);
|
|
702
201
|
|
|
703
|
-
const cap = captureOutput();
|
|
704
|
-
const code = await workflowCommand({
|
|
705
|
-
name: "default-attach",
|
|
706
|
-
agent: "copilot",
|
|
707
|
-
passthroughArgs: [],
|
|
708
|
-
cwd: tempDir,
|
|
709
|
-
});
|
|
710
|
-
cap.restore();
|
|
711
|
-
|
|
712
|
-
expect(code).toBe(0);
|
|
713
202
|
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
714
|
-
expect(
|
|
203
|
+
expect(executeWorkflowCalls[0]!.workflowKey).toBe("claude/deep-research-codebase");
|
|
715
204
|
});
|
|
716
205
|
|
|
717
|
-
test("
|
|
718
|
-
await
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
name: "struct-run",
|
|
726
|
-
inputs: [
|
|
727
|
-
{ name: "topic", type: "string", required: true },
|
|
728
|
-
{ name: "depth", type: "enum", values: ["shallow", "deep"], default: "shallow" },
|
|
729
|
-
],
|
|
730
|
-
})
|
|
731
|
-
.run(async () => {})
|
|
732
|
-
.compile();
|
|
733
|
-
`,
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
const cap = captureOutput();
|
|
737
|
-
const code = await workflowCommand({
|
|
738
|
-
name: "struct-run",
|
|
739
|
-
agent: "copilot",
|
|
740
|
-
passthroughArgs: ["--topic=authz", "--depth=deep"],
|
|
741
|
-
cwd: tempDir,
|
|
742
|
-
});
|
|
743
|
-
cap.restore();
|
|
206
|
+
test("--detach flag threads detach=true to executor", async () => {
|
|
207
|
+
await workflowCommand.parseAsync([
|
|
208
|
+
"node", "cli",
|
|
209
|
+
"-n", "ralph",
|
|
210
|
+
"-a", "claude",
|
|
211
|
+
"--prompt", "test",
|
|
212
|
+
"--detach",
|
|
213
|
+
]);
|
|
744
214
|
|
|
745
|
-
expect(code).toBe(0);
|
|
746
215
|
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
747
|
-
expect(
|
|
748
|
-
topic: "authz",
|
|
749
|
-
depth: "deep",
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
test("runLoadedWorkflow surfaces executor failures as exit code 1", async () => {
|
|
754
|
-
await writeCompiledWorkflow({ name: "boom", agent: "copilot" });
|
|
755
|
-
|
|
756
|
-
executeWorkflowMock.mockImplementationOnce(async () => {
|
|
757
|
-
throw new Error("tmux is on fire");
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
const cap = captureOutput();
|
|
761
|
-
const code = await workflowCommand({
|
|
762
|
-
name: "boom",
|
|
763
|
-
agent: "copilot",
|
|
764
|
-
passthroughArgs: ["try", "it"],
|
|
765
|
-
cwd: tempDir,
|
|
766
|
-
});
|
|
767
|
-
cap.restore();
|
|
768
|
-
|
|
769
|
-
expect(code).toBe(1);
|
|
770
|
-
expect(cap.stderr).toContain("Workflow failed");
|
|
771
|
-
expect(cap.stderr).toContain("tmux is on fire");
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
test("runLoadedWorkflow stringifies non-Error throwns", async () => {
|
|
775
|
-
await writeCompiledWorkflow({ name: "non-err", agent: "copilot" });
|
|
776
|
-
|
|
777
|
-
executeWorkflowMock.mockImplementationOnce(async () => {
|
|
778
|
-
// Thrown value is a plain string — the catch branch falls back to
|
|
779
|
-
// `String(error)` rather than reading `.message`.
|
|
780
|
-
throw "raw string failure";
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
const cap = captureOutput();
|
|
784
|
-
const code = await workflowCommand({
|
|
785
|
-
name: "non-err",
|
|
786
|
-
agent: "copilot",
|
|
787
|
-
passthroughArgs: [],
|
|
788
|
-
cwd: tempDir,
|
|
789
|
-
});
|
|
790
|
-
cap.restore();
|
|
791
|
-
|
|
792
|
-
expect(code).toBe(1);
|
|
793
|
-
expect(cap.stderr).toContain("raw string failure");
|
|
216
|
+
expect(executeWorkflowCalls[0]!.detach).toBe(true);
|
|
794
217
|
});
|
|
795
|
-
});
|
|
796
218
|
|
|
797
|
-
|
|
219
|
+
test("-d shorthand also sets detach=true", async () => {
|
|
220
|
+
await workflowCommand.parseAsync([
|
|
221
|
+
"node", "cli",
|
|
222
|
+
"-n", "ralph",
|
|
223
|
+
"-a", "claude",
|
|
224
|
+
"--prompt", "test",
|
|
225
|
+
"-d",
|
|
226
|
+
]);
|
|
798
227
|
|
|
799
|
-
describe("workflowCommand ATOMIC_AGENT inference", () => {
|
|
800
|
-
// Top-level beforeEach already clears ATOMIC_AGENT; tests that need it
|
|
801
|
-
// set it explicitly and rely on the next test's clear to reset.
|
|
802
|
-
|
|
803
|
-
test("infers -a from ATOMIC_AGENT when omitted", async () => {
|
|
804
|
-
// Agents spawned inside an atomic chat/workflow pane inherit
|
|
805
|
-
// ATOMIC_AGENT. Re-passing their own provider back through `-a` is
|
|
806
|
-
// boilerplate we can eliminate.
|
|
807
|
-
await writeCompiledWorkflow({ name: "inferred", agent: "claude" });
|
|
808
|
-
process.env.ATOMIC_AGENT = "claude";
|
|
809
|
-
|
|
810
|
-
const cap = captureOutput();
|
|
811
|
-
const code = await workflowCommand({
|
|
812
|
-
name: "inferred",
|
|
813
|
-
// no agent passed
|
|
814
|
-
passthroughArgs: ["go"],
|
|
815
|
-
cwd: tempDir,
|
|
816
|
-
});
|
|
817
|
-
cap.restore();
|
|
818
|
-
|
|
819
|
-
expect(code).toBe(0);
|
|
820
228
|
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
821
|
-
expect(
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
test("forces detach=true when ATOMIC_AGENT is set", async () => {
|
|
825
|
-
// Attaching from inside the atomic socket would switch-client the
|
|
826
|
-
// caller's own terminal onto the new workflow session — hijacking the
|
|
827
|
-
// very pane the agent is running in. Force detach so the command
|
|
828
|
-
// returns immediately and the caller can attach on their own terms.
|
|
829
|
-
await writeCompiledWorkflow({ name: "auto-detach", agent: "copilot" });
|
|
830
|
-
process.env.ATOMIC_AGENT = "copilot";
|
|
831
|
-
|
|
832
|
-
const cap = captureOutput();
|
|
833
|
-
const code = await workflowCommand({
|
|
834
|
-
name: "auto-detach",
|
|
835
|
-
agent: "copilot",
|
|
836
|
-
// detach intentionally omitted
|
|
837
|
-
passthroughArgs: [],
|
|
838
|
-
cwd: tempDir,
|
|
839
|
-
});
|
|
840
|
-
cap.restore();
|
|
841
|
-
|
|
842
|
-
expect(code).toBe(0);
|
|
843
|
-
expect(executeWorkflowMock.mock.calls[0]![0].detach).toBe(true);
|
|
229
|
+
expect(executeWorkflowCalls[0]!.detach).toBe(true);
|
|
844
230
|
});
|
|
845
231
|
|
|
846
|
-
test("
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
232
|
+
test("detach defaults to false when flag omitted", async () => {
|
|
233
|
+
await workflowCommand.parseAsync([
|
|
234
|
+
"node", "cli",
|
|
235
|
+
"-n", "ralph",
|
|
236
|
+
"-a", "claude",
|
|
237
|
+
"--prompt", "test",
|
|
238
|
+
]);
|
|
851
239
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
name: "override",
|
|
855
|
-
agent: "copilot",
|
|
856
|
-
passthroughArgs: [],
|
|
857
|
-
cwd: tempDir,
|
|
858
|
-
});
|
|
859
|
-
cap.restore();
|
|
860
|
-
|
|
861
|
-
expect(code).toBe(0);
|
|
862
|
-
expect(executeWorkflowMock.mock.calls[0]![0].agent).toBe("copilot");
|
|
240
|
+
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
241
|
+
expect(executeWorkflowCalls[0]!.detach).toBe(false);
|
|
863
242
|
});
|
|
864
243
|
|
|
865
|
-
test("
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
244
|
+
test("integer input --max_loops is forwarded to executor", async () => {
|
|
245
|
+
await workflowCommand.parseAsync([
|
|
246
|
+
"node", "cli",
|
|
247
|
+
"-n", "ralph",
|
|
248
|
+
"-a", "claude",
|
|
249
|
+
"--prompt", "test",
|
|
250
|
+
"--max_loops", "3",
|
|
251
|
+
]);
|
|
873
252
|
|
|
874
|
-
expect(
|
|
875
|
-
expect(
|
|
253
|
+
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
254
|
+
expect(executeWorkflowCalls[0]!.inputs?.["max_loops"]).toBe("3");
|
|
876
255
|
});
|
|
877
256
|
|
|
878
|
-
test("
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const code = await workflowCommand({
|
|
886
|
-
name: "anything",
|
|
887
|
-
cwd: tempDir,
|
|
888
|
-
});
|
|
889
|
-
cap.restore();
|
|
257
|
+
test("workflowKey is always <agent>/<name>", async () => {
|
|
258
|
+
await workflowCommand.parseAsync([
|
|
259
|
+
"node", "cli",
|
|
260
|
+
"-n", "deep-research-codebase",
|
|
261
|
+
"-a", "copilot",
|
|
262
|
+
"--prompt", "research something",
|
|
263
|
+
]);
|
|
890
264
|
|
|
891
|
-
expect(
|
|
892
|
-
expect(
|
|
265
|
+
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
266
|
+
expect(executeWorkflowCalls[0]!.workflowKey).toBe("copilot/deep-research-codebase");
|
|
893
267
|
});
|
|
894
268
|
});
|
|
895
269
|
|
|
896
|
-
// ───
|
|
897
|
-
|
|
898
|
-
describe("workflowCommand
|
|
899
|
-
test("
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
expect(
|
|
915
|
-
expect(
|
|
270
|
+
// ─── Named mode — error paths ─────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
describe("workflowCommand named mode — error paths", () => {
|
|
273
|
+
test("unknown workflow name throws (Commander exits via exitOverride)", async () => {
|
|
274
|
+
enableExitOverride();
|
|
275
|
+
let threw = false;
|
|
276
|
+
const cap = captureOutput();
|
|
277
|
+
try {
|
|
278
|
+
await workflowCommand.parseAsync([
|
|
279
|
+
"node", "cli",
|
|
280
|
+
"-n", "bogus-workflow",
|
|
281
|
+
"-a", "claude",
|
|
282
|
+
]);
|
|
283
|
+
} catch (_e) {
|
|
284
|
+
threw = true;
|
|
285
|
+
} finally {
|
|
286
|
+
cap.restore();
|
|
287
|
+
}
|
|
288
|
+
expect(threw).toBe(true);
|
|
289
|
+
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
916
290
|
});
|
|
917
291
|
|
|
918
|
-
test("
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
expect(
|
|
934
|
-
|
|
935
|
-
expect(cap.stderr).toMatch(/(tmux|psmux) is not installed/);
|
|
292
|
+
test("unknown agent throws (Commander exits via exitOverride)", async () => {
|
|
293
|
+
enableExitOverride();
|
|
294
|
+
let threw = false;
|
|
295
|
+
const cap = captureOutput();
|
|
296
|
+
try {
|
|
297
|
+
await workflowCommand.parseAsync([
|
|
298
|
+
"node", "cli",
|
|
299
|
+
"-n", "ralph",
|
|
300
|
+
"-a", "bogus-agent",
|
|
301
|
+
]);
|
|
302
|
+
} catch (_e) {
|
|
303
|
+
threw = true;
|
|
304
|
+
} finally {
|
|
305
|
+
cap.restore();
|
|
306
|
+
}
|
|
307
|
+
expect(threw).toBe(true);
|
|
308
|
+
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
936
309
|
});
|
|
937
310
|
|
|
938
|
-
test("
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
expect(
|
|
956
|
-
expect(cap.stderr).toContain("Not logged in to GitHub Copilot CLI");
|
|
957
|
-
expect(cap.stderr).toContain("oauth token missing");
|
|
958
|
-
expect(cap.stderr).toContain("copilot");
|
|
959
|
-
// Downstream installers never run — the auth gate short-circuits.
|
|
960
|
-
expect(ensureTmuxInstalledMock).not.toHaveBeenCalled();
|
|
311
|
+
test("missing required prompt for ralph throws from validateAndResolve", async () => {
|
|
312
|
+
enableExitOverride();
|
|
313
|
+
let threw = false;
|
|
314
|
+
const cap = captureOutput();
|
|
315
|
+
try {
|
|
316
|
+
await workflowCommand.parseAsync([
|
|
317
|
+
"node", "cli",
|
|
318
|
+
"-n", "ralph",
|
|
319
|
+
"-a", "claude",
|
|
320
|
+
// --prompt intentionally omitted
|
|
321
|
+
]);
|
|
322
|
+
} catch (_e) {
|
|
323
|
+
threw = true;
|
|
324
|
+
} finally {
|
|
325
|
+
cap.restore();
|
|
326
|
+
}
|
|
327
|
+
expect(threw).toBe(true);
|
|
328
|
+
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
961
329
|
});
|
|
962
330
|
|
|
963
|
-
test("
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
expect(
|
|
982
|
-
// The crash message never surfaces — the catch block just swallows it.
|
|
983
|
-
expect(cap.stderr).not.toContain("installer crashed");
|
|
984
|
-
expect(cap.stderr).toMatch(/(tmux|psmux) is not installed/);
|
|
331
|
+
test("non-integer value for --max_loops throws from validateAndResolve", async () => {
|
|
332
|
+
enableExitOverride();
|
|
333
|
+
let threw = false;
|
|
334
|
+
const cap = captureOutput();
|
|
335
|
+
try {
|
|
336
|
+
await workflowCommand.parseAsync([
|
|
337
|
+
"node", "cli",
|
|
338
|
+
"-n", "ralph",
|
|
339
|
+
"-a", "claude",
|
|
340
|
+
"--prompt", "test",
|
|
341
|
+
"--max_loops", "not-an-int",
|
|
342
|
+
]);
|
|
343
|
+
} catch (_e) {
|
|
344
|
+
threw = true;
|
|
345
|
+
} finally {
|
|
346
|
+
cap.restore();
|
|
347
|
+
}
|
|
348
|
+
expect(threw).toBe(true);
|
|
349
|
+
expect(executeWorkflowMock).not.toHaveBeenCalled();
|
|
985
350
|
});
|
|
986
351
|
});
|
|
987
352
|
|
|
988
|
-
// ───
|
|
989
|
-
|
|
990
|
-
describe("workflowCommand picker discovery branches", () => {
|
|
991
|
-
test("returns 1 when discovery finds zero workflows", async () => {
|
|
992
|
-
// Picker mode without any workflows on disk — the CLI should explain
|
|
993
|
-
// where to put a new workflow rather than render an empty picker.
|
|
994
|
-
discoverWorkflowsMock.mockImplementationOnce(async () => []);
|
|
353
|
+
// ─── Enum input coercion ──────────────────────────────────────────────────────
|
|
995
354
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
355
|
+
describe("workflowCommand enum input coercion", () => {
|
|
356
|
+
test("valid enum value accepted for open-claude-design --output-type", async () => {
|
|
357
|
+
await workflowCommand.parseAsync([
|
|
358
|
+
"node", "cli",
|
|
359
|
+
"-n", "open-claude-design",
|
|
360
|
+
"-a", "claude",
|
|
361
|
+
"--prompt", "design a button",
|
|
362
|
+
"--output-type", "prototype",
|
|
363
|
+
]);
|
|
1002
364
|
|
|
1003
|
-
expect(
|
|
1004
|
-
expect(
|
|
1005
|
-
expect(cap.stderr).toContain(".atomic/workflows/<name>/copilot/index.ts");
|
|
365
|
+
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
366
|
+
expect(executeWorkflowCalls[0]!.inputs?.["output-type"]).toBe("prototype");
|
|
1006
367
|
});
|
|
1007
368
|
|
|
1008
|
-
test("
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
loadWorkflowsMetadataMock.mockImplementationOnce(async () => []);
|
|
1020
|
-
|
|
1021
|
-
const cap = captureOutput();
|
|
1022
|
-
const code = await workflowCommand({
|
|
1023
|
-
agent: "copilot",
|
|
1024
|
-
cwd: tempDir,
|
|
1025
|
-
});
|
|
1026
|
-
cap.restore();
|
|
369
|
+
test("default enum value applied when --output-type omitted", async () => {
|
|
370
|
+
// output-type has default "prototype" — validateAndResolve fills it in.
|
|
371
|
+
// Note: Commander camelCases hyphenated flags (output-type → outputType),
|
|
372
|
+
// so the CLI flag lookup for "output-type" falls through to the default.
|
|
373
|
+
await workflowCommand.parseAsync([
|
|
374
|
+
"node", "cli",
|
|
375
|
+
"-n", "open-claude-design",
|
|
376
|
+
"-a", "claude",
|
|
377
|
+
"--prompt", "design a button",
|
|
378
|
+
// --output-type intentionally omitted
|
|
379
|
+
]);
|
|
1027
380
|
|
|
1028
|
-
expect(
|
|
1029
|
-
expect(
|
|
381
|
+
expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
|
|
382
|
+
expect(executeWorkflowCalls[0]!.inputs?.["output-type"]).toBe("prototype");
|
|
1030
383
|
});
|
|
1031
384
|
});
|
|
1032
|
-
|
|
1033
|
-
// Note on the picker success path: the branches that actually open the
|
|
1034
|
-
// interactive picker (runPickerMode lines after the "no workflows found" and
|
|
1035
|
-
// "all failed to load" guards, plus all of runResolvedSelection) are not
|
|
1036
|
-
// covered from this file. Exercising them requires mocking
|
|
1037
|
-
// `WorkflowPickerPanel`, which is a side-effectful class that spins up a
|
|
1038
|
-
// real CliRenderer on stdin/stdout. Mocking it process-wide via mock.module
|
|
1039
|
-
// leaks into the WorkflowPickerPanel's own unit tests (they share the same
|
|
1040
|
-
// bun test process) and breaks them — the same live-binding issue that
|
|
1041
|
-
// mock.module has with other consumers in the suite. Rather than fight the
|
|
1042
|
-
// tooling, we accept a small amount of uncovered code in the picker success
|
|
1043
|
-
// path; the remaining coverage comfortably clears the per-file threshold.
|