Merge pull request 'feat: add --count/-c flag to uwf thread step' (#390) from feat/373-thread-step-count into main
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
const CLI_PATH = join(import.meta.dirname, "..", "cli.js");
|
||||||
|
|
||||||
|
function runCli(args: string[]): { stdout: string; stderr: string; exitCode: number } {
|
||||||
|
try {
|
||||||
|
const stdout = execFileSync("bun", ["run", CLI_PATH, ...args], {
|
||||||
|
encoding: "utf8",
|
||||||
|
env: { ...process.env, WORKFLOW_STORAGE_ROOT: "/tmp/uwf-test-nonexistent" },
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
return { stdout, stderr: "", exitCode: 0 };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const err = e as NodeJS.ErrnoException & { stdout?: string; stderr?: string; status?: number };
|
||||||
|
return {
|
||||||
|
stdout: err.stdout ?? "",
|
||||||
|
stderr: err.stderr ?? "",
|
||||||
|
exitCode: err.status ?? 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("thread step --count CLI parsing", () => {
|
||||||
|
test("--help shows -c/--count option", () => {
|
||||||
|
const result = runCli(["thread", "step", "--help"]);
|
||||||
|
expect(result.stdout).toContain("--count");
|
||||||
|
expect(result.stdout).toContain("-c");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("description says 'one or more steps'", () => {
|
||||||
|
const result = runCli(["thread", "step", "--help"]);
|
||||||
|
expect(result.stdout).toContain("one or more steps");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdThreadStep count logic", () => {
|
||||||
|
test("count=0 fails with validation error", () => {
|
||||||
|
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "0"]);
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
expect(result.stderr).toContain("positive integer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("negative count fails with validation error", () => {
|
||||||
|
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "-1"]);
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
expect(result.stderr).toContain("positive integer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-integer count fails with validation error", () => {
|
||||||
|
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "1.5"]);
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
expect(result.stderr).toContain("positive integer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("count=1 is the default (no -c flag)", () => {
|
||||||
|
// Without -c, it should attempt to run 1 step (failing on missing thread, not on count validation)
|
||||||
|
const result = runCli(["thread", "step", "FAKE_THREAD_ID"]);
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
// Should NOT contain "positive integer" error — should fail on thread lookup instead
|
||||||
|
expect(result.stderr).not.toContain("positive integer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("count=3 passes validation (fails on thread lookup)", () => {
|
||||||
|
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "3"]);
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
// Should NOT contain "positive integer" error — should fail on thread/storage lookup
|
||||||
|
expect(result.stderr).not.toContain("positive integer");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -109,15 +109,21 @@ thread
|
|||||||
|
|
||||||
thread
|
thread
|
||||||
.command("step")
|
.command("step")
|
||||||
.description("Execute one step")
|
.description("Execute one or more steps")
|
||||||
.argument("<thread-id>", "Thread ULID")
|
.argument("<thread-id>", "Thread ULID")
|
||||||
.option("--agent <cmd>", "Override agent command")
|
.option("--agent <cmd>", "Override agent command")
|
||||||
.action((threadId: string, opts: { agent: string | undefined }) => {
|
.option("-c, --count <number>", "Number of steps to run (default: 1)")
|
||||||
|
.action((threadId: string, opts: { agent: string | undefined; count: string | undefined }) => {
|
||||||
const storageRoot = resolveStorageRoot();
|
const storageRoot = resolveStorageRoot();
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
const agentOverride = opts.agent ?? null;
|
const agentOverride = opts.agent ?? null;
|
||||||
const result = await cmdThreadStep(storageRoot, threadId, agentOverride);
|
const count = opts.count !== undefined ? Number(opts.count) : 1;
|
||||||
writeOutput(result);
|
const results = await cmdThreadStep(storageRoot, threadId, agentOverride, count);
|
||||||
|
if (results.length === 1) {
|
||||||
|
writeOutput(results[0]);
|
||||||
|
} else {
|
||||||
|
writeOutput(results);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -673,6 +673,27 @@ export async function cmdThreadStep(
|
|||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
threadId: ThreadId,
|
threadId: ThreadId,
|
||||||
agentOverride: string | null,
|
agentOverride: string | null,
|
||||||
|
count: number,
|
||||||
|
): Promise<StepOutput[]> {
|
||||||
|
if (count < 1 || !Number.isInteger(count)) {
|
||||||
|
fail(`--count must be a positive integer, got: ${count}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: StepOutput[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const result = await cmdThreadStepOnce(storageRoot, threadId, agentOverride);
|
||||||
|
results.push(result);
|
||||||
|
if (result.done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdThreadStepOnce(
|
||||||
|
storageRoot: string,
|
||||||
|
threadId: ThreadId,
|
||||||
|
agentOverride: string | null,
|
||||||
): Promise<StepOutput> {
|
): Promise<StepOutput> {
|
||||||
const index = await loadThreadsIndex(storageRoot);
|
const index = await loadThreadsIndex(storageRoot);
|
||||||
const headHash = index[threadId];
|
const headHash = index[threadId];
|
||||||
|
|||||||
Reference in New Issue
Block a user