@bastani/atomic 0.5.34 → 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
|
@@ -12,12 +12,9 @@ import {
|
|
|
12
12
|
renderInputsText,
|
|
13
13
|
workflowInputsCommand,
|
|
14
14
|
type WorkflowInputsDeps,
|
|
15
|
+
type ResolvedWorkflowEntry,
|
|
15
16
|
} from "./workflow-inputs.ts";
|
|
16
|
-
import type {
|
|
17
|
-
WorkflowInput,
|
|
18
|
-
DiscoveredWorkflow,
|
|
19
|
-
WorkflowDefinition,
|
|
20
|
-
} from "../../sdk/workflows/index.ts";
|
|
17
|
+
import type { AgentType, WorkflowInput, WorkflowDefinition } from "../../sdk/workflows/index.ts";
|
|
21
18
|
|
|
22
19
|
let originalNoColor: string | undefined;
|
|
23
20
|
beforeAll(() => {
|
|
@@ -153,12 +150,10 @@ function captureOutput(): {
|
|
|
153
150
|
};
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
function fakeDiscovered(name: string):
|
|
153
|
+
function fakeDiscovered(name: string): ResolvedWorkflowEntry {
|
|
157
154
|
return {
|
|
158
155
|
name,
|
|
159
156
|
agent: "claude",
|
|
160
|
-
path: `/fake/path/${name}.ts`,
|
|
161
|
-
source: "builtin",
|
|
162
157
|
};
|
|
163
158
|
}
|
|
164
159
|
|
|
@@ -166,10 +161,12 @@ function fakeDefinition(
|
|
|
166
161
|
name: string,
|
|
167
162
|
description: string,
|
|
168
163
|
inputs: WorkflowInput[],
|
|
164
|
+
agent: AgentType = "claude",
|
|
169
165
|
): WorkflowDefinition {
|
|
170
166
|
return {
|
|
171
167
|
__brand: "WorkflowDefinition",
|
|
172
168
|
name,
|
|
169
|
+
agent,
|
|
173
170
|
description,
|
|
174
171
|
inputs,
|
|
175
172
|
minSDKVersion: null,
|
|
@@ -181,11 +178,9 @@ function makeDeps(overrides: Partial<WorkflowInputsDeps> = {}): WorkflowInputsDe
|
|
|
181
178
|
return {
|
|
182
179
|
findWorkflow: mock(async () => fakeDiscovered("gen-spec")) as unknown as
|
|
183
180
|
WorkflowInputsDeps["findWorkflow"],
|
|
184
|
-
loadWorkflow: mock(async (
|
|
181
|
+
loadWorkflow: mock(async () => ({
|
|
185
182
|
ok: true,
|
|
186
183
|
value: {
|
|
187
|
-
...plan,
|
|
188
|
-
warnings: [],
|
|
189
184
|
definition: fakeDefinition("gen-spec", "spec generator", [
|
|
190
185
|
{ name: "research_doc", type: "string", required: true },
|
|
191
186
|
]),
|
|
@@ -319,4 +314,39 @@ describe("workflowInputsCommand", () => {
|
|
|
319
314
|
cap.restore();
|
|
320
315
|
}
|
|
321
316
|
});
|
|
317
|
+
|
|
318
|
+
test("default deps resolve against the builtin registry on success", async () => {
|
|
319
|
+
// Omit the deps argument so the module-level `defaultDeps` runs —
|
|
320
|
+
// this exercises `registryFindWorkflow` + `registryLoadWorkflow`.
|
|
321
|
+
const cap = captureOutput();
|
|
322
|
+
try {
|
|
323
|
+
const code = await workflowInputsCommand({
|
|
324
|
+
name: "ralph",
|
|
325
|
+
agent: "claude",
|
|
326
|
+
format: "json",
|
|
327
|
+
});
|
|
328
|
+
expect(code).toBe(0);
|
|
329
|
+
const parsed = JSON.parse(cap.stdout());
|
|
330
|
+
expect(parsed.workflow).toBe("ralph");
|
|
331
|
+
expect(parsed.agent).toBe("claude");
|
|
332
|
+
} finally {
|
|
333
|
+
cap.restore();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("default deps report 'not found' for an unknown workflow", async () => {
|
|
338
|
+
const cap = captureOutput();
|
|
339
|
+
try {
|
|
340
|
+
const code = await workflowInputsCommand({
|
|
341
|
+
name: "definitely-not-a-real-workflow",
|
|
342
|
+
agent: "claude",
|
|
343
|
+
format: "json",
|
|
344
|
+
});
|
|
345
|
+
expect(code).toBe(1);
|
|
346
|
+
const parsed = JSON.parse(cap.stdout());
|
|
347
|
+
expect(parsed.error).toContain("not found");
|
|
348
|
+
} finally {
|
|
349
|
+
cap.restore();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
322
352
|
});
|
|
@@ -13,12 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { COLORS, createPainter } from "../../theme/colors.ts";
|
|
16
|
-
import { AGENT_CONFIG
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
WorkflowLoader,
|
|
20
|
-
} from "../../sdk/workflows/index.ts";
|
|
21
|
-
import type { WorkflowInput } from "../../sdk/workflows/index.ts";
|
|
16
|
+
import { AGENT_CONFIG } from "../../services/config/index.ts";
|
|
17
|
+
import { createBuiltinRegistry } from "../../sdk/workflows/builtin-registry.ts";
|
|
18
|
+
import type { WorkflowInput, WorkflowDefinition, AgentType } from "../../sdk/workflows/index.ts";
|
|
22
19
|
|
|
23
20
|
export type WorkflowInputsFormat = "json" | "text";
|
|
24
21
|
|
|
@@ -151,19 +148,57 @@ export interface WorkflowInputsOptions {
|
|
|
151
148
|
cwd?: string;
|
|
152
149
|
}
|
|
153
150
|
|
|
151
|
+
/**
|
|
152
|
+
* A resolved workflow entry — enough information for the inputs command
|
|
153
|
+
* to load and render the workflow's declared schema.
|
|
154
|
+
*/
|
|
155
|
+
export interface ResolvedWorkflowEntry {
|
|
156
|
+
name: string;
|
|
157
|
+
agent: AgentType;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Result of loading a workflow definition — either success with the
|
|
162
|
+
* definition or a failure with a stage label and message.
|
|
163
|
+
*/
|
|
164
|
+
export type WorkflowLoadResult =
|
|
165
|
+
| { ok: true; value: { definition: WorkflowDefinition } }
|
|
166
|
+
| { ok: false; stage: string; error: unknown; message: string };
|
|
167
|
+
|
|
154
168
|
/**
|
|
155
169
|
* Deps for `workflowInputsCommand`. Injected so tests can drive every
|
|
156
170
|
* branch (unknown agent / missing workflow / load failure / success)
|
|
157
|
-
* without the
|
|
171
|
+
* without touching the real registry or filesystem.
|
|
158
172
|
*/
|
|
159
173
|
export interface WorkflowInputsDeps {
|
|
160
|
-
findWorkflow:
|
|
161
|
-
loadWorkflow:
|
|
174
|
+
findWorkflow: (name: string, agent: AgentType, cwd?: string) => Promise<ResolvedWorkflowEntry | null>;
|
|
175
|
+
loadWorkflow: (entry: ResolvedWorkflowEntry) => Promise<WorkflowLoadResult>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function registryFindWorkflow(name: string, agent: AgentType): Promise<ResolvedWorkflowEntry | null> {
|
|
179
|
+
const registry = createBuiltinRegistry();
|
|
180
|
+
const wf = registry.resolve(name, agent);
|
|
181
|
+
if (!wf) return Promise.resolve(null);
|
|
182
|
+
return Promise.resolve({ name: wf.name, agent: wf.agent });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function registryLoadWorkflow(entry: ResolvedWorkflowEntry): Promise<WorkflowLoadResult> {
|
|
186
|
+
const registry = createBuiltinRegistry();
|
|
187
|
+
const wf = registry.resolve(entry.name, entry.agent);
|
|
188
|
+
if (!wf) {
|
|
189
|
+
return Promise.resolve({
|
|
190
|
+
ok: false,
|
|
191
|
+
stage: "resolve",
|
|
192
|
+
error: new Error(`Workflow not found: ${entry.agent}/${entry.name}`),
|
|
193
|
+
message: `Workflow not found: ${entry.agent}/${entry.name}`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return Promise.resolve({ ok: true, value: { definition: wf } });
|
|
162
197
|
}
|
|
163
198
|
|
|
164
199
|
const defaultDeps: WorkflowInputsDeps = {
|
|
165
|
-
findWorkflow:
|
|
166
|
-
loadWorkflow:
|
|
200
|
+
findWorkflow: registryFindWorkflow,
|
|
201
|
+
loadWorkflow: registryLoadWorkflow,
|
|
167
202
|
};
|
|
168
203
|
|
|
169
204
|
/**
|
|
@@ -185,7 +220,7 @@ export async function workflowInputsCommand(
|
|
|
185
220
|
`Unknown agent '${options.agent}'. Valid agents: ${validAgents.join(", ")}`,
|
|
186
221
|
);
|
|
187
222
|
}
|
|
188
|
-
const agent = options.agent as
|
|
223
|
+
const agent = options.agent as AgentType;
|
|
189
224
|
|
|
190
225
|
const discovered = await deps.findWorkflow(options.name, agent, options.cwd);
|
|
191
226
|
if (!discovered) {
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `atomic workflow list` — the subcommand that replaced
|
|
3
|
+
* the old dispatcher `-l/--list` flag.
|
|
4
|
+
*
|
|
5
|
+
* The command itself is thin; these tests cover the behaviors that
|
|
6
|
+
* would silently break if the filter logic regressed: agent filtering,
|
|
7
|
+
* empty results, unknown agent rejection, and the sort order that
|
|
8
|
+
* keeps output stable across runs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, beforeAll, beforeEach, afterAll, afterEach, mock } from "bun:test";
|
|
12
|
+
import { workflowListCommand } from "./workflow-list.ts";
|
|
13
|
+
import type { AgentType, WorkflowDefinition } from "../../sdk/workflows/index.ts";
|
|
14
|
+
|
|
15
|
+
function def(agent: AgentType, name: string, description = ""): WorkflowDefinition {
|
|
16
|
+
return {
|
|
17
|
+
__brand: "WorkflowDefinition",
|
|
18
|
+
agent,
|
|
19
|
+
name,
|
|
20
|
+
description,
|
|
21
|
+
inputs: [],
|
|
22
|
+
minSDKVersion: null,
|
|
23
|
+
run: async () => {},
|
|
24
|
+
} as unknown as WorkflowDefinition;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let originalNoColor: string | undefined;
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
originalNoColor = process.env.NO_COLOR;
|
|
30
|
+
process.env.NO_COLOR = "1";
|
|
31
|
+
});
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
if (originalNoColor === undefined) delete process.env.NO_COLOR;
|
|
34
|
+
else process.env.NO_COLOR = originalNoColor;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
interface Captured {
|
|
38
|
+
stdout: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
restore: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function captureOutput(): Captured {
|
|
44
|
+
const captured: Captured = { stdout: "", stderr: "", restore: () => {} };
|
|
45
|
+
const origStdout = process.stdout.write.bind(process.stdout);
|
|
46
|
+
const origStderr = process.stderr.write.bind(process.stderr);
|
|
47
|
+
process.stdout.write = ((chunk: string | Uint8Array): boolean => {
|
|
48
|
+
captured.stdout += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
49
|
+
return true;
|
|
50
|
+
}) as typeof process.stdout.write;
|
|
51
|
+
process.stderr.write = ((chunk: string | Uint8Array): boolean => {
|
|
52
|
+
captured.stderr += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
53
|
+
return true;
|
|
54
|
+
}) as typeof process.stderr.write;
|
|
55
|
+
captured.restore = () => {
|
|
56
|
+
process.stdout.write = origStdout;
|
|
57
|
+
process.stderr.write = origStderr;
|
|
58
|
+
};
|
|
59
|
+
return captured;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fixture = [
|
|
63
|
+
def("claude", "ralph", "iterative plan/orchestrate/review/debug loop"),
|
|
64
|
+
def("copilot", "ralph", "iterative plan/orchestrate/review/debug loop"),
|
|
65
|
+
def("opencode", "ralph", "iterative plan/orchestrate/review/debug loop"),
|
|
66
|
+
def("claude", "deep-research-codebase", "scout -> explorer fan-out"),
|
|
67
|
+
def("copilot", "deep-research-codebase", "scout -> explorer fan-out"),
|
|
68
|
+
def("opencode", "deep-research-codebase", "scout -> explorer fan-out"),
|
|
69
|
+
def("claude", "open-claude-design", "design system onboarding"),
|
|
70
|
+
def("copilot", "open-claude-design", "design system onboarding"),
|
|
71
|
+
def("opencode", "open-claude-design", "design system onboarding"),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
let savedNoColor: string | undefined;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
savedNoColor = process.env.NO_COLOR;
|
|
77
|
+
process.env.NO_COLOR = "1";
|
|
78
|
+
});
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
if (savedNoColor === undefined) delete process.env.NO_COLOR;
|
|
81
|
+
else process.env.NO_COLOR = savedNoColor;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("workflowListCommand", () => {
|
|
85
|
+
test("no agent filter: prints every registered workflow, grouped by name", async () => {
|
|
86
|
+
const cap = captureOutput();
|
|
87
|
+
let code: number;
|
|
88
|
+
try {
|
|
89
|
+
code = await workflowListCommand({}, { list: () => fixture });
|
|
90
|
+
} finally {
|
|
91
|
+
cap.restore();
|
|
92
|
+
}
|
|
93
|
+
expect(code).toBe(0);
|
|
94
|
+
expect(cap.stdout).toContain("ralph");
|
|
95
|
+
expect(cap.stdout).toContain("deep-research-codebase");
|
|
96
|
+
expect(cap.stdout).toContain("open-claude-design");
|
|
97
|
+
// Grouping deduplicates identical (name, description) pairs, so each
|
|
98
|
+
// workflow name appears exactly once.
|
|
99
|
+
expect(cap.stdout.match(/\bralph\b/g)?.length).toBe(1);
|
|
100
|
+
// Agents are rendered as a badge line under each group.
|
|
101
|
+
expect(cap.stdout).toContain("claude");
|
|
102
|
+
expect(cap.stdout).toContain("copilot");
|
|
103
|
+
expect(cap.stdout).toContain("opencode");
|
|
104
|
+
expect(cap.stdout).toContain(" · ");
|
|
105
|
+
// The old `agent=<name>` label is gone — grouping replaces it.
|
|
106
|
+
expect(cap.stdout).not.toContain("agent=");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("agent filter narrows to the selected agent only", async () => {
|
|
110
|
+
const cap = captureOutput();
|
|
111
|
+
let code: number;
|
|
112
|
+
try {
|
|
113
|
+
code = await workflowListCommand({ agent: "claude" }, { list: () => fixture });
|
|
114
|
+
} finally {
|
|
115
|
+
cap.restore();
|
|
116
|
+
}
|
|
117
|
+
expect(code).toBe(0);
|
|
118
|
+
// Every workflow with a claude variant appears.
|
|
119
|
+
expect(cap.stdout).toContain("ralph");
|
|
120
|
+
expect(cap.stdout).toContain("deep-research-codebase");
|
|
121
|
+
expect(cap.stdout).toContain("open-claude-design");
|
|
122
|
+
// Non-matching agents should not appear in the listing. We check for
|
|
123
|
+
// the agent-badge separator, not the strings themselves — a workflow
|
|
124
|
+
// named "open-claude-design" legitimately contains the token
|
|
125
|
+
// "claude" outside the badge list.
|
|
126
|
+
expect(cap.stdout).not.toContain("copilot");
|
|
127
|
+
expect(cap.stdout).not.toContain("opencode");
|
|
128
|
+
// Filtered output suppresses the agent-badge line entirely, since
|
|
129
|
+
// the filter itself provides the context.
|
|
130
|
+
expect(cap.stdout).not.toContain(" · ");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("unknown agent returns non-zero and writes an error message", async () => {
|
|
134
|
+
const cap = captureOutput();
|
|
135
|
+
let code: number;
|
|
136
|
+
try {
|
|
137
|
+
code = await workflowListCommand({ agent: "bogus" }, { list: () => fixture });
|
|
138
|
+
} finally {
|
|
139
|
+
cap.restore();
|
|
140
|
+
}
|
|
141
|
+
expect(code).toBe(1);
|
|
142
|
+
expect(cap.stderr).toContain("Unknown agent 'bogus'");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("empty registry prints a helpful placeholder", async () => {
|
|
146
|
+
const cap = captureOutput();
|
|
147
|
+
let code: number;
|
|
148
|
+
try {
|
|
149
|
+
code = await workflowListCommand({}, { list: () => [] });
|
|
150
|
+
} finally {
|
|
151
|
+
cap.restore();
|
|
152
|
+
}
|
|
153
|
+
expect(code).toBe(0);
|
|
154
|
+
expect(cap.stdout).toContain("No workflows registered.");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("agent filter with zero matches prints the placeholder, not an error", async () => {
|
|
158
|
+
const cap = captureOutput();
|
|
159
|
+
let code: number;
|
|
160
|
+
try {
|
|
161
|
+
code = await workflowListCommand(
|
|
162
|
+
{ agent: "opencode" },
|
|
163
|
+
{ list: () => [def("claude", "ralph")] },
|
|
164
|
+
);
|
|
165
|
+
} finally {
|
|
166
|
+
cap.restore();
|
|
167
|
+
}
|
|
168
|
+
expect(code).toBe(0);
|
|
169
|
+
expect(cap.stdout).toContain("No workflows registered.");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("groups are sorted alphabetically by workflow name", async () => {
|
|
173
|
+
const shuffled = [
|
|
174
|
+
def("opencode", "ralph"),
|
|
175
|
+
def("claude", "deep-research-codebase"),
|
|
176
|
+
def("claude", "ralph"),
|
|
177
|
+
def("copilot", "ralph"),
|
|
178
|
+
];
|
|
179
|
+
const cap = captureOutput();
|
|
180
|
+
try {
|
|
181
|
+
await workflowListCommand({}, { list: () => shuffled });
|
|
182
|
+
} finally {
|
|
183
|
+
cap.restore();
|
|
184
|
+
}
|
|
185
|
+
const deepIdx = cap.stdout.indexOf("deep-research-codebase");
|
|
186
|
+
const ralphIdx = cap.stdout.indexOf("ralph");
|
|
187
|
+
expect(deepIdx).toBeGreaterThan(-1);
|
|
188
|
+
expect(ralphIdx).toBeGreaterThan(deepIdx);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("agents within a group are sorted alphabetically", async () => {
|
|
192
|
+
const shuffled = [
|
|
193
|
+
def("opencode", "ralph", "plan"),
|
|
194
|
+
def("claude", "ralph", "plan"),
|
|
195
|
+
def("copilot", "ralph", "plan"),
|
|
196
|
+
];
|
|
197
|
+
const cap = captureOutput();
|
|
198
|
+
try {
|
|
199
|
+
await workflowListCommand({}, { list: () => shuffled });
|
|
200
|
+
} finally {
|
|
201
|
+
cap.restore();
|
|
202
|
+
}
|
|
203
|
+
const claudeIdx = cap.stdout.indexOf("claude");
|
|
204
|
+
const copilotIdx = cap.stdout.indexOf("copilot");
|
|
205
|
+
const opencodeIdx = cap.stdout.indexOf("opencode");
|
|
206
|
+
expect(claudeIdx).toBeGreaterThan(-1);
|
|
207
|
+
expect(claudeIdx).toBeLessThan(copilotIdx);
|
|
208
|
+
expect(copilotIdx).toBeLessThan(opencodeIdx);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("variants with differing descriptions print as separate groups", async () => {
|
|
212
|
+
// Two variants of `ralph` with different descriptions — the grouping
|
|
213
|
+
// should keep them separate so no information is lost, instead of
|
|
214
|
+
// silently collapsing to one arbitrary description.
|
|
215
|
+
const entries = [
|
|
216
|
+
def("claude", "ralph", "plan version A"),
|
|
217
|
+
def("copilot", "ralph", "plan version B"),
|
|
218
|
+
];
|
|
219
|
+
const cap = captureOutput();
|
|
220
|
+
try {
|
|
221
|
+
await workflowListCommand({}, { list: () => entries });
|
|
222
|
+
} finally {
|
|
223
|
+
cap.restore();
|
|
224
|
+
}
|
|
225
|
+
expect(cap.stdout).toContain("plan version A");
|
|
226
|
+
expect(cap.stdout).toContain("plan version B");
|
|
227
|
+
// Two separate groups means "ralph" appears as a heading twice.
|
|
228
|
+
expect(cap.stdout.match(/\bralph\b/g)?.length).toBe(2);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// mock import kept at module scope to match the lint pattern used by
|
|
233
|
+
// other test files — suppresses an unused-import warning if any.
|
|
234
|
+
void mock;
|
|
Binary file
|