feat: add loop, schedule, and chat CLI commands (#149)
* Add metrics collection and terminal performance tests * feat: add loop, schedule, and chat commands with slash-namespaced RPC Introduce three new server-side features with CLI command groups: - `paseo loop` — iterative agent execution with verify-check/verify-prompt - `paseo schedule` — recurring tasks on interval or cron cadence - `paseo chat` — chat rooms for agent-to-agent coordination All features use new slash-namespaced RPC methods (e.g. `loop/run`, `schedule/create`, `chat/post`) instead of flat message types, backed by file-based persistence and wired through the existing WebSocket session dispatch. * test: stabilize loop schedule chat rollout
This commit is contained in:
@@ -31,11 +31,6 @@ type ProviderModel = {
|
||||
};
|
||||
|
||||
const EXPECTED_CLAUDE_MODELS = [
|
||||
{
|
||||
id: "claude-sonnet-4-5-20250929",
|
||||
model: "Sonnet 4.5",
|
||||
descriptionFragment: "Best for everyday tasks",
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-6",
|
||||
model: "Sonnet 4.6",
|
||||
@@ -47,7 +42,7 @@ const EXPECTED_CLAUDE_MODELS = [
|
||||
descriptionFragment: "Most capable",
|
||||
},
|
||||
{
|
||||
id: "claude-haiku-4-5-20251001",
|
||||
id: "claude-haiku-4-5",
|
||||
model: "Haiku 4.5",
|
||||
descriptionFragment: "Fastest",
|
||||
},
|
||||
@@ -207,10 +202,13 @@ try {
|
||||
ids.includes("opencode/gpt-5-nano"),
|
||||
"opencode output should include opencode/gpt-5-nano",
|
||||
);
|
||||
assert(ids.includes("openai/o3-mini"), "opencode output should include openai/o3-mini");
|
||||
assert(
|
||||
ids.includes("openai/gpt-5.3-codex-spark"),
|
||||
"opencode output should include openai/gpt-5.3-codex-spark",
|
||||
ids.some((id) => id.startsWith("openrouter/openai/")),
|
||||
"opencode output should include OpenRouter OpenAI models",
|
||||
);
|
||||
assert(
|
||||
ids.includes("openrouter/openai/gpt-5.3-codex"),
|
||||
"opencode output should include openrouter/openai/gpt-5.3-codex",
|
||||
);
|
||||
console.log("✓ provider models opencode returns namespaced model IDs\n");
|
||||
}
|
||||
@@ -269,8 +267,8 @@ try {
|
||||
"--quiet should print the current Claude catalog IDs",
|
||||
);
|
||||
assert(
|
||||
claudeModelsFromJson.some((m) => m.id === "claude-sonnet-4-5-20250929"),
|
||||
"captured --json output should still include the Claude default model id",
|
||||
claudeModelsFromJson.some((m) => m.id === "claude-sonnet-4-6"),
|
||||
"captured --json output should include the current Claude everyday model id",
|
||||
);
|
||||
console.log("✓ provider models --quiet outputs model IDs only\n");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import assert from "node:assert";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { createE2ETestContext } from "./helpers/test-daemon.ts";
|
||||
|
||||
console.log("=== Chat Command Tests ===\n");
|
||||
|
||||
const ctx = await createE2ETestContext({ timeout: 30000 });
|
||||
|
||||
try {
|
||||
{
|
||||
console.log("Test 1: chat create/ls/inspect work");
|
||||
const created = await ctx.paseo(["chat", "create", "coord-room", "--purpose", "Coordination"]);
|
||||
assert.strictEqual(created.exitCode, 0, created.stderr);
|
||||
assert(created.stdout.includes("coord-room"), created.stdout);
|
||||
|
||||
const listed = await ctx.paseo(["chat", "ls"]);
|
||||
assert.strictEqual(listed.exitCode, 0, listed.stderr);
|
||||
assert(listed.stdout.includes("coord-room"), listed.stdout);
|
||||
|
||||
const inspected = await ctx.paseo(["chat", "inspect", "coord-room"]);
|
||||
assert.strictEqual(inspected.exitCode, 0, inspected.stderr);
|
||||
assert(inspected.stdout.includes("Coordination"), inspected.stdout);
|
||||
console.log("chat create/ls/inspect work\n");
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Test 2: chat post/read/wait work");
|
||||
const posted = await ctx.paseo([
|
||||
"chat",
|
||||
"post",
|
||||
"coord-room",
|
||||
"first message",
|
||||
"--mention",
|
||||
"agent-1",
|
||||
]);
|
||||
assert.strictEqual(posted.exitCode, 0, posted.stderr);
|
||||
assert(posted.stdout.includes("first message"), posted.stdout);
|
||||
|
||||
const read = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10"]);
|
||||
assert.strictEqual(read.exitCode, 0, read.stderr);
|
||||
assert(read.stdout.includes("first message"), read.stdout);
|
||||
|
||||
const waitPromise = ctx.paseo(["chat", "wait", "coord-room", "--timeout", "5s"]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
const secondPost = await ctx.paseo(["chat", "post", "coord-room", "second message"]);
|
||||
assert.strictEqual(secondPost.exitCode, 0, secondPost.stderr);
|
||||
|
||||
const waited = await waitPromise;
|
||||
assert.strictEqual(waited.exitCode, 0, waited.stderr);
|
||||
assert(waited.stdout.includes("second message"), waited.stdout);
|
||||
console.log("chat post/read/wait work\n");
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Test 3: duplicate room create fails");
|
||||
const duplicate = await ctx.paseo(["chat", "create", "coord-room"]);
|
||||
assert.notStrictEqual(duplicate.exitCode, 0, "duplicate create should fail");
|
||||
const combined = `${duplicate.stdout}\n${duplicate.stderr}`;
|
||||
assert(combined.toLowerCase().includes("already exists"), combined);
|
||||
console.log("duplicate room create fails\n");
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Test 4: chat delete works");
|
||||
const deleted = await ctx.paseo(["chat", "delete", "coord-room"]);
|
||||
assert.strictEqual(deleted.exitCode, 0, deleted.stderr);
|
||||
assert(deleted.stdout.includes("coord-room"), deleted.stdout);
|
||||
console.log("chat delete works\n");
|
||||
}
|
||||
} finally {
|
||||
await ctx.stop();
|
||||
await rm(ctx.paseoHome, { recursive: true, force: true });
|
||||
await rm(ctx.workDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log("=== Chat Command Tests Passed ===");
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import assert from "node:assert";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { createE2ETestContext } from "./helpers/test-daemon.ts";
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
console.log("=== Loop And Schedule Command Tests ===\n");
|
||||
|
||||
const ctx = await createE2ETestContext({ timeout: 30000 });
|
||||
|
||||
try {
|
||||
{
|
||||
console.log("Test 1: schedule create/ls/inspect/pause/resume/delete work");
|
||||
const created = await ctx.paseo(
|
||||
["schedule", "create", "Review new PRs", "--every", "5m", "--name", "review-prs", "--json"],
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
assert.strictEqual(created.exitCode, 0, created.stderr);
|
||||
const createdJson = JSON.parse(created.stdout);
|
||||
assert.strictEqual(createdJson.name, "review-prs");
|
||||
assert.strictEqual(createdJson.cadence, "every:5m");
|
||||
|
||||
const listed = await ctx.paseo(["schedule", "ls", "--json"]);
|
||||
assert.strictEqual(listed.exitCode, 0, listed.stderr);
|
||||
const listedJson = JSON.parse(listed.stdout);
|
||||
assert(Array.isArray(listedJson), listed.stdout);
|
||||
assert(listedJson.some((item: { id: string }) => item.id === createdJson.id), listed.stdout);
|
||||
|
||||
const inspected = await ctx.paseo(["schedule", "inspect", createdJson.id, "--json"]);
|
||||
assert.strictEqual(inspected.exitCode, 0, inspected.stderr);
|
||||
const inspectedJson = JSON.parse(inspected.stdout);
|
||||
assert.strictEqual(inspectedJson.status, "active");
|
||||
assert.strictEqual(inspectedJson.prompt, "Review new PRs");
|
||||
|
||||
const paused = await ctx.paseo(["schedule", "pause", createdJson.id, "--json"]);
|
||||
assert.strictEqual(paused.exitCode, 0, paused.stderr);
|
||||
assert.strictEqual(JSON.parse(paused.stdout).status, "paused");
|
||||
|
||||
const resumed = await ctx.paseo(["schedule", "resume", createdJson.id, "--json"]);
|
||||
assert.strictEqual(resumed.exitCode, 0, resumed.stderr);
|
||||
assert.strictEqual(JSON.parse(resumed.stdout).status, "active");
|
||||
|
||||
const deleted = await ctx.paseo(["schedule", "delete", createdJson.id, "--json"]);
|
||||
assert.strictEqual(deleted.exitCode, 0, deleted.stderr);
|
||||
assert.strictEqual(JSON.parse(deleted.stdout).id, createdJson.id);
|
||||
console.log("schedule commands work\n");
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Test 2: loop run/ls/inspect/logs/stop work");
|
||||
const run = await ctx.paseo(
|
||||
["loop", "run", "Return any response", "--name", "smoke-loop", "--verify-check", "true", "--json"],
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
assert.strictEqual(run.exitCode, 0, run.stderr);
|
||||
const runJson = JSON.parse(run.stdout);
|
||||
assert.strictEqual(runJson.name, "smoke-loop");
|
||||
|
||||
const listed = await ctx.paseo(["loop", "ls", "--json"]);
|
||||
assert.strictEqual(listed.exitCode, 0, listed.stderr);
|
||||
const listedJson = JSON.parse(listed.stdout);
|
||||
assert(Array.isArray(listedJson), listed.stdout);
|
||||
assert(listedJson.some((item: { id: string }) => item.id === runJson.id), listed.stdout);
|
||||
|
||||
let status = "running";
|
||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||
const inspect = await ctx.paseo(["loop", "inspect", runJson.id, "--json"]);
|
||||
assert.strictEqual(inspect.exitCode, 0, inspect.stderr);
|
||||
const inspectJson = JSON.parse(inspect.stdout);
|
||||
status = inspectJson.status;
|
||||
if (status !== "running") {
|
||||
assert.strictEqual(status, "succeeded", inspect.stdout);
|
||||
break;
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
assert.strictEqual(status, "succeeded");
|
||||
|
||||
const logs = await ctx.paseo(["loop", "logs", runJson.id], { timeout: 15000 });
|
||||
assert.strictEqual(logs.exitCode, 0, logs.stderr);
|
||||
assert(logs.stdout.includes("verify-check"), logs.stdout);
|
||||
|
||||
const stopped = await ctx.paseo(["loop", "stop", runJson.id, "--json"]);
|
||||
assert.strictEqual(stopped.exitCode, 0, stopped.stderr);
|
||||
const stoppedJson = JSON.parse(stopped.stdout);
|
||||
assert(["succeeded", "stopped"].includes(stoppedJson.status), stopped.stdout);
|
||||
console.log("loop commands work\n");
|
||||
}
|
||||
} finally {
|
||||
await ctx.stop();
|
||||
await rm(ctx.paseoHome, { recursive: true, force: true });
|
||||
await rm(ctx.workDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log("=== Loop And Schedule Command Tests Passed ===");
|
||||
Reference in New Issue
Block a user