test(cli): verify integer options against a real daemon

This commit is contained in:
见安
2026-10-12 06:20:51 +02:00
committed by paseo-bot[bot]
parent 35edd688b5
commit 4d67f08922
4 changed files with 236 additions and 1 deletions
+1 -1
View File
@@ -89,7 +89,7 @@
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
"test": "npm run test:unit && npm run test:integration",
"test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"",
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts && vitest run --maxWorkers=1 src/server/daemon-e2e/git-operations.e2e.test.ts -t \"merges into\" && vitest run --maxWorkers=1 src/server/session.create-agent-worktree-autoarchive.e2e.test.ts -t \"creates a worktree and auto-archives both\" && vitest run --maxWorkers=1 src/server/agent/providers/opencode-bridge.local.e2e.test.ts -t \"versioned runtime\"",
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/schedule-run-lifecycle.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts && vitest run --maxWorkers=1 src/server/daemon-e2e/git-operations.e2e.test.ts -t \"merges into\" && vitest run --maxWorkers=1 src/server/session.create-agent-worktree-autoarchive.e2e.test.ts -t \"creates a worktree and auto-archives both\" && vitest run --maxWorkers=1 src/server/agent/providers/opencode-bridge.local.e2e.test.ts -t \"versioned runtime\"",
"test:hub-cli-contract": "vitest run --maxWorkers=1 src/server/hub/hub-cli-contract.test.ts",
"test:integration:all": "npm run test:e2e",
"test:integration:real": "vitest run real.e2e.test.ts",
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, expect, test } from "vitest";
import { createPaseoClient, type PaseoClient } from "@getpaseo/client";
import { DaemonClient } from "../test-utils/daemon-client.js";
import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { runPaseoCli } from "../test-utils/cli.js";
let daemon: TestPaseoDaemon;
let client: DaemonClient;
@@ -28,6 +29,68 @@ afterEach(async () => {
await rm(cwd, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
});
async function createQuietTerminal() {
const workspaceId = await createWorkspace("CLI capture");
return sdk.terminals.create({
workspaceId,
command: process.execPath,
args: ["-e", "process.stdin.resume()"],
size: { rows: 10, cols: 40 },
});
}
test.each([
["--start", "1.5"],
["--end", "1.5"],
["--start", "1abc"],
["--end", "1abc"],
["--start", ""],
["--end", ""],
["--start", "9007199254740993"],
["--end", "9007199254740993"],
])("CLI terminal capture rejects %s %j", async (flag, value) => {
const terminal = await createQuietTerminal();
try {
const result = await runPaseoCli({
daemon,
args: ["terminal", "capture", terminal.id, flag, value, "--json"],
});
expect(result.exitCode, result.stdout + result.stderr).toBe(1);
expect(JSON.parse(result.stderr)).toEqual({
error: {
code: "INVALID_LINE_NUMBER",
message: `Invalid ${flag} value: ${value}`,
details: "Use an integer line number.",
},
});
} finally {
await terminal.kill();
}
});
test.each([
{ args: [], lineCount: 10 },
{ args: ["--start", "0", "--end", "1"], lineCount: 2 },
{ args: ["--start", "-2", "--end", "-1"], lineCount: 2 },
{ args: ["--scrollback", "--start", "7", "--end", "1"], lineCount: 2 },
])("CLI terminal capture preserves valid ranges $args", async ({ args, lineCount }) => {
const terminal = await createQuietTerminal();
try {
const result = await runPaseoCli({
daemon,
args: ["terminal", "capture", terminal.id, ...args, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
terminalId: terminal.id,
lines: Array.from({ length: lineCount }, () => ""),
totalLines: 10,
});
} finally {
await terminal.kill();
}
});
test("SDK and workspace handles preserve ownership and actual process directories", async () => {
const first = await createWorkspace("main");
const second = await createWorkspace("feature-work");
@@ -13,6 +13,7 @@ import {
} from "./test-utils/index.js";
import type { SessionOutboundMessage } from "./messages.js";
import { getPaseoWorktreesRoot } from "../utils/worktree.js";
import { runPaseoCli } from "./test-utils/cli.js";
type AgentUpdateMessage = Extract<SessionOutboundMessage, { type: "agent_update" }>;
type WorkspaceUpdateMessage = Extract<SessionOutboundMessage, { type: "workspace_update" }>;
@@ -42,6 +43,131 @@ function makeTempDir(prefix: string): string {
return dir;
}
test.each(["2.5", "2runs", "9007199254740993", "0", "-1", ""])(
"CLI schedule create rejects max-runs %j without saving a schedule",
async (maxRuns) => {
const cwd = makeTempDir("schedule-cli-integer-");
const result = await runPaseoCli({
daemon: ctx.daemon,
args: [
"schedule",
"create",
"Integer limit",
"--cron",
"0 0 1 1 *",
"--provider",
"claude",
"--cwd",
cwd,
"--max-runs",
maxRuns,
"--json",
],
});
expect(result.exitCode, result.stdout + result.stderr).toBe(1);
expect(JSON.parse(result.stderr)).toEqual({
error: { code: "INVALID_INTEGER", message: "--max-runs must be a positive integer" },
});
expect((await ctx.client.scheduleList()).schedules).toEqual([]);
},
);
test.each(["2.5", "2runs"])(
"CLI schedule update rejects max-runs %j without changing the schedule",
async (maxRuns) => {
const cwd = makeTempDir("schedule-cli-update-integer-");
const schedule = await createNewAgentSchedule({
prompt: "Original prompt",
cadence: { type: "cron", expression: "0 0 1 1 *", timezone: "UTC" },
target: { type: "new-agent", config: { provider: "claude", cwd } },
maxRuns: 3,
runOnCreate: false,
});
const before = await ctx.client.scheduleInspect({ id: schedule.id });
const result = await runPaseoCli({
daemon: ctx.daemon,
args: [
"schedule",
"update",
schedule.id,
"--prompt",
"Should not apply",
"--max-runs",
maxRuns,
"--json",
],
});
expect(result.exitCode, result.stdout + result.stderr).toBe(1);
expect(JSON.parse(result.stderr)).toEqual({
error: { code: "INVALID_INTEGER", message: "--max-runs must be a positive integer" },
});
const after = await ctx.client.scheduleInspect({ id: schedule.id });
expect(after.error).toBeNull();
expect(after.schedule).toEqual(before.schedule);
},
);
test.each([
{ limitArgs: [], maxRuns: null },
{ limitArgs: ["--max-runs", "3"], maxRuns: 3 },
{ limitArgs: ["--max-runs", "3.0"], maxRuns: 3 },
{ limitArgs: ["--max-runs", "1e2"], maxRuns: 100 },
])(
"CLI schedule create stores max-runs $maxRuns from $limitArgs",
async ({ limitArgs, maxRuns }) => {
const cwd = makeTempDir("schedule-cli-valid-integer-");
const result = await runPaseoCli({
daemon: ctx.daemon,
args: [
"schedule",
"create",
"Integer limit",
"--cron",
"0 0 1 1 *",
"--provider",
"claude",
"--cwd",
cwd,
...limitArgs,
"--json",
],
});
expect(result.exitCode, result.stderr).toBe(0);
expect((await ctx.client.scheduleList()).schedules).toEqual([
expect.objectContaining({ maxRuns, prompt: "Integer limit" }),
]);
},
);
test("CLI schedule update preserves, changes, and clears valid run limits", async () => {
const cwd = makeTempDir("schedule-cli-valid-update-");
const schedule = await createNewAgentSchedule({
prompt: "Original prompt",
cadence: { type: "cron", expression: "0 0 1 1 *", timezone: "UTC" },
target: { type: "new-agent", config: { provider: "claude", cwd } },
maxRuns: 3,
runOnCreate: false,
});
for (const { args, maxRuns } of [
{ args: ["--name", "Renamed"], maxRuns: 3 },
{ args: ["--max-runs", "5"], maxRuns: 5 },
{ args: ["--no-max-runs"], maxRuns: null },
]) {
const result = await runPaseoCli({
daemon: ctx.daemon,
args: ["schedule", "update", schedule.id, ...args, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
expect((await ctx.client.scheduleInspect({ id: schedule.id })).schedule).toMatchObject({
maxRuns,
name: "Renamed",
prompt: schedule.prompt,
target: schedule.target,
cadence: schedule.cadence,
});
}
}, 60_000);
function createGitRepo(): string {
const tempRoot = makeTempDir("schedule-run-worktree-");
const repoDir = path.join(tempRoot, "repo");
@@ -0,0 +1,46 @@
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import type { TestPaseoDaemon } from "./paseo-daemon.js";
interface CliOptions {
daemon: Pick<TestPaseoDaemon, "port" | "paseoHome">;
args: string[];
}
interface CliResult {
exitCode: number | null;
stdout: string;
stderr: string;
}
export function runPaseoCli({ daemon, args }: CliOptions): Promise<CliResult> {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
"--no-warnings",
"--conditions=source",
"--import",
"tsx",
fileURLToPath(new URL("../../../../cli/src/index.ts", import.meta.url)),
"--host",
`127.0.0.1:${daemon.port}`,
...args,
],
{
env: { ...process.env, PASEO_HOME: daemon.paseoHome },
stdio: ["ignore", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
child.once("error", reject);
child.once("close", (exitCode) => resolve({ exitCode, stdout, stderr }));
});
}