mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 03:03:04 +00:00
test(cli): verify integer options against a real daemon
This commit is contained in:
@@ -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 }));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user