From 4d67f0892254acd63b0aed449b0eba8397e25e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A7=81=E5=AE=89?= <73627805+Mar-garet@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:03:46 +0800 Subject: [PATCH] test(cli): verify integer options against a real daemon --- packages/server/package.json | 2 +- .../terminal-workspace-sdk.e2e.test.ts | 63 +++++++++ .../server/schedule-run-lifecycle.e2e.test.ts | 126 ++++++++++++++++++ packages/server/src/server/test-utils/cli.ts | 46 +++++++ 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/server/test-utils/cli.ts diff --git a/packages/server/package.json b/packages/server/package.json index b80941d56f..f332e6ffed 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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", diff --git a/packages/server/src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts b/packages/server/src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts index 19ccdf45e8..82ba1b5cab 100644 --- a/packages/server/src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/terminal-workspace-sdk.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"); diff --git a/packages/server/src/server/schedule-run-lifecycle.e2e.test.ts b/packages/server/src/server/schedule-run-lifecycle.e2e.test.ts index 877ec3aced..c671b42164 100644 --- a/packages/server/src/server/schedule-run-lifecycle.e2e.test.ts +++ b/packages/server/src/server/schedule-run-lifecycle.e2e.test.ts @@ -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; type WorkspaceUpdateMessage = Extract; @@ -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"); diff --git a/packages/server/src/server/test-utils/cli.ts b/packages/server/src/server/test-utils/cli.ts new file mode 100644 index 0000000000..e180519fda --- /dev/null +++ b/packages/server/src/server/test-utils/cli.ts @@ -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; + args: string[]; +} + +interface CliResult { + exitCode: number | null; + stdout: string; + stderr: string; +} + +export function runPaseoCli({ daemon, args }: CliOptions): Promise { + 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 })); + }); +}