Switch to double quotes and reformat codebase with Biome

This commit is contained in:
Mohamed Boudra
2026-03-21 20:22:15 +07:00
parent ab40786882
commit f6579dbeb4
805 changed files with 38132 additions and 40175 deletions
+26 -26
View File
@@ -8,46 +8,46 @@
* - paseo --help shows commands
*/
import { $ } from 'zx'
import { $ } from "zx";
$.verbose = false
$.verbose = false;
console.log('📋 Phase 1: Foundation Tests\n')
console.log("📋 Phase 1: Foundation Tests\n");
// Test 1.1: --version outputs version
console.log(' Testing paseo --version...')
const versionResult = await $`paseo --version`.nothrow()
console.log(" Testing paseo --version...");
const versionResult = await $`paseo --version`.nothrow();
if (versionResult.exitCode !== 0) {
console.error(' ❌ paseo --version failed with exit code', versionResult.exitCode)
console.error(' stderr:', versionResult.stderr)
process.exit(1)
console.error(" ❌ paseo --version failed with exit code", versionResult.exitCode);
console.error(" stderr:", versionResult.stderr);
process.exit(1);
}
const versionOutput = versionResult.stdout.trim()
const versionOutput = versionResult.stdout.trim();
if (!versionOutput.match(/\d+\.\d+\.\d+/)) {
console.error(' ❌ paseo --version output does not contain version number')
console.error(' output:', versionOutput)
process.exit(1)
console.error(" ❌ paseo --version output does not contain version number");
console.error(" output:", versionOutput);
process.exit(1);
}
console.log(' ✅ paseo --version outputs:', versionOutput)
console.log(" ✅ paseo --version outputs:", versionOutput);
// Test 1.2: --help shows commands
console.log(' Testing paseo --help...')
const helpResult = await $`paseo --help`.nothrow()
console.log(" Testing paseo --help...");
const helpResult = await $`paseo --help`.nothrow();
if (helpResult.exitCode !== 0) {
console.error(' ❌ paseo --help failed with exit code', helpResult.exitCode)
console.error(' stderr:', helpResult.stderr)
process.exit(1)
console.error(" ❌ paseo --help failed with exit code", helpResult.exitCode);
console.error(" stderr:", helpResult.stderr);
process.exit(1);
}
const helpOutput = helpResult.stdout
const helpOutput = helpResult.stdout;
// Check for expected sections in help output
const expectedTerms = ['agent', 'daemon', 'Usage', 'Options', 'Commands']
const missingTerms = expectedTerms.filter(term => !helpOutput.includes(term))
const expectedTerms = ["agent", "daemon", "Usage", "Options", "Commands"];
const missingTerms = expectedTerms.filter((term) => !helpOutput.includes(term));
if (missingTerms.length > 0) {
console.error(' ❌ paseo --help missing expected terms:', missingTerms.join(', '))
console.error(' output:', helpOutput)
process.exit(1)
console.error(" ❌ paseo --help missing expected terms:", missingTerms.join(", "));
console.error(" output:", helpOutput);
process.exit(1);
}
console.log(' ✅ paseo --help shows commands')
console.log(" ✅ paseo --help shows commands");
console.log('\n✅ Phase 1: Foundation Tests PASSED')
console.log("\n✅ Phase 1: Foundation Tests PASSED");
+172 -172
View File
@@ -5,7 +5,7 @@
* Verifies that renderers correctly format structured data.
*/
import assert from 'node:assert'
import assert from "node:assert";
import {
render,
renderTable,
@@ -19,261 +19,261 @@ import {
type SingleResult,
type OutputSchema,
type CommandError,
} from '../src/output/index.js'
} from "../src/output/index.js";
// Test data types
interface Agent {
id: string
title: string
status: 'running' | 'idle' | 'error'
provider: string
id: string;
title: string;
status: "running" | "idle" | "error";
provider: string;
}
// Schema for agents
const agentSchema: OutputSchema<Agent> = {
idField: 'id',
idField: "id",
columns: [
{ header: 'ID', field: 'id', width: 8 },
{ header: 'TITLE', field: 'title', width: 20 },
{ header: "ID", field: "id", width: 8 },
{ header: "TITLE", field: "title", width: 20 },
{
header: 'STATUS',
field: 'status',
header: "STATUS",
field: "status",
color: (value) => {
switch (value) {
case 'running':
return 'green'
case 'idle':
return 'dim'
case 'error':
return 'red'
case "running":
return "green";
case "idle":
return "dim";
case "error":
return "red";
default:
return undefined
return undefined;
}
},
},
{ header: 'PROVIDER', field: 'provider' },
{ header: "PROVIDER", field: "provider" },
],
}
};
// Test data
const testAgents: Agent[] = [
{ id: 'abc123', title: 'Feature Implementation', status: 'running', provider: 'claude' },
{ id: 'def456', title: 'Bug Fix', status: 'idle', provider: 'codex' },
{ id: 'ghi789', title: 'Failed Task', status: 'error', provider: 'claude' },
]
{ id: "abc123", title: "Feature Implementation", status: "running", provider: "claude" },
{ id: "def456", title: "Bug Fix", status: "idle", provider: "codex" },
{ id: "ghi789", title: "Failed Task", status: "error", provider: "claude" },
];
const listResult: ListResult<Agent> = {
type: 'list',
type: "list",
data: testAgents,
schema: agentSchema,
}
};
const singleResult: SingleResult<Agent> = {
type: 'single',
type: "single",
data: testAgents[0],
schema: agentSchema,
}
};
// Test utilities
let passed = 0
let failed = 0
let passed = 0;
let failed = 0;
function test(name: string, fn: () => void): void {
try {
fn()
console.log(`✓ ${name}`)
passed++
fn();
console.log(`✓ ${name}`);
passed++;
} catch (error) {
console.error(`✗ ${name}`)
console.error(` ${error instanceof Error ? error.message : error}`)
failed++
console.error(`✗ ${name}`);
console.error(` ${error instanceof Error ? error.message : error}`);
failed++;
}
}
// Table renderer tests
console.log('\n=== Table Renderer ===\n')
console.log("\n=== Table Renderer ===\n");
test('renderTable formats list data with headers', () => {
const output = renderTable(listResult, createOutputOptions({ noColor: true }))
assert.ok(output.includes('ID'), 'Should include ID header')
assert.ok(output.includes('TITLE'), 'Should include TITLE header')
assert.ok(output.includes('STATUS'), 'Should include STATUS header')
assert.ok(output.includes('abc123'), 'Should include first agent ID')
assert.ok(output.includes('Feature Implementation'), 'Should include first agent title')
})
test("renderTable formats list data with headers", () => {
const output = renderTable(listResult, createOutputOptions({ noColor: true }));
assert.ok(output.includes("ID"), "Should include ID header");
assert.ok(output.includes("TITLE"), "Should include TITLE header");
assert.ok(output.includes("STATUS"), "Should include STATUS header");
assert.ok(output.includes("abc123"), "Should include first agent ID");
assert.ok(output.includes("Feature Implementation"), "Should include first agent title");
});
test('renderTable omits headers when noHeaders is true', () => {
const output = renderTable(listResult, createOutputOptions({ noHeaders: true, noColor: true }))
assert.ok(!output.includes('ID '), 'Should not include header row')
assert.ok(output.includes('abc123'), 'Should still include data')
})
test("renderTable omits headers when noHeaders is true", () => {
const output = renderTable(listResult, createOutputOptions({ noHeaders: true, noColor: true }));
assert.ok(!output.includes("ID "), "Should not include header row");
assert.ok(output.includes("abc123"), "Should still include data");
});
test('renderTable handles empty data', () => {
test("renderTable handles empty data", () => {
const emptyResult: ListResult<Agent> = {
type: 'list',
type: "list",
data: [],
schema: agentSchema,
}
const output = renderTable(emptyResult, createOutputOptions())
assert.strictEqual(output, '', 'Should return empty string for empty data')
})
};
const output = renderTable(emptyResult, createOutputOptions());
assert.strictEqual(output, "", "Should return empty string for empty data");
});
test('renderTable handles single result', () => {
const output = renderTable(singleResult, createOutputOptions({ noColor: true }))
assert.ok(output.includes('abc123'), 'Should include agent ID')
assert.ok(output.includes('Feature Implementation'), 'Should include agent title')
})
test("renderTable handles single result", () => {
const output = renderTable(singleResult, createOutputOptions({ noColor: true }));
assert.ok(output.includes("abc123"), "Should include agent ID");
assert.ok(output.includes("Feature Implementation"), "Should include agent title");
});
// JSON renderer tests
console.log('\n=== JSON Renderer ===\n')
console.log("\n=== JSON Renderer ===\n");
test('renderJson outputs valid JSON for list', () => {
const output = renderJson(listResult, createOutputOptions())
const parsed = JSON.parse(output)
assert.ok(Array.isArray(parsed), 'Should be an array')
assert.strictEqual(parsed.length, 3, 'Should have 3 items')
assert.strictEqual(parsed[0].id, 'abc123', 'Should include correct data')
})
test("renderJson outputs valid JSON for list", () => {
const output = renderJson(listResult, createOutputOptions());
const parsed = JSON.parse(output);
assert.ok(Array.isArray(parsed), "Should be an array");
assert.strictEqual(parsed.length, 3, "Should have 3 items");
assert.strictEqual(parsed[0].id, "abc123", "Should include correct data");
});
test('renderJson outputs valid JSON for single item', () => {
const output = renderJson(singleResult, createOutputOptions())
const parsed = JSON.parse(output)
assert.strictEqual(parsed.id, 'abc123', 'Should include correct data')
assert.strictEqual(parsed.title, 'Feature Implementation', 'Should include correct title')
})
test("renderJson outputs valid JSON for single item", () => {
const output = renderJson(singleResult, createOutputOptions());
const parsed = JSON.parse(output);
assert.strictEqual(parsed.id, "abc123", "Should include correct data");
assert.strictEqual(parsed.title, "Feature Implementation", "Should include correct title");
});
test('renderJson uses custom serializer when provided', () => {
test("renderJson uses custom serializer when provided", () => {
const customSchema: OutputSchema<Agent> = {
...agentSchema,
serialize: (agent) => ({ agentId: agent.id, name: agent.title }),
}
};
const customResult: SingleResult<Agent> = {
type: 'single',
type: "single",
data: testAgents[0],
schema: customSchema,
}
const output = renderJson(customResult, createOutputOptions())
const parsed = JSON.parse(output)
assert.ok('agentId' in parsed, 'Should use custom serialization')
assert.ok('name' in parsed, 'Should use custom field names')
})
};
const output = renderJson(customResult, createOutputOptions());
const parsed = JSON.parse(output);
assert.ok("agentId" in parsed, "Should use custom serialization");
assert.ok("name" in parsed, "Should use custom field names");
});
// YAML renderer tests
console.log('\n=== YAML Renderer ===\n')
console.log("\n=== YAML Renderer ===\n");
test('renderYaml outputs valid YAML for list', () => {
const output = renderYaml(listResult, createOutputOptions())
assert.ok(output.includes('- id: abc123'), 'Should format as YAML list')
assert.ok(output.includes('title: Feature Implementation'), 'Should include title')
})
test("renderYaml outputs valid YAML for list", () => {
const output = renderYaml(listResult, createOutputOptions());
assert.ok(output.includes("- id: abc123"), "Should format as YAML list");
assert.ok(output.includes("title: Feature Implementation"), "Should include title");
});
test('renderYaml outputs valid YAML for single item', () => {
const output = renderYaml(singleResult, createOutputOptions())
assert.ok(output.includes('id: abc123'), 'Should include ID')
assert.ok(!output.startsWith('-'), 'Should not be a list')
})
test("renderYaml outputs valid YAML for single item", () => {
const output = renderYaml(singleResult, createOutputOptions());
assert.ok(output.includes("id: abc123"), "Should include ID");
assert.ok(!output.startsWith("-"), "Should not be a list");
});
// Quiet renderer tests
console.log('\n=== Quiet Renderer ===\n')
console.log("\n=== Quiet Renderer ===\n");
test('renderQuiet outputs only IDs for list', () => {
const output = renderQuiet(listResult, createOutputOptions())
const lines = output.split('\n')
assert.strictEqual(lines.length, 3, 'Should have 3 lines')
assert.strictEqual(lines[0], 'abc123', 'First line should be first ID')
assert.strictEqual(lines[1], 'def456', 'Second line should be second ID')
})
test("renderQuiet outputs only IDs for list", () => {
const output = renderQuiet(listResult, createOutputOptions());
const lines = output.split("\n");
assert.strictEqual(lines.length, 3, "Should have 3 lines");
assert.strictEqual(lines[0], "abc123", "First line should be first ID");
assert.strictEqual(lines[1], "def456", "Second line should be second ID");
});
test('renderQuiet outputs only ID for single item', () => {
const output = renderQuiet(singleResult, createOutputOptions())
assert.strictEqual(output, 'abc123', 'Should output only the ID')
})
test("renderQuiet outputs only ID for single item", () => {
const output = renderQuiet(singleResult, createOutputOptions());
assert.strictEqual(output, "abc123", "Should output only the ID");
});
test('renderQuiet supports function idField', () => {
test("renderQuiet supports function idField", () => {
const customSchema: OutputSchema<Agent> = {
...agentSchema,
idField: (agent) => `${agent.provider}/${agent.id}`,
}
};
const customResult: SingleResult<Agent> = {
type: 'single',
type: "single",
data: testAgents[0],
schema: customSchema,
}
const output = renderQuiet(customResult, createOutputOptions())
assert.strictEqual(output, 'claude/abc123', 'Should use function to extract ID')
})
};
const output = renderQuiet(customResult, createOutputOptions());
assert.strictEqual(output, "claude/abc123", "Should use function to extract ID");
});
// Main render dispatcher tests
console.log('\n=== Render Dispatcher ===\n')
console.log("\n=== Render Dispatcher ===\n");
test('render uses table format by default', () => {
const output = render(listResult, { noColor: true })
assert.ok(output.includes('ID'), 'Should use table format with headers')
})
test("render uses table format by default", () => {
const output = render(listResult, { noColor: true });
assert.ok(output.includes("ID"), "Should use table format with headers");
});
test('render uses json format when specified', () => {
const output = render(listResult, { format: 'json' })
const parsed = JSON.parse(output)
assert.ok(Array.isArray(parsed), 'Should be valid JSON array')
})
test("render uses json format when specified", () => {
const output = render(listResult, { format: "json" });
const parsed = JSON.parse(output);
assert.ok(Array.isArray(parsed), "Should be valid JSON array");
});
test('render uses yaml format when specified', () => {
const output = render(listResult, { format: 'yaml' })
assert.ok(output.includes('- id:'), 'Should be valid YAML')
})
test("render uses yaml format when specified", () => {
const output = render(listResult, { format: "yaml" });
assert.ok(output.includes("- id:"), "Should be valid YAML");
});
test('render uses quiet mode when quiet is true', () => {
const output = render(listResult, { quiet: true })
assert.strictEqual(output, 'abc123\ndef456\nghi789', 'Should output only IDs')
})
test("render uses quiet mode when quiet is true", () => {
const output = render(listResult, { quiet: true });
assert.strictEqual(output, "abc123\ndef456\nghi789", "Should output only IDs");
});
test('quiet mode takes precedence over format', () => {
const output = render(listResult, { format: 'json', quiet: true })
assert.strictEqual(output, 'abc123\ndef456\nghi789', 'Quiet should override format')
})
test("quiet mode takes precedence over format", () => {
const output = render(listResult, { format: "json", quiet: true });
assert.strictEqual(output, "abc123\ndef456\nghi789", "Quiet should override format");
});
// Error rendering tests
console.log('\n=== Error Rendering ===\n')
console.log("\n=== Error Rendering ===\n");
test('renderError formats error for table format', () => {
const error: CommandError = { code: 'NOT_FOUND', message: 'Agent not found' }
const output = renderError(error, { noColor: true })
assert.ok(output.includes('Error:'), 'Should include Error prefix')
assert.ok(output.includes('Agent not found'), 'Should include message')
})
test("renderError formats error for table format", () => {
const error: CommandError = { code: "NOT_FOUND", message: "Agent not found" };
const output = renderError(error, { noColor: true });
assert.ok(output.includes("Error:"), "Should include Error prefix");
assert.ok(output.includes("Agent not found"), "Should include message");
});
test('renderError formats error as JSON', () => {
const error: CommandError = { code: 'NOT_FOUND', message: 'Agent not found' }
const output = renderError(error, { format: 'json' })
const parsed = JSON.parse(output)
assert.ok('error' in parsed, 'Should have error property')
assert.strictEqual(parsed.error.code, 'NOT_FOUND', 'Should include error code')
})
test("renderError formats error as JSON", () => {
const error: CommandError = { code: "NOT_FOUND", message: "Agent not found" };
const output = renderError(error, { format: "json" });
const parsed = JSON.parse(output);
assert.ok("error" in parsed, "Should have error property");
assert.strictEqual(parsed.error.code, "NOT_FOUND", "Should include error code");
});
test('renderError formats error as YAML', () => {
const error: CommandError = { code: 'NOT_FOUND', message: 'Agent not found' }
const output = renderError(error, { format: 'yaml' })
assert.ok(output.includes('error:'), 'Should have error key')
assert.ok(output.includes('code: NOT_FOUND'), 'Should include error code')
})
test("renderError formats error as YAML", () => {
const error: CommandError = { code: "NOT_FOUND", message: "Agent not found" };
const output = renderError(error, { format: "yaml" });
assert.ok(output.includes("error:"), "Should have error key");
assert.ok(output.includes("code: NOT_FOUND"), "Should include error code");
});
test('toCommandError converts Error to CommandError', () => {
const error = new Error('Something went wrong')
const commandError = toCommandError(error)
assert.strictEqual(commandError.code, 'UNKNOWN_ERROR', 'Should use UNKNOWN_ERROR code')
assert.strictEqual(commandError.message, 'Something went wrong', 'Should preserve message')
})
test("toCommandError converts Error to CommandError", () => {
const error = new Error("Something went wrong");
const commandError = toCommandError(error);
assert.strictEqual(commandError.code, "UNKNOWN_ERROR", "Should use UNKNOWN_ERROR code");
assert.strictEqual(commandError.message, "Something went wrong", "Should preserve message");
});
test('toCommandError passes through CommandError', () => {
const error: CommandError = { code: 'CUSTOM', message: 'Custom error' }
const commandError = toCommandError(error)
assert.strictEqual(commandError.code, 'CUSTOM', 'Should preserve code')
assert.strictEqual(commandError.message, 'Custom error', 'Should preserve message')
})
test("toCommandError passes through CommandError", () => {
const error: CommandError = { code: "CUSTOM", message: "Custom error" };
const commandError = toCommandError(error);
assert.strictEqual(commandError.code, "CUSTOM", "Should preserve code");
assert.strictEqual(commandError.message, "Custom error", "Should preserve message");
});
// Summary
console.log('\n=== Summary ===\n')
console.log(`Passed: ${passed}`)
console.log(`Failed: ${failed}`)
console.log("\n=== Summary ===\n");
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0)
process.exit(failed > 0 ? 1 : 0);
+70 -66
View File
@@ -14,112 +14,116 @@
* - daemon restart starts the daemon and can be cleaned up
*/
import assert from 'node:assert'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { runLocalPaseo } from './helpers/local-cli.ts'
import assert from "node:assert";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { runLocalPaseo } from "./helpers/local-cli.ts";
console.log('=== Daemon Commands ===\n')
console.log("=== Daemon Commands ===\n");
// Keep restart off default 6767 to avoid collisions with any existing daemon.
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
function daemonCommand(args: string[]) {
return runLocalPaseo(['daemon', ...args], { PASEO_HOME: paseoHome })
return runLocalPaseo(["daemon", ...args], { PASEO_HOME: paseoHome });
}
try {
// Test 1: daemon --help shows subcommands
{
console.log('Test 1: daemon --help shows subcommands')
const result = await runLocalPaseo(['daemon', '--help'])
assert.strictEqual(result.exitCode, 0, 'daemon --help should exit 0')
assert(result.stdout.includes('start'), 'help should mention start')
assert(result.stdout.includes('status'), 'help should mention status')
assert(result.stdout.includes('stop'), 'help should mention stop')
assert(result.stdout.includes('restart'), 'help should mention restart')
assert(result.stdout.includes('pair'), 'help should mention pair')
console.log('✓ daemon --help shows subcommands\n')
console.log("Test 1: daemon --help shows subcommands");
const result = await runLocalPaseo(["daemon", "--help"]);
assert.strictEqual(result.exitCode, 0, "daemon --help should exit 0");
assert(result.stdout.includes("start"), "help should mention start");
assert(result.stdout.includes("status"), "help should mention status");
assert(result.stdout.includes("stop"), "help should mention stop");
assert(result.stdout.includes("restart"), "help should mention restart");
assert(result.stdout.includes("pair"), "help should mention pair");
console.log("✓ daemon --help shows subcommands\n");
}
// Test 2: daemon pair works without daemon process
{
console.log('Test 2: daemon pair prints local pairing URL')
const result = await daemonCommand(['pair'])
assert.strictEqual(result.exitCode, 0, 'daemon pair should succeed')
assert(result.stdout.includes('Scan to pair:'), 'output should include scan header')
assert(result.stdout.includes('#offer='), 'output should include pairing offer fragment')
console.log('✓ daemon pair prints local pairing URL\n')
console.log("Test 2: daemon pair prints local pairing URL");
const result = await daemonCommand(["pair"]);
assert.strictEqual(result.exitCode, 0, "daemon pair should succeed");
assert(result.stdout.includes("Scan to pair:"), "output should include scan header");
assert(result.stdout.includes("#offer="), "output should include pairing offer fragment");
console.log("✓ daemon pair prints local pairing URL\n");
}
// Test 3: daemon status reports stopped when daemon not running
{
console.log('Test 3: daemon status reports stopped when not running')
const result = await daemonCommand(['status'])
assert.strictEqual(result.exitCode, 0, 'status should succeed when daemon is stopped')
const output = result.stdout.toLowerCase()
assert(output.includes('status'), 'status table should include Status row')
assert(output.includes('stopped'), 'status should report stopped')
console.log('✓ daemon status reports stopped when not running\n')
console.log("Test 3: daemon status reports stopped when not running");
const result = await daemonCommand(["status"]);
assert.strictEqual(result.exitCode, 0, "status should succeed when daemon is stopped");
const output = result.stdout.toLowerCase();
assert(output.includes("status"), "status table should include Status row");
assert(output.includes("stopped"), "status should report stopped");
console.log("✓ daemon status reports stopped when not running\n");
}
// Test 4: daemon pair --json outputs valid JSON
{
console.log('Test 4: daemon pair --json outputs JSON')
const result = await daemonCommand(['pair', '--json'])
assert.strictEqual(result.exitCode, 0, 'daemon pair --json should succeed')
const pairing = JSON.parse(result.stdout)
assert.strictEqual(pairing.relayEnabled, true, 'pairing should report relay enabled')
assert.match(pairing.url, /#offer=/, 'pairing URL should include offer fragment')
assert.strictEqual(typeof pairing.qr, 'string', 'pairing should include QR content')
console.log('✓ daemon pair --json outputs valid JSON\n')
console.log("Test 4: daemon pair --json outputs JSON");
const result = await daemonCommand(["pair", "--json"]);
assert.strictEqual(result.exitCode, 0, "daemon pair --json should succeed");
const pairing = JSON.parse(result.stdout);
assert.strictEqual(pairing.relayEnabled, true, "pairing should report relay enabled");
assert.match(pairing.url, /#offer=/, "pairing URL should include offer fragment");
assert.strictEqual(typeof pairing.qr, "string", "pairing should include QR content");
console.log("✓ daemon pair --json outputs valid JSON\n");
}
// Test 5: daemon status --json outputs valid JSON
{
console.log('Test 5: daemon status --json outputs JSON')
const result = await daemonCommand(['status', '--json'])
assert.strictEqual(result.exitCode, 0, '--json status should succeed')
const status = JSON.parse(result.stdout)
assert.strictEqual(typeof status.serverId, 'string', 'json status should include serverId')
assert.strictEqual(status.status, 'stopped', 'json status should report stopped')
assert.strictEqual(status.home, paseoHome, 'json status should reflect the isolated home')
assert.strictEqual(status.hostname, null, 'json status should include hostname when unavailable')
console.log('✓ daemon status --json outputs valid JSON\n')
console.log("Test 5: daemon status --json outputs JSON");
const result = await daemonCommand(["status", "--json"]);
assert.strictEqual(result.exitCode, 0, "--json status should succeed");
const status = JSON.parse(result.stdout);
assert.strictEqual(typeof status.serverId, "string", "json status should include serverId");
assert.strictEqual(status.status, "stopped", "json status should report stopped");
assert.strictEqual(status.home, paseoHome, "json status should reflect the isolated home");
assert.strictEqual(
status.hostname,
null,
"json status should include hostname when unavailable",
);
console.log("✓ daemon status --json outputs valid JSON\n");
}
// Test 6: daemon stop handles daemon not running gracefully
{
console.log('Test 6: daemon stop handles daemon not running')
const result = await daemonCommand(['stop'])
console.log("Test 6: daemon stop handles daemon not running");
const result = await daemonCommand(["stop"]);
// Stop should succeed even if daemon is not running (idempotent).
assert.strictEqual(result.exitCode, 0, 'stop should succeed when daemon not running')
const output = result.stdout + result.stderr
assert.strictEqual(result.exitCode, 0, "stop should succeed when daemon not running");
const output = result.stdout + result.stderr;
const mentionsNotRunning =
output.toLowerCase().includes('not running') ||
output.toLowerCase().includes('was not running')
assert(mentionsNotRunning, 'output should mention daemon was not running')
console.log('✓ daemon stop succeeds gracefully when daemon not running\n')
output.toLowerCase().includes("not running") ||
output.toLowerCase().includes("was not running");
assert(mentionsNotRunning, "output should mention daemon was not running");
console.log("✓ daemon stop succeeds gracefully when daemon not running\n");
}
// Test 7: daemon restart starts daemon and can be stopped
{
console.log('Test 7: daemon restart starts daemon and can be stopped')
const result = await daemonCommand(['restart', '--port', String(port)])
assert.strictEqual(result.exitCode, 0, 'restart should succeed even when previously stopped')
assert(result.stdout.toLowerCase().includes('restarted'), 'output should report restart')
console.log("Test 7: daemon restart starts daemon and can be stopped");
const result = await daemonCommand(["restart", "--port", String(port)]);
assert.strictEqual(result.exitCode, 0, "restart should succeed even when previously stopped");
assert(result.stdout.toLowerCase().includes("restarted"), "output should report restart");
const cleanup = await daemonCommand(['stop', '--force'])
assert.strictEqual(cleanup.exitCode, 0, 'cleanup stop should succeed after restart')
console.log('✓ daemon restart starts and stop cleanup succeeds\n')
const cleanup = await daemonCommand(["stop", "--force"]);
assert.strictEqual(cleanup.exitCode, 0, "cleanup stop should succeed after restart");
console.log("✓ daemon restart starts and stop cleanup succeeds\n");
}
} finally {
// Best-effort daemon cleanup in case assertions fail before explicit stop.
await daemonCommand(['stop', '--force'])
await daemonCommand(["stop", "--force"]);
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All daemon tests passed ===')
console.log("=== All daemon tests passed ===");
+79 -79
View File
@@ -19,153 +19,153 @@
* - paseo ls does not support --ui
*/
import assert from 'node:assert'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { runLocalPaseo } from './helpers/local-cli.ts'
import assert from "node:assert";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { runLocalPaseo } from "./helpers/local-cli.ts";
console.log('=== LS Command Tests ===\n')
console.log("=== LS Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: paseo --help shows ls command
{
console.log('Test 1: paseo --help shows ls command')
const result = await runLocalPaseo(['--help'])
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('ls'), 'help should mention ls command')
console.log('✓ paseo --help shows ls command\n')
console.log("Test 1: paseo --help shows ls command");
const result = await runLocalPaseo(["--help"]);
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("ls"), "help should mention ls command");
console.log("✓ paseo --help shows ls command\n");
}
// Test 2: paseo ls --help shows options
{
console.log('Test 2: paseo ls --help shows options')
const result = await runLocalPaseo(['ls', '--help'])
assert.strictEqual(result.exitCode, 0, 'paseo ls --help should exit 0')
assert(result.stdout.includes('-a'), 'help should mention -a flag')
assert(result.stdout.includes('--all'), 'help should mention --all flag')
assert(result.stdout.includes('-g'), 'help should mention -g flag')
assert(result.stdout.includes('--global'), 'help should mention --global flag')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(!result.stdout.includes('--ui'), 'help should not mention --ui')
console.log('✓ paseo ls --help shows options\n')
console.log("Test 2: paseo ls --help shows options");
const result = await runLocalPaseo(["ls", "--help"]);
assert.strictEqual(result.exitCode, 0, "paseo ls --help should exit 0");
assert(result.stdout.includes("-a"), "help should mention -a flag");
assert(result.stdout.includes("--all"), "help should mention --all flag");
assert(result.stdout.includes("-g"), "help should mention -g flag");
assert(result.stdout.includes("--global"), "help should mention --global flag");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(!result.stdout.includes("--ui"), "help should not mention --ui");
console.log("✓ paseo ls --help shows options\n");
}
// Test 3: paseo ls returns error when no daemon running
{
console.log('Test 3: paseo ls handles daemon not running')
const result = await runLocalPaseo(['ls'], {
console.log("Test 3: paseo ls handles daemon not running");
const result = await runLocalPaseo(["ls"], {
PASEO_HOST: `localhost:${port}`,
PASEO_HOME: paseoHome,
})
});
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ paseo ls handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ paseo ls handles daemon not running\n");
}
// Test 4: paseo ls --json returns valid JSON error
{
console.log('Test 4: paseo ls --json handles errors')
const result = await runLocalPaseo(['ls', '--json'], {
console.log("Test 4: paseo ls --json handles errors");
const result = await runLocalPaseo(["ls", "--json"], {
PASEO_HOST: `localhost:${port}`,
PASEO_HOME: paseoHome,
})
});
// Should still fail (daemon not running)
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
// But output should be valid JSON if present
const output = result.stdout.trim()
const output = result.stdout.trim();
if (output.length > 0) {
try {
JSON.parse(output)
console.log('✓ paseo ls --json outputs valid JSON error\n')
JSON.parse(output);
console.log("✓ paseo ls --json outputs valid JSON error\n");
} catch {
// Empty or stderr-only output is acceptable
console.log('✓ paseo ls --json handled error (output may be in stderr)\n')
console.log("✓ paseo ls --json handled error (output may be in stderr)\n");
}
} else {
console.log('✓ paseo ls --json handled error gracefully\n')
console.log("✓ paseo ls --json handled error gracefully\n");
}
}
// Test 5: paseo ls -a flag is accepted
{
console.log('Test 5: paseo ls -a flag is accepted')
const result = await runLocalPaseo(['ls', '-a'], {
console.log("Test 5: paseo ls -a flag is accepted");
const result = await runLocalPaseo(["ls", "-a"], {
PASEO_HOST: `localhost:${port}`,
PASEO_HOME: paseoHome,
})
});
// Will fail due to no daemon, but flag should be parsed without error
// (no "unknown option" error)
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -a flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ paseo ls -a flag is accepted\n')
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -a flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ paseo ls -a flag is accepted\n");
}
// Test 6: paseo ls -g flag is accepted
{
console.log('Test 6: paseo ls -g flag is accepted')
const result = await runLocalPaseo(['ls', '-g'], {
console.log("Test 6: paseo ls -g flag is accepted");
const result = await runLocalPaseo(["ls", "-g"], {
PASEO_HOST: `localhost:${port}`,
PASEO_HOME: paseoHome,
})
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -g flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ paseo ls -g flag is accepted\n')
});
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -g flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ paseo ls -g flag is accepted\n");
}
// Test 7: paseo ls -ag combined flags are accepted
{
console.log('Test 7: paseo ls -ag combined flags are accepted')
const result = await runLocalPaseo(['ls', '-ag'], {
console.log("Test 7: paseo ls -ag combined flags are accepted");
const result = await runLocalPaseo(["ls", "-ag"], {
PASEO_HOST: `localhost:${port}`,
PASEO_HOME: paseoHome,
})
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -ag flags')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ paseo ls -ag combined flags are accepted\n')
});
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -ag flags");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ paseo ls -ag combined flags are accepted\n");
}
// Test 8: -q (quiet) flag is accepted globally
{
console.log('Test 8: -q (quiet) flag is accepted')
const result = await runLocalPaseo(['-q', 'ls'], {
console.log("Test 8: -q (quiet) flag is accepted");
const result = await runLocalPaseo(["-q", "ls"], {
PASEO_HOST: `localhost:${port}`,
PASEO_HOME: paseoHome,
})
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted\n')
});
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted\n");
}
// Test 9: paseo ls --ui is rejected (flag removed)
{
console.log('Test 9: paseo ls --ui is rejected')
const result = await runLocalPaseo(['ls', '--ui'], {
console.log("Test 9: paseo ls --ui is rejected");
const result = await runLocalPaseo(["ls", "--ui"], {
PASEO_HOST: `localhost:${port}`,
PASEO_HOME: paseoHome,
})
assert.notStrictEqual(result.exitCode, 0, 'should fail for removed --ui flag')
const output = result.stdout + result.stderr
assert(output.includes('unknown option'), 'should report unknown option for --ui')
console.log('✓ paseo ls --ui is rejected\n')
});
assert.notStrictEqual(result.exitCode, 0, "should fail for removed --ui flag");
const output = result.stdout + result.stderr;
assert(output.includes("unknown option"), "should report unknown option for --ui");
console.log("✓ paseo ls --ui is rejected\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All ls tests passed ===')
console.log("=== All ls tests passed ===");
+131 -128
View File
@@ -20,229 +20,232 @@
* - run --cwd flag is accepted
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Run Command Tests ===\n')
console.log("=== Run Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const schemaPath = join(paseoHome, 'run-output-schema.json')
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
const schemaPath = join(paseoHome, "run-output-schema.json");
await writeFile(
schemaPath,
JSON.stringify({
type: 'object',
type: "object",
properties: {
status: { type: 'string' },
status: { type: "string" },
},
required: ['status'],
required: ["status"],
additionalProperties: false,
})
)
}),
);
try {
// Test 1: run --help shows options
{
console.log('Test 1: run --help shows options')
const result = await $`npx paseo run --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'run --help should exit 0')
assert(result.stdout.includes('-d'), 'help should mention -d flag')
assert(result.stdout.includes('--detach'), 'help should mention --detach flag')
assert(result.stdout.includes('--name'), 'help should mention --name option')
assert(result.stdout.includes('--provider'), 'help should mention --provider option')
assert(result.stdout.includes('--mode'), 'help should mention --mode option')
assert(result.stdout.includes('--cwd'), 'help should mention --cwd option')
assert(result.stdout.includes('--output-schema'), 'help should mention --output-schema option')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<prompt>'), 'help should mention prompt argument')
assert(!result.stdout.includes('--ui'), 'help should not mention removed --ui option')
console.log('✓ run --help shows options\n')
console.log("Test 1: run --help shows options");
const result = await $`npx paseo run --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "run --help should exit 0");
assert(result.stdout.includes("-d"), "help should mention -d flag");
assert(result.stdout.includes("--detach"), "help should mention --detach flag");
assert(result.stdout.includes("--name"), "help should mention --name option");
assert(result.stdout.includes("--provider"), "help should mention --provider option");
assert(result.stdout.includes("--mode"), "help should mention --mode option");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
assert(result.stdout.includes("--output-schema"), "help should mention --output-schema option");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<prompt>"), "help should mention prompt argument");
assert(!result.stdout.includes("--ui"), "help should not mention removed --ui option");
console.log("✓ run --help shows options\n");
}
// Test 2: run requires prompt argument
{
console.log('Test 2: run requires prompt argument')
console.log("Test 2: run requires prompt argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without prompt')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without prompt");
const output = result.stdout + result.stderr;
// Commander should complain about missing argument
const hasMissingArg =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument')
assert(hasMissingArg, 'error should mention missing argument')
console.log('✓ run requires prompt argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasMissingArg, "error should mention missing argument");
console.log("✓ run requires prompt argument\n");
}
// Test 3: run handles daemon not running
{
console.log('Test 3: run handles daemon not running')
console.log("Test 3: run handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run "test prompt"`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run "test prompt"`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ run handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ run handles daemon not running\n");
}
// Test 4: run -d flag is accepted
{
console.log('Test 4: run -d flag is accepted')
console.log("Test 4: run -d flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run -d "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -d flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ run -d flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run -d "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -d flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run -d flag is accepted\n");
}
// Test 5: run --name flag is accepted
{
console.log('Test 5: run --name flag is accepted')
console.log("Test 5: run --name flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --name "test-agent" "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --name flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ run --name flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --name "test-agent" "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --name flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run --name flag is accepted\n");
}
// Test 6: run --provider flag is accepted
{
console.log('Test 6: run --provider flag is accepted')
console.log("Test 6: run --provider flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --provider flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ run --provider flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --provider flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run --provider flag is accepted\n");
}
// Test 6b: run --provider provider/model syntax is accepted
{
console.log('Test 6b: run --provider provider/model syntax is accepted')
console.log("Test 6b: run --provider provider/model syntax is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex/gpt-5.4 "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept provider/model syntax')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ run --provider provider/model syntax is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex/gpt-5.4 "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept provider/model syntax");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run --provider provider/model syntax is accepted\n");
}
// Test 7: run --mode flag is accepted
{
console.log('Test 7: run --mode flag is accepted')
console.log("Test 7: run --mode flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --mode bypass "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --mode flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ run --mode flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --mode bypass "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --mode flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run --mode flag is accepted\n");
}
// Test 8: run --cwd flag is accepted
{
console.log('Test 8: run --cwd flag is accepted')
console.log("Test 8: run --cwd flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --cwd /tmp "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --cwd flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ run --cwd flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --cwd /tmp "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --cwd flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run --cwd flag is accepted\n");
}
// Test 9: run --output-schema flag is accepted
{
console.log('Test 9: run --output-schema flag is accepted')
console.log("Test 9: run --output-schema flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --output-schema ${schemaPath} "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --output-schema flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ run --output-schema flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --output-schema ${schemaPath} "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --output-schema flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run --output-schema flag is accepted\n");
}
// Test 10: run --output-schema cannot be used with --detach
{
console.log('Test 10: run --output-schema cannot be used with --detach')
console.log("Test 10: run --output-schema cannot be used with --detach");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run -d --output-schema ${schemaPath} "test prompt"`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail with --detach and --output-schema')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run -d --output-schema ${schemaPath} "test prompt"`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail with --detach and --output-schema");
const output = result.stdout + result.stderr;
assert(
output.includes('--output-schema cannot be used with --detach'),
'error should explain detach incompatibility'
)
console.log('✓ run --output-schema cannot be used with --detach\n')
output.includes("--output-schema cannot be used with --detach"),
"error should explain detach incompatibility",
);
console.log("✓ run --output-schema cannot be used with --detach\n");
}
// Test 11: -q (quiet) flag is accepted with run
{
console.log('Test 11: -q (quiet) flag is accepted with run')
console.log("Test 11: -q (quiet) flag is accepted with run");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with run\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with run\n");
}
// Test 12: Combined flags work together
{
console.log('Test 12: Combined flags work together')
console.log("Test 12: Combined flags work together");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d --name "test-fixer" --provider claude --mode bypass --cwd /tmp "Fix the tests"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept all combined flags')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ Combined flags work together\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d --name "test-fixer" --provider claude --mode bypass --cwd /tmp "Fix the tests"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept all combined flags");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ Combined flags work together\n");
}
// Test 12b: conflicting provider/model syntax is rejected before connect
{
console.log('Test 12b: conflicting provider/model syntax is rejected')
console.log("Test 12b: conflicting provider/model syntax is rejected");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex/gpt-5.4 --model gpt-5.5 "test prompt"`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail for conflicting model inputs')
const output = result.stdout + result.stderr
assert(output.includes('Conflicting model values provided'), 'should explain conflicting model inputs')
console.log('✓ conflicting provider/model syntax is rejected\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex/gpt-5.4 --model gpt-5.5 "test prompt"`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail for conflicting model inputs");
const output = result.stdout + result.stderr;
assert(
output.includes("Conflicting model values provided"),
"should explain conflicting model inputs",
);
console.log("✓ conflicting provider/model syntax is rejected\n");
}
// Test 13: paseo --help shows run command
{
console.log('Test 13: paseo --help shows run command')
const result = await $`npx paseo --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('run'), 'help should mention run command')
console.log('✓ paseo --help shows run command\n')
console.log("Test 13: paseo --help shows run command");
const result = await $`npx paseo --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("run"), "help should mention run command");
console.log("✓ paseo --help shows run command\n");
}
// Test 14: run --ui is rejected (flag removed)
{
console.log('Test 14: run --ui is rejected')
console.log("Test 14: run --ui is rejected");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --ui "test prompt"`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail for removed --ui flag')
const output = result.stdout + result.stderr
assert(output.includes('unknown option'), 'should report unknown option for --ui')
console.log('✓ run --ui is rejected\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --ui "test prompt"`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail for removed --ui flag");
const output = result.stdout + result.stderr;
assert(output.includes("unknown option"), "should report unknown option for --ui");
console.log("✓ run --ui is rejected\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All run tests passed ===')
console.log("=== All run tests passed ===");
+111 -112
View File
@@ -17,194 +17,193 @@
* - agent shows send in subcommands
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Send Command Tests ===\n')
console.log("=== Send Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const promptFilePath = join(paseoHome, 'send-prompt.txt')
await writeFile(promptFilePath, 'prompt from file')
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
const promptFilePath = join(paseoHome, "send-prompt.txt");
await writeFile(promptFilePath, "prompt from file");
try {
// Test 1: send --help shows options
{
console.log('Test 1: send --help shows options')
const result = await $`npx paseo send --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'send --help should exit 0')
assert(result.stdout.includes('--prompt'), 'help should mention --prompt option')
assert(result.stdout.includes('--prompt-file'), 'help should mention --prompt-file option')
assert(result.stdout.includes('--no-wait'), 'help should mention --no-wait flag')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<id>'), 'help should mention id argument')
assert(result.stdout.includes('[prompt]'), 'help should mention optional prompt argument')
console.log(' help should mention --prompt option')
console.log(' help should mention --prompt-file option')
console.log(' help should mention --no-wait flag')
console.log(' help should mention --host option')
console.log(' help should mention <id> argument')
console.log(' help should mention [prompt] argument')
console.log('✓ send --help shows options\n')
console.log("Test 1: send --help shows options");
const result = await $`npx paseo send --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "send --help should exit 0");
assert(result.stdout.includes("--prompt"), "help should mention --prompt option");
assert(result.stdout.includes("--prompt-file"), "help should mention --prompt-file option");
assert(result.stdout.includes("--no-wait"), "help should mention --no-wait flag");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<id>"), "help should mention id argument");
assert(result.stdout.includes("[prompt]"), "help should mention optional prompt argument");
console.log(" help should mention --prompt option");
console.log(" help should mention --prompt-file option");
console.log(" help should mention --no-wait flag");
console.log(" help should mention --host option");
console.log(" help should mention <id> argument");
console.log(" help should mention [prompt] argument");
console.log("✓ send --help shows options\n");
}
// Test 2: send requires id argument
{
console.log('Test 2: send requires id argument')
console.log("Test 2: send requires id argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
// Commander should complain about missing argument
const hasMissingArg =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument')
assert(hasMissingArg, 'error should mention missing argument')
console.log('✓ send requires id argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasMissingArg, "error should mention missing argument");
console.log("✓ send requires id argument\n");
}
// Test 3: send requires prompt argument
{
console.log('Test 3: send requires prompt argument')
console.log("Test 3: send requires prompt argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without prompt')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without prompt");
const output = result.stdout + result.stderr;
// Commander should complain about missing argument
const hasMissingArg =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument')
assert(hasMissingArg, 'error should mention missing argument')
console.log('✓ send requires prompt argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasMissingArg, "error should mention missing argument");
console.log("✓ send requires prompt argument\n");
}
// Test 4: send handles daemon not running
{
console.log('Test 4: send handles daemon not running')
console.log("Test 4: send handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123 "test prompt"`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123 "test prompt"`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ send handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ send handles daemon not running\n");
}
// Test 5: send --no-wait flag is accepted
{
console.log('Test 5: send --no-wait flag is accepted')
console.log("Test 5: send --no-wait flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --no-wait abc123 "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --no-wait flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ send --no-wait flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --no-wait abc123 "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --no-wait flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ send --no-wait flag is accepted\n");
}
// Test 5b: send --prompt flag is accepted
{
console.log('Test 5b: send --prompt flag is accepted')
console.log("Test 5b: send --prompt flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --prompt "test prompt" abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --prompt flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ send --prompt flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --prompt "test prompt" abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --prompt flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ send --prompt flag is accepted\n");
}
// Test 5c: send --prompt-file flag is accepted
{
console.log('Test 5c: send --prompt-file flag is accepted')
console.log("Test 5c: send --prompt-file flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --prompt-file ${promptFilePath} abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --prompt-file flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ send --prompt-file flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --prompt-file ${promptFilePath} abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --prompt-file flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ send --prompt-file flag is accepted\n");
}
// Test 6: send --host flag is accepted
{
console.log('Test 6: send --host flag is accepted')
console.log("Test 6: send --host flag is accepted");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo send --host localhost:${port} abc123 "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ send --host flag is accepted\n')
await $`PASEO_HOME=${paseoHome} npx paseo send --host localhost:${port} abc123 "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ send --host flag is accepted\n");
}
// Test 7: -q (quiet) flag is accepted with send
{
console.log('Test 7: -q (quiet) flag is accepted with send')
console.log("Test 7: -q (quiet) flag is accepted with send");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q send --no-wait abc123 "test prompt"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with send\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q send --no-wait abc123 "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with send\n");
}
// Test 8: Combined flags work together
{
console.log('Test 8: Combined flags work together')
console.log("Test 8: Combined flags work together");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q send --no-wait abc123 "Run the linter"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept all combined flags')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ Combined flags work together\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q send --no-wait abc123 "Run the linter"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept all combined flags");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ Combined flags work together\n");
}
// Test 8b: conflicting prompt sources are rejected
{
console.log('Test 8b: conflicting prompt sources are rejected')
console.log("Test 8b: conflicting prompt sources are rejected");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123 "positional prompt" --prompt "flag prompt"`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail for conflicting prompt sources')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123 "positional prompt" --prompt "flag prompt"`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail for conflicting prompt sources");
const output = result.stdout + result.stderr;
assert(
output.includes('Provide exactly one of prompt argument, --prompt, or --prompt-file'),
'should explain conflicting prompt sources'
)
console.log('✓ conflicting prompt sources are rejected\n')
output.includes("Provide exactly one of prompt argument, --prompt, or --prompt-file"),
"should explain conflicting prompt sources",
);
console.log("✓ conflicting prompt sources are rejected\n");
}
// Test 9: paseo --help shows send command
{
console.log('Test 9: paseo --help shows send command')
const result = await $`npx paseo --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('send'), 'help should mention send command')
console.log('✓ paseo --help shows send command\n')
console.log("Test 9: paseo --help shows send command");
const result = await $`npx paseo --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("send"), "help should mention send command");
console.log("✓ paseo --help shows send command\n");
}
// Test 10: ID prefix syntax is mentioned in help
{
console.log('Test 10: send command description mentions ID')
const result = await $`npx paseo send --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'send --help should exit 0')
console.log("Test 10: send command description mentions ID");
const result = await $`npx paseo send --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "send --help should exit 0");
const hasIdMention =
result.stdout.toLowerCase().includes('id') ||
result.stdout.toLowerCase().includes('prefix')
assert(hasIdMention, 'help should mention ID or prefix')
console.log('✓ send command description mentions ID\n')
result.stdout.toLowerCase().includes("id") || result.stdout.toLowerCase().includes("prefix");
assert(hasIdMention, "help should mention ID or prefix");
console.log("✓ send command description mentions ID\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All send tests passed ===')
console.log("=== All send tests passed ===");
+67 -67
View File
@@ -17,120 +17,120 @@
* - stop --cwd flag is accepted
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Stop Command Tests ===\n')
console.log("=== Stop Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: stop --help shows options
{
console.log('Test 1: stop --help shows options')
const result = await $`npx paseo stop --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'stop --help should exit 0')
assert(result.stdout.includes('--all'), 'help should mention --all flag')
assert(result.stdout.includes('--cwd'), 'help should mention --cwd option')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('[id]'), 'help should mention optional id argument')
console.log('✓ stop --help shows options\n')
console.log("Test 1: stop --help shows options");
const result = await $`npx paseo stop --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "stop --help should exit 0");
assert(result.stdout.includes("--all"), "help should mention --all flag");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("[id]"), "help should mention optional id argument");
console.log("✓ stop --help shows options\n");
}
// Test 2: stop requires ID, --all, or --cwd
{
console.log('Test 2: stop requires ID, --all, or --cwd')
console.log("Test 2: stop requires ID, --all, or --cwd");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id, --all, or --cwd')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id, --all, or --cwd");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument') ||
output.toLowerCase().includes('id')
assert(hasError, 'error should mention missing argument')
console.log('✓ stop requires ID, --all, or --cwd\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument") ||
output.toLowerCase().includes("id");
assert(hasError, "error should mention missing argument");
console.log("✓ stop requires ID, --all, or --cwd\n");
}
// Test 3: stop handles daemon not running
{
console.log('Test 3: stop handles daemon not running')
console.log("Test 3: stop handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop abc123`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop abc123`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ stop handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ stop handles daemon not running\n");
}
// Test 4: stop --all flag is accepted
{
console.log('Test 4: stop --all flag is accepted')
console.log("Test 4: stop --all flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop --all`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --all flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ stop --all flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop --all`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --all flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ stop --all flag is accepted\n");
}
// Test 5: stop --cwd flag is accepted
{
console.log('Test 5: stop --cwd flag is accepted')
console.log("Test 5: stop --cwd flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop --cwd /tmp`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --cwd flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ stop --cwd flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop --cwd /tmp`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --cwd flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ stop --cwd flag is accepted\n");
}
// Test 6: stop with ID and --host flag is accepted
{
console.log('Test 6: stop with ID and --host flag is accepted')
console.log("Test 6: stop with ID and --host flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop abc123 --host localhost:${port}`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ stop with ID and --host flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop abc123 --host localhost:${port}`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ stop with ID and --host flag is accepted\n");
}
// Test 7: paseo --help shows stop command
{
console.log('Test 7: paseo --help shows stop command')
const result = await $`npx paseo --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('stop'), 'help should mention stop command')
console.log('✓ paseo --help shows stop command\n')
console.log("Test 7: paseo --help shows stop command");
const result = await $`npx paseo --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("stop"), "help should mention stop command");
console.log("✓ paseo --help shows stop command\n");
}
// Test 8: -q (quiet) flag is accepted with stop
{
console.log('Test 8: -q (quiet) flag is accepted with stop')
console.log("Test 8: -q (quiet) flag is accepted with stop");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q stop abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with stop\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q stop abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with stop\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All stop tests passed ===')
console.log("=== All stop tests passed ===");
+76 -73
View File
@@ -17,132 +17,135 @@
* - logs --tail flag is accepted
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Logs Command Tests ===\n')
console.log("=== Logs Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: logs --help shows options
{
console.log('Test 1: logs --help shows options')
const result = await $`npx paseo logs --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'logs --help should exit 0')
assert(result.stdout.includes('-f') || result.stdout.includes('--follow'), 'help should mention -f/--follow flag')
assert(result.stdout.includes('--tail'), 'help should mention --tail option')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<id>'), 'help should mention required id argument')
console.log('✓ logs --help shows options\n')
console.log("Test 1: logs --help shows options");
const result = await $`npx paseo logs --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "logs --help should exit 0");
assert(
result.stdout.includes("-f") || result.stdout.includes("--follow"),
"help should mention -f/--follow flag",
);
assert(result.stdout.includes("--tail"), "help should mention --tail option");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<id>"), "help should mention required id argument");
console.log("✓ logs --help shows options\n");
}
// Test 2: logs requires ID argument
{
console.log('Test 2: logs requires ID argument')
console.log("Test 2: logs requires ID argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument') ||
output.toLowerCase().includes('id')
assert(hasError, 'error should mention missing argument')
console.log('✓ logs requires ID argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument") ||
output.toLowerCase().includes("id");
assert(hasError, "error should mention missing argument");
console.log("✓ logs requires ID argument\n");
}
// Test 3: logs handles daemon not running
{
console.log('Test 3: logs handles daemon not running')
console.log("Test 3: logs handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs abc123`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs abc123`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ logs handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ logs handles daemon not running\n");
}
// Test 4: logs -f (follow) flag is accepted
{
console.log('Test 4: logs -f (follow) flag is accepted')
console.log("Test 4: logs -f (follow) flag is accepted");
// Use timeout to avoid hanging on follow mode
const result =
await $`timeout 1 bash -c 'PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs -f abc123' || true`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -f flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ logs -f (follow) flag is accepted\n')
await $`timeout 1 bash -c 'PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs -f abc123' || true`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -f flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ logs -f (follow) flag is accepted\n");
}
// Test 5: logs --follow flag is accepted
{
console.log('Test 5: logs --follow flag is accepted')
console.log("Test 5: logs --follow flag is accepted");
const result =
await $`timeout 1 bash -c 'PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs --follow abc123' || true`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --follow flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ logs --follow flag is accepted\n')
await $`timeout 1 bash -c 'PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs --follow abc123' || true`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --follow flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ logs --follow flag is accepted\n");
}
// Test 6: logs --tail flag is accepted
{
console.log('Test 6: logs --tail flag is accepted')
console.log("Test 6: logs --tail flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs --tail 50 abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --tail flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ logs --tail flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs --tail 50 abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --tail flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ logs --tail flag is accepted\n");
}
// Test 7: logs with ID and --host flag is accepted
{
console.log('Test 7: logs with ID and --host flag is accepted')
console.log("Test 7: logs with ID and --host flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs abc123 --host localhost:${port}`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ logs with ID and --host flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs abc123 --host localhost:${port}`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ logs with ID and --host flag is accepted\n");
}
// Test 8: paseo --help shows logs command
{
console.log('Test 8: paseo --help shows logs command')
const result = await $`npx paseo --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('logs'), 'help should mention logs command')
console.log('✓ paseo --help shows logs command\n')
console.log("Test 8: paseo --help shows logs command");
const result = await $`npx paseo --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("logs"), "help should mention logs command");
console.log("✓ paseo --help shows logs command\n");
}
// Test 9: -q (quiet) flag is accepted with logs
{
console.log('Test 9: -q (quiet) flag is accepted with logs')
console.log("Test 9: -q (quiet) flag is accepted with logs");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q logs abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with logs\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q logs abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with logs\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All logs tests passed ===')
console.log("=== All logs tests passed ===");
+80 -81
View File
@@ -17,145 +17,144 @@
* - agent shows inspect in subcommands
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Inspect Command Tests ===\n')
console.log("=== Inspect Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: inspect --help shows options
{
console.log('Test 1: inspect --help shows options')
const result = await $`npx paseo inspect --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'inspect --help should exit 0')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<id>'), 'help should mention id argument')
console.log(' help should mention --host option')
console.log(' help should mention <id> argument')
console.log('inspect --help shows options\n')
console.log("Test 1: inspect --help shows options");
const result = await $`npx paseo inspect --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "inspect --help should exit 0");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<id>"), "help should mention id argument");
console.log(" help should mention --host option");
console.log(" help should mention <id> argument");
console.log("inspect --help shows options\n");
}
// Test 2: inspect requires id argument
{
console.log('Test 2: inspect requires id argument')
console.log("Test 2: inspect requires id argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
// Commander should complain about missing argument
const hasMissingArg =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument')
assert(hasMissingArg, 'error should mention missing argument')
console.log('inspect requires id argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasMissingArg, "error should mention missing argument");
console.log("inspect requires id argument\n");
}
// Test 3: inspect handles daemon not running
{
console.log('Test 3: inspect handles daemon not running')
console.log("Test 3: inspect handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect abc123`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect abc123`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('inspect handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("inspect handles daemon not running\n");
}
// Test 4: inspect --host flag is accepted
{
console.log('Test 4: inspect --host flag is accepted')
console.log("Test 4: inspect --host flag is accepted");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo inspect --host localhost:${port} abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('inspect --host flag is accepted\n')
await $`PASEO_HOME=${paseoHome} npx paseo inspect --host localhost:${port} abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("inspect --host flag is accepted\n");
}
// Test 5: -q (quiet) flag is accepted with inspect
{
console.log('Test 5: -q (quiet) flag is accepted with inspect')
console.log("Test 5: -q (quiet) flag is accepted with inspect");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q inspect abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('-q (quiet) flag is accepted with inspect\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q inspect abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("-q (quiet) flag is accepted with inspect\n");
}
// Test 6: --json flag is accepted with inspect
{
console.log('Test 6: --json flag is accepted with inspect')
console.log("Test 6: --json flag is accepted with inspect");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect abc123 --json`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --json flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('--json flag is accepted with inspect\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect abc123 --json`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --json flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("--json flag is accepted with inspect\n");
}
// Test 7: --format yaml flag is accepted with inspect
{
console.log('Test 7: --format yaml flag is accepted with inspect')
console.log("Test 7: --format yaml flag is accepted with inspect");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format yaml inspect abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --format yaml flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('--format yaml flag is accepted with inspect\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format yaml inspect abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --format yaml flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("--format yaml flag is accepted with inspect\n");
}
// Test 8: paseo --help shows inspect command
{
console.log('Test 8: paseo --help shows inspect command')
const result = await $`npx paseo --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('inspect'), 'help should mention inspect command')
console.log('paseo --help shows inspect command\n')
console.log("Test 8: paseo --help shows inspect command");
const result = await $`npx paseo --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("inspect"), "help should mention inspect command");
console.log("paseo --help shows inspect command\n");
}
// Test 9: inspect command description is helpful
{
console.log('Test 9: inspect command description is helpful')
const result = await $`npx paseo inspect --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'inspect --help should exit 0')
console.log("Test 9: inspect command description is helpful");
const result = await $`npx paseo inspect --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "inspect --help should exit 0");
const hasDescription =
result.stdout.toLowerCase().includes('detail') ||
result.stdout.toLowerCase().includes('information') ||
result.stdout.toLowerCase().includes('show')
assert(hasDescription, 'help should describe what inspect does')
console.log('inspect command description is helpful\n')
result.stdout.toLowerCase().includes("detail") ||
result.stdout.toLowerCase().includes("information") ||
result.stdout.toLowerCase().includes("show");
assert(hasDescription, "help should describe what inspect does");
console.log("inspect command description is helpful\n");
}
// Test 10: ID prefix syntax is mentioned in help
{
console.log('Test 10: inspect command mentions ID')
const result = await $`npx paseo inspect --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'inspect --help should exit 0')
console.log("Test 10: inspect command mentions ID");
const result = await $`npx paseo inspect --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "inspect --help should exit 0");
const hasIdMention =
result.stdout.toLowerCase().includes('id') ||
result.stdout.toLowerCase().includes('prefix')
assert(hasIdMention, 'help should mention ID or prefix')
console.log('inspect command mentions ID\n')
result.stdout.toLowerCase().includes("id") || result.stdout.toLowerCase().includes("prefix");
assert(hasIdMention, "help should mention ID or prefix");
console.log("inspect command mentions ID\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All inspect tests passed ===')
console.log("=== All inspect tests passed ===");
+69 -69
View File
@@ -16,124 +16,124 @@
* - agent mode --list flag is accepted
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Agent Mode Command Tests ===\n')
console.log("=== Agent Mode Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: agent mode --help shows options
{
console.log('Test 1: agent mode --help shows options')
const result = await $`npx paseo agent mode --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'agent mode --help should exit 0')
assert(result.stdout.includes('--list'), 'help should mention --list flag')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<id>'), 'help should mention id argument')
assert(result.stdout.includes('[mode]'), 'help should mention optional mode argument')
console.log('✓ agent mode --help shows options\n')
console.log("Test 1: agent mode --help shows options");
const result = await $`npx paseo agent mode --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent mode --help should exit 0");
assert(result.stdout.includes("--list"), "help should mention --list flag");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<id>"), "help should mention id argument");
assert(result.stdout.includes("[mode]"), "help should mention optional mode argument");
console.log("✓ agent mode --help shows options\n");
}
// Test 2: agent mode requires id argument
{
console.log('Test 2: agent mode requires id argument')
console.log("Test 2: agent mode requires id argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument') ||
output.toLowerCase().includes('id')
assert(hasError, 'error should mention missing argument')
console.log('✓ agent mode requires id argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument") ||
output.toLowerCase().includes("id");
assert(hasError, "error should mention missing argument");
console.log("✓ agent mode requires id argument\n");
}
// Test 3: agent mode handles daemon not running
{
console.log('Test 3: agent mode handles daemon not running')
console.log("Test 3: agent mode handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123 bypass`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123 bypass`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ agent mode handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ agent mode handles daemon not running\n");
}
// Test 4: agent mode --list flag is accepted
{
console.log('Test 4: agent mode --list flag is accepted')
console.log("Test 4: agent mode --list flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode --list abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --list flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ agent mode --list flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode --list abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --list flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ agent mode --list flag is accepted\n");
}
// Test 5: agent mode with ID and --host flag is accepted
{
console.log('Test 5: agent mode with ID and --host flag is accepted')
console.log("Test 5: agent mode with ID and --host flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123 plan --host localhost:${port}`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ agent mode with ID and --host flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123 plan --host localhost:${port}`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ agent mode with ID and --host flag is accepted\n");
}
// Test 6: agent shows mode in subcommands
{
console.log('Test 6: agent --help shows mode subcommand')
const result = await $`npx paseo agent --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'agent --help should exit 0')
assert(result.stdout.includes('mode'), 'help should mention mode subcommand')
console.log('✓ agent --help shows mode subcommand\n')
console.log("Test 6: agent --help shows mode subcommand");
const result = await $`npx paseo agent --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent --help should exit 0");
assert(result.stdout.includes("mode"), "help should mention mode subcommand");
console.log("✓ agent --help shows mode subcommand\n");
}
// Test 7: -q (quiet) flag is accepted with agent mode
{
console.log('Test 7: -q (quiet) flag is accepted with agent mode')
console.log("Test 7: -q (quiet) flag is accepted with agent mode");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q agent mode abc123 bypass`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with agent mode\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q agent mode abc123 bypass`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with agent mode\n");
}
// Test 8: agent mode requires mode argument when not using --list
{
console.log('Test 8: agent mode requires mode argument when not using --list')
console.log("Test 8: agent mode requires mode argument when not using --list");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123`.nothrow();
// Should fail because mode is required unless --list is specified
assert.notStrictEqual(result.exitCode, 0, 'should fail without mode argument')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail without mode argument");
const output = result.stdout + result.stderr;
assert(
output.includes('Mode argument required unless --list is specified'),
'error should mention missing mode argument'
)
console.log('✓ agent mode requires mode argument when not using --list\n')
output.includes("Mode argument required unless --list is specified"),
"error should mention missing mode argument",
);
console.log("✓ agent mode requires mode argument when not using --list\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All agent mode tests passed ===')
console.log("=== All agent mode tests passed ===");
+59 -59
View File
@@ -16,107 +16,107 @@
* - agent archive --force flag is accepted
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Agent Archive Command Tests ===\n')
console.log("=== Agent Archive Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: agent archive --help shows options
{
console.log('Test 1: agent archive --help shows options')
const result = await $`npx paseo agent archive --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'agent archive --help should exit 0')
assert(result.stdout.includes('--force'), 'help should mention --force flag')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<id>'), 'help should mention required id argument')
console.log('✓ agent archive --help shows options\n')
console.log("Test 1: agent archive --help shows options");
const result = await $`npx paseo agent archive --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent archive --help should exit 0");
assert(result.stdout.includes("--force"), "help should mention --force flag");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<id>"), "help should mention required id argument");
console.log("✓ agent archive --help shows options\n");
}
// Test 2: agent archive requires ID argument
{
console.log('Test 2: agent archive requires ID argument')
console.log("Test 2: agent archive requires ID argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument')
assert(hasError, 'error should mention missing argument')
console.log('✓ agent archive requires ID argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasError, "error should mention missing argument");
console.log("✓ agent archive requires ID argument\n");
}
// Test 3: agent archive handles daemon not running
{
console.log('Test 3: agent archive handles daemon not running')
console.log("Test 3: agent archive handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ agent archive handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ agent archive handles daemon not running\n");
}
// Test 4: agent archive --force flag is accepted
{
console.log('Test 4: agent archive --force flag is accepted')
console.log("Test 4: agent archive --force flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123 --force`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --force flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ agent archive --force flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123 --force`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --force flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ agent archive --force flag is accepted\n");
}
// Test 5: agent archive with ID and --host flag is accepted
{
console.log('Test 5: agent archive with ID and --host flag is accepted')
console.log("Test 5: agent archive with ID and --host flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123 --host localhost:${port}`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ agent archive with ID and --host flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123 --host localhost:${port}`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ agent archive with ID and --host flag is accepted\n");
}
// Test 6: agent shows archive in subcommands
{
console.log('Test 6: agent --help shows archive subcommand')
const result = await $`npx paseo agent --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'agent --help should exit 0')
assert(result.stdout.includes('archive'), 'help should mention archive subcommand')
console.log('✓ agent --help shows archive subcommand\n')
console.log("Test 6: agent --help shows archive subcommand");
const result = await $`npx paseo agent --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent --help should exit 0");
assert(result.stdout.includes("archive"), "help should mention archive subcommand");
console.log("✓ agent --help shows archive subcommand\n");
}
// Test 7: -q (quiet) flag is accepted with agent archive
{
console.log('Test 7: -q (quiet) flag is accepted with agent archive')
console.log("Test 7: -q (quiet) flag is accepted with agent archive");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q agent archive abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with agent archive\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q agent archive abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with agent archive\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All agent archive tests passed ===')
console.log("=== All agent archive tests passed ===");
+93 -95
View File
@@ -18,169 +18,167 @@
* - agent shows wait in subcommands
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Wait Command Tests ===\n')
console.log("=== Wait Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: wait --help shows options
{
console.log('Test 1: wait --help shows options')
const result = await $`npx paseo wait --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'wait --help should exit 0')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('--timeout'), 'help should mention --timeout option')
assert(result.stdout.includes('<id>'), 'help should mention id argument')
console.log(' help should mention --host option')
console.log(' help should mention --timeout option')
console.log(' help should mention <id> argument')
console.log('wait --help shows options\n')
console.log("Test 1: wait --help shows options");
const result = await $`npx paseo wait --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "wait --help should exit 0");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("--timeout"), "help should mention --timeout option");
assert(result.stdout.includes("<id>"), "help should mention id argument");
console.log(" help should mention --host option");
console.log(" help should mention --timeout option");
console.log(" help should mention <id> argument");
console.log("wait --help shows options\n");
}
// Test 2: wait requires id argument
{
console.log('Test 2: wait requires id argument')
console.log("Test 2: wait requires id argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
// Commander should complain about missing argument
const hasMissingArg =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument')
assert(hasMissingArg, 'error should mention missing argument')
console.log('wait requires id argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasMissingArg, "error should mention missing argument");
console.log("wait requires id argument\n");
}
// Test 3: wait handles daemon not running
{
console.log('Test 3: wait handles daemon not running')
console.log("Test 3: wait handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait abc123`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait abc123`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('wait handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("wait handles daemon not running\n");
}
// Test 4: wait --timeout flag is accepted
{
console.log('Test 4: wait --timeout flag is accepted')
console.log("Test 4: wait --timeout flag is accepted");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo wait --timeout 30 --host localhost:${port} abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --timeout flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('wait --timeout flag is accepted\n')
await $`PASEO_HOME=${paseoHome} npx paseo wait --timeout 30 --host localhost:${port} abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --timeout flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("wait --timeout flag is accepted\n");
}
// Test 5: wait --host flag is accepted
{
console.log('Test 5: wait --host flag is accepted')
console.log("Test 5: wait --host flag is accepted");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo wait --host localhost:${port} abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('wait --host flag is accepted\n')
await $`PASEO_HOME=${paseoHome} npx paseo wait --host localhost:${port} abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("wait --host flag is accepted\n");
}
// Test 6: -q (quiet) flag is accepted with wait
{
console.log('Test 6: -q (quiet) flag is accepted with wait')
console.log("Test 6: -q (quiet) flag is accepted with wait");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q wait abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('-q (quiet) flag is accepted with wait\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q wait abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("-q (quiet) flag is accepted with wait\n");
}
// Test 7: --json flag is accepted with wait
{
console.log('Test 7: --json flag is accepted with wait')
console.log("Test 7: --json flag is accepted with wait");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait abc123 --json`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --json flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('--json flag is accepted with wait\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait abc123 --json`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --json flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("--json flag is accepted with wait\n");
}
// Test 8: --format yaml flag is accepted with wait
{
console.log('Test 8: --format yaml flag is accepted with wait')
console.log("Test 8: --format yaml flag is accepted with wait");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format yaml wait abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --format yaml flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('--format yaml flag is accepted with wait\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format yaml wait abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --format yaml flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("--format yaml flag is accepted with wait\n");
}
// Test 9: paseo --help shows wait command
{
console.log('Test 9: paseo --help shows wait command')
const result = await $`npx paseo --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('wait'), 'help should mention wait command')
console.log('paseo --help shows wait command\n')
console.log("Test 9: paseo --help shows wait command");
const result = await $`npx paseo --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("wait"), "help should mention wait command");
console.log("paseo --help shows wait command\n");
}
// Test 10: wait command description is helpful
{
console.log('Test 10: wait command description is helpful')
const result = await $`npx paseo wait --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'wait --help should exit 0')
console.log("Test 10: wait command description is helpful");
const result = await $`npx paseo wait --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "wait --help should exit 0");
const hasDescription =
result.stdout.toLowerCase().includes('wait') ||
result.stdout.toLowerCase().includes('idle')
assert(hasDescription, 'help should describe what wait does')
console.log('wait command description is helpful\n')
result.stdout.toLowerCase().includes("wait") || result.stdout.toLowerCase().includes("idle");
assert(hasDescription, "help should describe what wait does");
console.log("wait command description is helpful\n");
}
// Test 11: ID prefix syntax is mentioned in help
{
console.log('Test 11: wait command mentions ID')
const result = await $`npx paseo wait --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'wait --help should exit 0')
console.log("Test 11: wait command mentions ID");
const result = await $`npx paseo wait --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "wait --help should exit 0");
const hasIdMention =
result.stdout.toLowerCase().includes('id') ||
result.stdout.toLowerCase().includes('prefix')
assert(hasIdMention, 'help should mention ID or prefix')
console.log('wait command mentions ID\n')
result.stdout.toLowerCase().includes("id") || result.stdout.toLowerCase().includes("prefix");
assert(hasIdMention, "help should mention ID or prefix");
console.log("wait command mentions ID\n");
}
// Test 12: timeout option documents no default limit
{
console.log('Test 12: timeout option documents no default limit')
const result = await $`npx paseo wait --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'wait --help should exit 0')
console.log("Test 12: timeout option documents no default limit");
const result = await $`npx paseo wait --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "wait --help should exit 0");
assert(
result.stdout.toLowerCase().includes('default: no limit'),
'help should mention timeout default is no limit'
)
console.log('timeout option documents no default limit\n')
result.stdout.toLowerCase().includes("default: no limit"),
"help should mention timeout default is no limit",
);
console.log("timeout option documents no default limit\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All wait tests passed ===')
console.log("=== All wait tests passed ===");
+46 -46
View File
@@ -16,92 +16,92 @@
* - permit ls --json handles errors
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Permit LS Command Tests ===\n')
console.log("=== Permit LS Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: permit --help shows subcommands
{
console.log('Test 1: permit --help shows subcommands')
const result = await $`npx paseo permit --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'permit --help should exit 0')
assert(result.stdout.includes('ls'), 'help should mention ls subcommand')
assert(result.stdout.includes('allow'), 'help should mention allow subcommand')
assert(result.stdout.includes('deny'), 'help should mention deny subcommand')
console.log('✓ permit --help shows subcommands\n')
console.log("Test 1: permit --help shows subcommands");
const result = await $`npx paseo permit --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "permit --help should exit 0");
assert(result.stdout.includes("ls"), "help should mention ls subcommand");
assert(result.stdout.includes("allow"), "help should mention allow subcommand");
assert(result.stdout.includes("deny"), "help should mention deny subcommand");
console.log("✓ permit --help shows subcommands\n");
}
// Test 2: permit ls --help shows options
{
console.log('Test 2: permit ls --help shows options')
const result = await $`npx paseo permit ls --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'permit ls --help should exit 0')
assert(result.stdout.includes('--host'), 'help should mention --host option')
console.log('✓ permit ls --help shows options\n')
console.log("Test 2: permit ls --help shows options");
const result = await $`npx paseo permit ls --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "permit ls --help should exit 0");
assert(result.stdout.includes("--host"), "help should mention --host option");
console.log("✓ permit ls --help shows options\n");
}
// Test 3: permit ls returns error when no daemon running
{
console.log('Test 3: permit ls handles daemon not running')
console.log("Test 3: permit ls handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ permit ls handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ permit ls handles daemon not running\n");
}
// Test 4: permit ls --json handles errors
{
console.log('Test 4: permit ls --json handles errors')
console.log("Test 4: permit ls --json handles errors");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls --json`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls --json`.nothrow();
// Should still fail (daemon not running)
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
// But output should be valid JSON if present
const output = result.stdout.trim()
const output = result.stdout.trim();
if (output.length > 0) {
try {
JSON.parse(output)
console.log('✓ permit ls --json outputs valid JSON error\n')
JSON.parse(output);
console.log("✓ permit ls --json outputs valid JSON error\n");
} catch {
// Empty or stderr-only output is acceptable
console.log('✓ permit ls --json handled error (output may be in stderr)\n')
console.log("✓ permit ls --json handled error (output may be in stderr)\n");
}
} else {
console.log('✓ permit ls --json handled error gracefully\n')
console.log("✓ permit ls --json handled error gracefully\n");
}
}
// Test 5: -q (quiet) flag is accepted globally
{
console.log('Test 5: -q (quiet) flag is accepted')
console.log("Test 5: -q (quiet) flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q permit ls`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q permit ls`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All permit ls tests passed ===')
console.log("=== All permit ls tests passed ===");
+84 -84
View File
@@ -21,155 +21,155 @@
* - permit allow --input flag is accepted
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Permit Allow/Deny Command Tests ===\n')
console.log("=== Permit Allow/Deny Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: permit allow --help shows options
{
console.log('Test 1: permit allow --help shows options')
const result = await $`npx paseo permit allow --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'permit allow --help should exit 0')
assert(result.stdout.includes('--all'), 'help should mention --all flag')
assert(result.stdout.includes('--input'), 'help should mention --input option')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<agent>'), 'help should mention agent argument')
console.log('✓ permit allow --help shows options\n')
console.log("Test 1: permit allow --help shows options");
const result = await $`npx paseo permit allow --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "permit allow --help should exit 0");
assert(result.stdout.includes("--all"), "help should mention --all flag");
assert(result.stdout.includes("--input"), "help should mention --input option");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<agent>"), "help should mention agent argument");
console.log("✓ permit allow --help shows options\n");
}
// Test 2: permit deny --help shows options
{
console.log('Test 2: permit deny --help shows options')
const result = await $`npx paseo permit deny --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'permit deny --help should exit 0')
assert(result.stdout.includes('--all'), 'help should mention --all flag')
assert(result.stdout.includes('--message'), 'help should mention --message option')
assert(result.stdout.includes('--interrupt'), 'help should mention --interrupt flag')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<agent>'), 'help should mention agent argument')
console.log('✓ permit deny --help shows options\n')
console.log("Test 2: permit deny --help shows options");
const result = await $`npx paseo permit deny --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "permit deny --help should exit 0");
assert(result.stdout.includes("--all"), "help should mention --all flag");
assert(result.stdout.includes("--message"), "help should mention --message option");
assert(result.stdout.includes("--interrupt"), "help should mention --interrupt flag");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<agent>"), "help should mention agent argument");
console.log("✓ permit deny --help shows options\n");
}
// Test 3: permit allow handles daemon not running
{
console.log('Test 3: permit allow handles daemon not running')
console.log("Test 3: permit allow handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 req456`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 req456`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ permit allow handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ permit allow handles daemon not running\n");
}
// Test 4: permit deny handles daemon not running
{
console.log('Test 4: permit deny handles daemon not running')
console.log("Test 4: permit deny handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ permit deny handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ permit deny handles daemon not running\n");
}
// Test 5: permit allow --all flag is accepted
{
console.log('Test 5: permit allow --all flag is accepted')
console.log("Test 5: permit allow --all flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 --all`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --all flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ permit allow --all flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 --all`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --all flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ permit allow --all flag is accepted\n");
}
// Test 6: permit deny --all flag is accepted
{
console.log('Test 6: permit deny --all flag is accepted')
console.log("Test 6: permit deny --all flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 --all`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --all flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ permit deny --all flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 --all`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --all flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ permit deny --all flag is accepted\n");
}
// Test 7: permit deny --message flag is accepted
{
console.log('Test 7: permit deny --message flag is accepted')
console.log("Test 7: permit deny --message flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456 --message "Not allowed"`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --message flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ permit deny --message flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456 --message "Not allowed"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --message flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ permit deny --message flag is accepted\n");
}
// Test 8: permit deny --interrupt flag is accepted
{
console.log('Test 8: permit deny --interrupt flag is accepted')
console.log("Test 8: permit deny --interrupt flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456 --interrupt`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --interrupt flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ permit deny --interrupt flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456 --interrupt`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --interrupt flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ permit deny --interrupt flag is accepted\n");
}
// Test 9: permit allow --input flag is accepted
{
console.log('Test 9: permit allow --input flag is accepted')
console.log("Test 9: permit allow --input flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 req456 --input '{"key":"value"}'`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --input flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ permit allow --input flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 req456 --input '{"key":"value"}'`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --input flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ permit allow --input flag is accepted\n");
}
// Test 10: permit allow without req_id and without --all fails gracefully
{
console.log('Test 10: permit allow requires req_id or --all')
console.log("Test 10: permit allow requires req_id or --all");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123`.nothrow();
// This might fail due to daemon not running first, or due to missing argument
// The important thing is it doesn't crash with an unhandled error
assert.notStrictEqual(result.exitCode, 0, 'should fail without req_id or --all')
console.log('✓ permit allow requires req_id or --all\n')
assert.notStrictEqual(result.exitCode, 0, "should fail without req_id or --all");
console.log("✓ permit allow requires req_id or --all\n");
}
// Test 11: permit deny without req_id and without --all fails gracefully
{
console.log('Test 11: permit deny requires req_id or --all')
console.log("Test 11: permit deny requires req_id or --all");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without req_id or --all')
console.log('✓ permit deny requires req_id or --all\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without req_id or --all");
console.log("✓ permit deny requires req_id or --all\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All permit allow/deny tests passed ===')
console.log("=== All permit allow/deny tests passed ===");
+84 -84
View File
@@ -18,152 +18,152 @@
* - worktree archive handles daemon not running
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false
$.verbose = false;
console.log('=== Worktree Command Tests ===\n')
console.log("=== Worktree Command Tests ===\n");
// Get random port that's definitely not in use (never 6767)
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: worktree --help shows subcommands
{
console.log('Test 1: worktree --help shows subcommands')
const result = await $`npx paseo worktree --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'worktree --help should exit 0')
assert(result.stdout.includes('ls'), 'help should mention ls subcommand')
assert(result.stdout.includes('archive'), 'help should mention archive subcommand')
console.log('✓ worktree --help shows subcommands\n')
console.log("Test 1: worktree --help shows subcommands");
const result = await $`npx paseo worktree --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "worktree --help should exit 0");
assert(result.stdout.includes("ls"), "help should mention ls subcommand");
assert(result.stdout.includes("archive"), "help should mention archive subcommand");
console.log("✓ worktree --help shows subcommands\n");
}
// Test 2: worktree ls --help shows options
{
console.log('Test 2: worktree ls --help shows options')
const result = await $`npx paseo worktree ls --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'worktree ls --help should exit 0')
assert(result.stdout.includes('--host'), 'help should mention --host option')
console.log('✓ worktree ls --help shows options\n')
console.log("Test 2: worktree ls --help shows options");
const result = await $`npx paseo worktree ls --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "worktree ls --help should exit 0");
assert(result.stdout.includes("--host"), "help should mention --host option");
console.log("✓ worktree ls --help shows options\n");
}
// Test 3: worktree ls handles daemon not running
{
console.log('Test 3: worktree ls handles daemon not running')
console.log("Test 3: worktree ls handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ worktree ls handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ worktree ls handles daemon not running\n");
}
// Test 4: worktree ls with --host flag is accepted
{
console.log('Test 4: worktree ls with --host flag is accepted')
console.log("Test 4: worktree ls with --host flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls --host localhost:${port}`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ worktree ls with --host flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls --host localhost:${port}`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ worktree ls with --host flag is accepted\n");
}
// Test 5: worktree archive --help shows options
{
console.log('Test 5: worktree archive --help shows options')
const result = await $`npx paseo worktree archive --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'worktree archive --help should exit 0')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<name>'), 'help should mention required name argument')
console.log('✓ worktree archive --help shows options\n')
console.log("Test 5: worktree archive --help shows options");
const result = await $`npx paseo worktree archive --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "worktree archive --help should exit 0");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<name>"), "help should mention required name argument");
console.log("✓ worktree archive --help shows options\n");
}
// Test 6: worktree archive requires name argument
{
console.log('Test 6: worktree archive requires name argument')
console.log("Test 6: worktree archive requires name argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without name')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without name");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument')
assert(hasError, 'error should mention missing argument')
console.log('✓ worktree archive requires name argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasError, "error should mention missing argument");
console.log("✓ worktree archive requires name argument\n");
}
// Test 7: worktree archive handles daemon not running
{
console.log('Test 7: worktree archive handles daemon not running')
console.log("Test 7: worktree archive handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive test-worktree`.nothrow()
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive test-worktree`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ worktree archive handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ worktree archive handles daemon not running\n");
}
// Test 8: worktree archive with name and --host flag is accepted
{
console.log('Test 8: worktree archive with name and --host flag is accepted')
console.log("Test 8: worktree archive with name and --host flag is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive test-worktree --host localhost:${port}`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ worktree archive with name and --host flag is accepted\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive test-worktree --host localhost:${port}`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ worktree archive with name and --host flag is accepted\n");
}
// Test 9: -q (quiet) flag is accepted with worktree ls
{
console.log('Test 9: -q (quiet) flag is accepted with worktree ls')
console.log("Test 9: -q (quiet) flag is accepted with worktree ls");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q worktree ls`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with worktree ls\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q worktree ls`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with worktree ls\n");
}
// Test 10: --json flag is accepted with worktree ls
{
console.log('Test 10: --json flag is accepted with worktree ls')
console.log("Test 10: --json flag is accepted with worktree ls");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls --json`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --json flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ --json flag is accepted with worktree ls\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls --json`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --json flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ --json flag is accepted with worktree ls\n");
}
// Test 11: paseo --help shows worktree subcommand
{
console.log('Test 11: paseo --help shows worktree subcommand')
const result = await $`npx paseo --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('worktree'), 'help should mention worktree subcommand')
console.log('✓ paseo --help shows worktree subcommand\n')
console.log("Test 11: paseo --help shows worktree subcommand");
const result = await $`npx paseo --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("worktree"), "help should mention worktree subcommand");
console.log("✓ paseo --help shows worktree subcommand\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All worktree tests passed ===')
console.log("=== All worktree tests passed ===");
+168 -135
View File
@@ -19,230 +19,263 @@
* - provider models --json outputs valid JSON
*/
import assert from 'node:assert'
import { createE2ETestContext } from './helpers/test-daemon.ts'
import assert from "node:assert";
import { createE2ETestContext } from "./helpers/test-daemon.ts";
console.log('=== Provider Commands ===\n')
console.log("=== Provider Commands ===\n");
type ProviderModel = {
model: string
id: string
description?: string
}
model: string;
id: string;
description?: string;
};
const EXPECTED_CLAUDE_MODELS = [
{
id: 'claude-sonnet-4-5-20250929',
model: 'Sonnet 4.5',
descriptionFragment: 'Best for everyday tasks',
id: "claude-sonnet-4-5-20250929",
model: "Sonnet 4.5",
descriptionFragment: "Best for everyday tasks",
},
{
id: 'claude-sonnet-4-6',
model: 'Sonnet 4.6',
descriptionFragment: 'Best for everyday tasks',
id: "claude-sonnet-4-6",
model: "Sonnet 4.6",
descriptionFragment: "Best for everyday tasks",
},
{
id: 'claude-opus-4-6',
model: 'Opus 4.6',
descriptionFragment: 'Most capable',
id: "claude-opus-4-6",
model: "Opus 4.6",
descriptionFragment: "Most capable",
},
{
id: 'claude-haiku-4-5-20251001',
model: 'Haiku 4.5',
descriptionFragment: 'Fastest',
id: "claude-haiku-4-5-20251001",
model: "Haiku 4.5",
descriptionFragment: "Fastest",
},
] as const
] as const;
let claudeModelIdsFromJson: string[] = []
let claudeModelsFromJson: ProviderModel[] = []
let claudeModelIdsFromJson: string[] = [];
let claudeModelsFromJson: ProviderModel[] = [];
const ctx = await createE2ETestContext({ timeout: 120000 })
const ctx = await createE2ETestContext({ timeout: 120000 });
async function runProviderModelsJson(
provider: 'claude' | 'codex' | 'opencode'
provider: "claude" | "codex" | "opencode",
): Promise<ProviderModel[]> {
const transientNeedles = ['transport closed', 'timed out', 'timeout', 'socket', 'econn']
const transientNeedles = ["transport closed", "timed out", "timeout", "socket", "econn"];
for (let attempt = 1; attempt <= 3; attempt++) {
const result = await ctx.paseo(['provider', 'models', provider, '--json'])
const result = await ctx.paseo(["provider", "models", provider, "--json"]);
if (result.exitCode === 0) {
return JSON.parse(result.stdout.trim()) as ProviderModel[]
return JSON.parse(result.stdout.trim()) as ProviderModel[];
}
const combined = `${result.stdout}\n${result.stderr}`
const normalized = combined.toLowerCase()
const isTransient = transientNeedles.some((needle) => normalized.includes(needle))
const combined = `${result.stdout}\n${result.stderr}`;
const normalized = combined.toLowerCase();
const isTransient = transientNeedles.some((needle) => normalized.includes(needle));
if (!isTransient || attempt === 3) {
assert.fail(`provider models ${provider} should exit 0\n${combined}`)
assert.fail(`provider models ${provider} should exit 0\n${combined}`);
}
await new Promise((resolve) => setTimeout(resolve, 250 * attempt))
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
}
assert.fail(`provider models ${provider} exhausted retries`)
assert.fail(`provider models ${provider} exhausted retries`);
}
function assertClaudeModels(data: ProviderModel[]): void {
assert.strictEqual(data.length, EXPECTED_CLAUDE_MODELS.length, 'claude output should match the current catalog size')
assert.strictEqual(
data.length,
EXPECTED_CLAUDE_MODELS.length,
"claude output should match the current catalog size",
);
const byId = new Map(data.map((model) => [model.id, model]))
const ids = [...byId.keys()].sort()
const expectedIds = EXPECTED_CLAUDE_MODELS.map((model) => model.id).sort()
const byId = new Map(data.map((model) => [model.id, model]));
const ids = [...byId.keys()].sort();
const expectedIds = EXPECTED_CLAUDE_MODELS.map((model) => model.id).sort();
assert.strictEqual(byId.size, data.length, 'claude model IDs should be unique')
assert.deepStrictEqual(ids, expectedIds, 'claude IDs should match the current catalog')
assert.strictEqual(byId.size, data.length, "claude model IDs should be unique");
assert.deepStrictEqual(ids, expectedIds, "claude IDs should match the current catalog");
for (const expectedModel of EXPECTED_CLAUDE_MODELS) {
const actualModel = byId.get(expectedModel.id)
assert(actualModel, `claude output should include ${expectedModel.id}`)
assert.strictEqual(actualModel.model, expectedModel.model, `${expectedModel.id} should keep its display name`)
const actualModel = byId.get(expectedModel.id);
assert(actualModel, `claude output should include ${expectedModel.id}`);
assert.strictEqual(
actualModel.model,
expectedModel.model,
`${expectedModel.id} should keep its display name`,
);
assert(
(actualModel.description ?? '').includes(expectedModel.descriptionFragment),
`${expectedModel.id} description should mention ${expectedModel.descriptionFragment}`
)
(actualModel.description ?? "").includes(expectedModel.descriptionFragment),
`${expectedModel.id} description should mention ${expectedModel.descriptionFragment}`,
);
}
}
try {
// Test 1: provider --help shows subcommands
{
console.log('Test 1: provider --help shows subcommands')
const result = await ctx.paseo(['provider', '--help'])
assert.strictEqual(result.exitCode, 0, 'provider --help should exit 0')
assert(result.stdout.includes('ls'), 'help should mention ls')
assert(result.stdout.includes('models'), 'help should mention models')
console.log('✓ provider --help shows subcommands\n')
console.log("Test 1: provider --help shows subcommands");
const result = await ctx.paseo(["provider", "--help"]);
assert.strictEqual(result.exitCode, 0, "provider --help should exit 0");
assert(result.stdout.includes("ls"), "help should mention ls");
assert(result.stdout.includes("models"), "help should mention models");
console.log("✓ provider --help shows subcommands\n");
}
// Test 2: provider ls lists all providers
{
console.log('Test 2: provider ls lists all providers')
const result = await ctx.paseo(['provider', 'ls'])
assert.strictEqual(result.exitCode, 0, 'provider ls should exit 0')
assert(result.stdout.includes('claude'), 'output should include claude')
assert(result.stdout.includes('codex'), 'output should include codex')
assert(result.stdout.includes('opencode'), 'output should include opencode')
assert(result.stdout.includes('available'), 'output should show available status')
console.log('✓ provider ls lists all providers\n')
console.log("Test 2: provider ls lists all providers");
const result = await ctx.paseo(["provider", "ls"]);
assert.strictEqual(result.exitCode, 0, "provider ls should exit 0");
assert(result.stdout.includes("claude"), "output should include claude");
assert(result.stdout.includes("codex"), "output should include codex");
assert(result.stdout.includes("opencode"), "output should include opencode");
assert(result.stdout.includes("available"), "output should show available status");
console.log("✓ provider ls lists all providers\n");
}
// Test 3: provider ls --json outputs valid JSON
{
console.log('Test 3: provider ls --json outputs valid JSON')
const result = await ctx.paseo(['provider', 'ls', '--json'])
assert.strictEqual(result.exitCode, 0, 'should exit 0')
const data = JSON.parse(result.stdout.trim())
assert(Array.isArray(data), 'output should be an array')
assert.strictEqual(data.length, 3, 'should have 3 providers')
assert(data.some((p: { provider: string }) => p.provider === 'claude'), 'should include claude')
assert(data.some((p: { provider: string }) => p.provider === 'codex'), 'should include codex')
assert(data.some((p: { provider: string }) => p.provider === 'opencode'), 'should include opencode')
console.log('✓ provider ls --json outputs valid JSON\n')
console.log("Test 3: provider ls --json outputs valid JSON");
const result = await ctx.paseo(["provider", "ls", "--json"]);
assert.strictEqual(result.exitCode, 0, "should exit 0");
const data = JSON.parse(result.stdout.trim());
assert(Array.isArray(data), "output should be an array");
assert.strictEqual(data.length, 3, "should have 3 providers");
assert(
data.some((p: { provider: string }) => p.provider === "claude"),
"should include claude",
);
assert(
data.some((p: { provider: string }) => p.provider === "codex"),
"should include codex",
);
assert(
data.some((p: { provider: string }) => p.provider === "opencode"),
"should include opencode",
);
console.log("✓ provider ls --json outputs valid JSON\n");
}
// Test 4: provider ls --quiet outputs provider names only
{
console.log('Test 4: provider ls --quiet outputs provider names only')
const result = await ctx.paseo(['provider', 'ls', '--quiet'])
assert.strictEqual(result.exitCode, 0, 'should exit 0')
const lines = result.stdout.trim().split('\n')
assert.strictEqual(lines.length, 3, 'should have 3 lines')
assert(lines.includes('claude'), 'should include claude')
assert(lines.includes('codex'), 'should include codex')
assert(lines.includes('opencode'), 'should include opencode')
console.log('✓ provider ls --quiet outputs provider names only\n')
console.log("Test 4: provider ls --quiet outputs provider names only");
const result = await ctx.paseo(["provider", "ls", "--quiet"]);
assert.strictEqual(result.exitCode, 0, "should exit 0");
const lines = result.stdout.trim().split("\n");
assert.strictEqual(lines.length, 3, "should have 3 lines");
assert(lines.includes("claude"), "should include claude");
assert(lines.includes("codex"), "should include codex");
assert(lines.includes("opencode"), "should include opencode");
console.log("✓ provider ls --quiet outputs provider names only\n");
}
// Test 5: provider models claude lists canonical model aliases
{
console.log('Test 5: provider models claude lists canonical model aliases')
const data = await runProviderModelsJson('claude')
assertClaudeModels(data)
console.log('✓ provider models claude lists canonical model aliases\n')
console.log("Test 5: provider models claude lists canonical model aliases");
const data = await runProviderModelsJson("claude");
assertClaudeModels(data);
console.log("✓ provider models claude lists canonical model aliases\n");
}
// Test 6: provider models codex includes concrete codex model IDs
{
console.log('Test 6: provider models codex includes concrete codex model IDs')
const data = await runProviderModelsJson('codex')
assert(data.length >= 6, 'codex model list should include current codex lineup')
const ids = data.map((m) => m.id)
assert.strictEqual(new Set(ids).size, ids.length, 'codex model IDs should be unique')
assert(ids.includes('gpt-5.3-codex'), 'codex output should include gpt-5.3-codex')
assert(ids.includes('gpt-5.3-codex-spark'), 'codex output should include gpt-5.3-codex-spark')
assert(ids.includes('gpt-5.1-codex-max'), 'codex output should include gpt-5.1-codex-max')
assert(ids.includes('gpt-5.1-codex-mini'), 'codex output should include gpt-5.1-codex-mini')
console.log('✓ provider models codex includes concrete codex model IDs\n')
console.log("Test 6: provider models codex includes concrete codex model IDs");
const data = await runProviderModelsJson("codex");
assert(data.length >= 6, "codex model list should include current codex lineup");
const ids = data.map((m) => m.id);
assert.strictEqual(new Set(ids).size, ids.length, "codex model IDs should be unique");
assert(ids.includes("gpt-5.3-codex"), "codex output should include gpt-5.3-codex");
assert(ids.includes("gpt-5.3-codex-spark"), "codex output should include gpt-5.3-codex-spark");
assert(ids.includes("gpt-5.1-codex-max"), "codex output should include gpt-5.1-codex-max");
assert(ids.includes("gpt-5.1-codex-mini"), "codex output should include gpt-5.1-codex-mini");
console.log("✓ provider models codex includes concrete codex model IDs\n");
}
// Test 7: provider models opencode returns namespaced model IDs
{
console.log('Test 7: provider models opencode returns namespaced model IDs')
const data = await runProviderModelsJson('opencode')
assert(data.length >= 3, 'opencode model list should not be empty')
const ids = data.map((m) => m.id)
assert(data.every((m) => m.id.includes('/')), 'opencode model IDs should be provider-namespaced')
assert(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')
console.log("Test 7: provider models opencode returns namespaced model IDs");
const data = await runProviderModelsJson("opencode");
assert(data.length >= 3, "opencode model list should not be empty");
const ids = data.map((m) => m.id);
assert(
ids.includes('openai/gpt-5.3-codex-spark'),
'opencode output should include openai/gpt-5.3-codex-spark'
)
console.log('✓ provider models opencode returns namespaced model IDs\n')
data.every((m) => m.id.includes("/")),
"opencode model IDs should be provider-namespaced",
);
assert(
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",
);
console.log("✓ provider models opencode returns namespaced model IDs\n");
}
// Test 8: provider models unknown fails with error
{
console.log('Test 8: provider models unknown fails with error')
const result = await ctx.paseo(['provider', 'models', 'unknown'])
assert.notStrictEqual(result.exitCode, 0, 'should fail for unknown provider')
const output = result.stdout + result.stderr
console.log("Test 8: provider models unknown fails with error");
const result = await ctx.paseo(["provider", "models", "unknown"]);
assert.notStrictEqual(result.exitCode, 0, "should fail for unknown provider");
const output = result.stdout + result.stderr;
assert(
output.toLowerCase().includes('unknown') || output.toLowerCase().includes('provider'),
'error should mention unknown provider'
)
console.log('✓ provider models unknown fails with error\n')
output.toLowerCase().includes("unknown") || output.toLowerCase().includes("provider"),
"error should mention unknown provider",
);
console.log("✓ provider models unknown fails with error\n");
}
// Test 9: provider models --json outputs valid JSON
{
console.log('Test 9: provider models --json outputs valid JSON')
const data = await runProviderModelsJson('claude')
assert(Array.isArray(data), 'output should be an array')
assert(data.every((m) => m.model && m.id), 'each model should have name and id')
assertClaudeModels(data)
claudeModelIdsFromJson = data.map((m) => m.id)
claudeModelsFromJson = data
console.log('✓ provider models --json outputs valid JSON\n')
console.log("Test 9: provider models --json outputs valid JSON");
const data = await runProviderModelsJson("claude");
assert(Array.isArray(data), "output should be an array");
assert(
data.every((m) => m.model && m.id),
"each model should have name and id",
);
assertClaudeModels(data);
claudeModelIdsFromJson = data.map((m) => m.id);
claudeModelsFromJson = data;
console.log("✓ provider models --json outputs valid JSON\n");
}
// Test 10: provider models --quiet outputs model IDs only
{
console.log('Test 10: provider models --quiet outputs model IDs only')
assert(claudeModelIdsFromJson.length > 0, 'claude model IDs should be captured from --json output')
const result = await ctx.paseo(['provider', 'models', 'claude', '--quiet'])
assert.strictEqual(result.exitCode, 0, 'should exit 0')
const lines = result.stdout.trim().split('\n').filter(Boolean)
assert.strictEqual(lines.length, EXPECTED_CLAUDE_MODELS.length, 'should have one line per Claude catalog model')
console.log("Test 10: provider models --quiet outputs model IDs only");
assert(
claudeModelIdsFromJson.length > 0,
"claude model IDs should be captured from --json output",
);
const result = await ctx.paseo(["provider", "models", "claude", "--quiet"]);
assert.strictEqual(result.exitCode, 0, "should exit 0");
const lines = result.stdout.trim().split("\n").filter(Boolean);
assert.strictEqual(
lines.length,
EXPECTED_CLAUDE_MODELS.length,
"should have one line per Claude catalog model",
);
assert.deepStrictEqual(
[...lines].sort(),
[...claudeModelIdsFromJson].sort(),
'--quiet should print the same model IDs returned by --json'
)
"--quiet should print the same model IDs returned by --json",
);
assert.deepStrictEqual(
[...lines].sort(),
EXPECTED_CLAUDE_MODELS.map((model) => model.id).sort(),
'--quiet should print the current Claude catalog IDs'
)
"--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'
)
console.log('✓ provider models --quiet outputs model IDs only\n')
claudeModelsFromJson.some((m) => m.id === "claude-sonnet-4-5-20250929"),
"captured --json output should still include the Claude default model id",
);
console.log("✓ provider models --quiet outputs model IDs only\n");
}
} finally {
await ctx.stop()
await ctx.stop();
}
console.log('=== All provider tests passed ===')
console.log("=== All provider tests passed ===");
+59 -60
View File
@@ -10,103 +10,102 @@
* - Graceful daemon connection errors
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { getAvailablePort } from './helpers/network.ts'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false
$.verbose = false;
console.log('=== Agent Update Command Tests ===\n')
console.log("=== Agent Update Command Tests ===\n");
// Resolve an available local port so daemon-not-running checks stay deterministic.
const port = await getAvailablePort()
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const port = await getAvailablePort();
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: agent update --help shows options
{
console.log('Test 1: agent update --help shows options')
const result = await $`npx paseo agent update --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'agent update --help should exit 0')
assert(result.stdout.includes('--name'), 'help should mention --name flag')
assert(result.stdout.includes('--label'), 'help should mention --label flag')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('<id>'), 'help should mention required id argument')
console.log('✓ agent update --help shows options\n')
console.log("Test 1: agent update --help shows options");
const result = await $`npx paseo agent update --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent update --help should exit 0");
assert(result.stdout.includes("--name"), "help should mention --name flag");
assert(result.stdout.includes("--label"), "help should mention --label flag");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<id>"), "help should mention required id argument");
console.log("✓ agent update --help shows options\n");
}
// Test 2: agent update requires ID argument
{
console.log('Test 2: agent update requires ID argument')
console.log("Test 2: agent update requires ID argument");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update --name "New Name"`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without id')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update --name "New Name"`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument') ||
output.toLowerCase().includes('id')
assert(hasError, 'error should mention missing argument')
console.log('✓ agent update requires ID argument\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument") ||
output.toLowerCase().includes("id");
assert(hasError, "error should mention missing argument");
console.log("✓ agent update requires ID argument\n");
}
// Test 3: agent update requires at least one update field
{
console.log('Test 3: agent update requires update field')
console.log("Test 3: agent update requires update field");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail without --name/--label')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without --name/--label");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('nothing to update') ||
output.toLowerCase().includes('name') ||
output.toLowerCase().includes('label')
assert(hasError, 'error should mention missing update fields')
console.log('✓ agent update requires update field\n')
output.toLowerCase().includes("nothing to update") ||
output.toLowerCase().includes("name") ||
output.toLowerCase().includes("label");
assert(hasError, "error should mention missing update fields");
console.log("✓ agent update requires update field\n");
}
// Test 4: agent update handles daemon not running
{
console.log('Test 4: agent update handles daemon not running')
console.log("Test 4: agent update handles daemon not running");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --name "Renamed Agent"`.nothrow()
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --name "Renamed Agent"`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error should mention connection issue')
console.log('✓ agent update handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error should mention connection issue");
console.log("✓ agent update handles daemon not running\n");
}
// Test 5: agent update accepts multiple/comma-separated labels
{
console.log('Test 5: agent update accepts multi-label syntax')
console.log("Test 5: agent update accepts multi-label syntax");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --label surface=workspace,area=frontend --label priority=high`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --label flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ agent update accepts multi-label syntax\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --label surface=workspace,area=frontend --label priority=high`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --label flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ agent update accepts multi-label syntax\n");
}
// Test 6: agent --help shows update subcommand
{
console.log('Test 6: agent --help shows update subcommand')
const result = await $`npx paseo agent --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'agent --help should exit 0')
assert(result.stdout.includes('update'), 'help should mention update subcommand')
console.log('✓ agent --help shows update subcommand\n')
console.log("Test 6: agent --help shows update subcommand");
const result = await $`npx paseo agent --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent --help should exit 0");
assert(result.stdout.includes("update"), "help should mention update subcommand");
console.log("✓ agent --help shows update subcommand\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All agent update tests passed ===')
console.log("=== All agent update tests passed ===");
+63 -43
View File
@@ -1,65 +1,85 @@
#!/usr/bin/env npx tsx
import assert from 'node:assert'
import { readFile, mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { $ } from 'zx'
import { getAvailablePort } from './helpers/network.ts'
import assert from "node:assert";
import { readFile, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false
$.verbose = false;
console.log('=== Onboarding Command ===\n')
console.log("=== Onboarding Command ===\n");
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-onboard-home-'))
const port = await getAvailablePort()
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-onboard-home-"));
const port = await getAvailablePort();
try {
console.log('Test 1: `paseo` runs blocking onboarding and prints pairing info')
console.log("Test 1: `paseo` runs blocking onboarding and prints pairing info");
const onboard =
await $`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} PASEO_PAIRING_QR=0 npx paseo`.nothrow()
await $`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} PASEO_PAIRING_QR=0 npx paseo`.nothrow();
assert.strictEqual(
onboard.exitCode,
0,
`onboard should succeed:\nstdout:\n${onboard.stdout}\nstderr:\n${onboard.stderr}`
)
assert(onboard.stdout.includes('Scan to pair'), 'onboard output should include scan header')
assert(onboard.stdout.includes('Pairing link'), 'onboard output should include pairing link header')
assert(onboard.stdout.includes('#offer='), 'onboard output should include pairing offer URL')
assert(onboard.stdout.includes('CLI quick reference'), 'onboard output should include CLI quick reference')
assert(onboard.stdout.includes('paseo --help'), 'onboard output should include --help shortcut')
assert(onboard.stdout.includes('paseo ls'), 'onboard output should include ls shortcut')
assert(onboard.stdout.includes('paseo run "your prompt"'), 'onboard output should include run shortcut')
assert(onboard.stdout.includes('paseo status'), 'onboard output should include status shortcut')
assert(onboard.stdout.includes(join(paseoHome, 'daemon.log')), 'onboard output should include daemon log path')
`onboard should succeed:\nstdout:\n${onboard.stdout}\nstderr:\n${onboard.stderr}`,
);
assert(onboard.stdout.includes("Scan to pair"), "onboard output should include scan header");
assert(
onboard.stdout.includes("Pairing link"),
"onboard output should include pairing link header",
);
assert(onboard.stdout.includes("#offer="), "onboard output should include pairing offer URL");
assert(
onboard.stdout.includes("CLI quick reference"),
"onboard output should include CLI quick reference",
);
assert(onboard.stdout.includes("paseo --help"), "onboard output should include --help shortcut");
assert(onboard.stdout.includes("paseo ls"), "onboard output should include ls shortcut");
assert(
onboard.stdout.includes('paseo run "your prompt"'),
"onboard output should include run shortcut",
);
assert(onboard.stdout.includes("paseo status"), "onboard output should include status shortcut");
assert(
onboard.stdout.includes(join(paseoHome, "daemon.log")),
"onboard output should include daemon log path",
);
const status =
await $`PASEO_HOME=${paseoHome} npx paseo daemon status --home ${paseoHome}`.nothrow()
assert.strictEqual(status.exitCode, 0, `daemon status should succeed: ${status.stderr}`)
assert(status.stdout.includes('running'), 'daemon should be running when onboarding exits')
console.log('✓ onboarding prints pairing info and waits for daemon readiness\n')
await $`PASEO_HOME=${paseoHome} npx paseo daemon status --home ${paseoHome}`.nothrow();
assert.strictEqual(status.exitCode, 0, `daemon status should succeed: ${status.stderr}`);
assert(status.stdout.includes("running"), "daemon should be running when onboarding exits");
console.log("✓ onboarding prints pairing info and waits for daemon readiness\n");
console.log('Test 2: non-interactive onboarding persists voice disabled config')
const configRaw = await readFile(join(paseoHome, 'config.json'), 'utf-8')
console.log("Test 2: non-interactive onboarding persists voice disabled config");
const configRaw = await readFile(join(paseoHome, "config.json"), "utf-8");
const config = JSON.parse(configRaw) as {
features?: {
dictation?: { enabled?: boolean }
voiceMode?: { enabled?: boolean }
}
}
dictation?: { enabled?: boolean };
voiceMode?: { enabled?: boolean };
};
};
assert.strictEqual(config.features?.dictation?.enabled, false, 'dictation.enabled should be false')
assert.strictEqual(config.features?.voiceMode?.enabled, false, 'voiceMode.enabled should be false')
const daemonLog = await readFile(join(paseoHome, 'daemon.log'), 'utf-8')
assert.strictEqual(
config.features?.dictation?.enabled,
false,
"dictation.enabled should be false",
);
assert.strictEqual(
config.features?.voiceMode?.enabled,
false,
"voiceMode.enabled should be false",
);
const daemonLog = await readFile(join(paseoHome, "daemon.log"), "utf-8");
assert(
!daemonLog.includes('Ensuring local speech models'),
'daemon should not attempt local speech model setup when voice is disabled'
)
console.log('✓ non-interactive run persisted voice disabled choices\n')
!daemonLog.includes("Ensuring local speech models"),
"daemon should not attempt local speech model setup when voice is disabled",
);
console.log("✓ non-interactive run persisted voice disabled choices\n");
} finally {
await $`PASEO_HOME=${paseoHome} npx paseo daemon stop --home ${paseoHome} --force`.nothrow()
await rm(paseoHome, { recursive: true, force: true })
await $`PASEO_HOME=${paseoHome} npx paseo daemon stop --home ${paseoHome} --force`.nothrow();
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== Onboarding tests passed ===')
console.log("=== Onboarding tests passed ===");
@@ -6,40 +6,40 @@
* Tests pure helpers that do not require a running daemon.
*/
import assert from 'node:assert'
import { resolveTcpHostFromListen } from '../src/commands/daemon/local-daemon.js'
import assert from "node:assert";
import { resolveTcpHostFromListen } from "../src/commands/daemon/local-daemon.js";
console.log('=== Local Daemon Utility Helpers ===\n')
console.log("=== Local Daemon Utility Helpers ===\n");
{
console.log('Test 1: resolves numeric listen values to localhost host:port')
assert.strictEqual(resolveTcpHostFromListen('6767'), '127.0.0.1:6767')
assert.strictEqual(resolveTcpHostFromListen(' 7777 '), '127.0.0.1:7777')
console.log('✓ resolves numeric listen values\n')
console.log("Test 1: resolves numeric listen values to localhost host:port");
assert.strictEqual(resolveTcpHostFromListen("6767"), "127.0.0.1:6767");
assert.strictEqual(resolveTcpHostFromListen(" 7777 "), "127.0.0.1:7777");
console.log("✓ resolves numeric listen values\n");
}
{
console.log('Test 2: preserves explicit host:port listen values')
assert.strictEqual(resolveTcpHostFromListen('localhost:6767'), 'localhost:6767')
assert.strictEqual(resolveTcpHostFromListen('0.0.0.0:8080'), '0.0.0.0:8080')
console.log('✓ preserves explicit host:port values\n')
console.log("Test 2: preserves explicit host:port listen values");
assert.strictEqual(resolveTcpHostFromListen("localhost:6767"), "localhost:6767");
assert.strictEqual(resolveTcpHostFromListen("0.0.0.0:8080"), "0.0.0.0:8080");
console.log("✓ preserves explicit host:port values\n");
}
{
console.log('Test 3: rejects unix socket listen values')
assert.strictEqual(resolveTcpHostFromListen('/tmp/paseo.sock'), null)
assert.strictEqual(resolveTcpHostFromListen('unix:///tmp/paseo.sock'), null)
assert.strictEqual(resolveTcpHostFromListen('pipe://\\\\.\\pipe\\paseo-managed-test'), null)
assert.strictEqual(resolveTcpHostFromListen('\\\\.\\pipe\\paseo-managed-test'), null)
console.log('✓ rejects unix socket listen values\n')
console.log("Test 3: rejects unix socket listen values");
assert.strictEqual(resolveTcpHostFromListen("/tmp/paseo.sock"), null);
assert.strictEqual(resolveTcpHostFromListen("unix:///tmp/paseo.sock"), null);
assert.strictEqual(resolveTcpHostFromListen("pipe://\\\\.\\pipe\\paseo-managed-test"), null);
assert.strictEqual(resolveTcpHostFromListen("\\\\.\\pipe\\paseo-managed-test"), null);
console.log("✓ rejects unix socket listen values\n");
}
{
console.log('Test 4: rejects empty and non-host listen values')
assert.strictEqual(resolveTcpHostFromListen(''), null)
assert.strictEqual(resolveTcpHostFromListen(' '), null)
assert.strictEqual(resolveTcpHostFromListen('localhost'), null)
console.log('✓ rejects empty and non-host listen values\n')
console.log("Test 4: rejects empty and non-host listen values");
assert.strictEqual(resolveTcpHostFromListen(""), null);
assert.strictEqual(resolveTcpHostFromListen(" "), null);
assert.strictEqual(resolveTcpHostFromListen("localhost"), null);
console.log("✓ rejects empty and non-host listen values\n");
}
console.log('=== All local daemon utility tests passed ===')
console.log("=== All local daemon utility tests passed ===");
+12 -12
View File
@@ -1,22 +1,22 @@
#!/usr/bin/env npx tsx
import assert from 'node:assert'
import { getErrorMessage } from '../src/utils/errors.js'
import assert from "node:assert";
import { getErrorMessage } from "../src/utils/errors.js";
console.log('=== Error Utils ===\n')
console.log("=== Error Utils ===\n");
{
console.log('Test 1: returns Error.message for Error instances')
assert.strictEqual(getErrorMessage(new Error('boom')), 'boom')
console.log('✓ returns Error.message\n')
console.log("Test 1: returns Error.message for Error instances");
assert.strictEqual(getErrorMessage(new Error("boom")), "boom");
console.log("✓ returns Error.message\n");
}
{
console.log('Test 2: stringifies non-Error values')
assert.strictEqual(getErrorMessage('plain string'), 'plain string')
assert.strictEqual(getErrorMessage(42), '42')
assert.strictEqual(getErrorMessage(null), 'null')
console.log('✓ stringifies non-Error values\n')
console.log("Test 2: stringifies non-Error values");
assert.strictEqual(getErrorMessage("plain string"), "plain string");
assert.strictEqual(getErrorMessage(42), "42");
assert.strictEqual(getErrorMessage(null), "null");
console.log("✓ stringifies non-Error values\n");
}
console.log('=== All error utility tests passed ===')
console.log("=== All error utility tests passed ===");
+19 -19
View File
@@ -1,38 +1,38 @@
#!/usr/bin/env npx tsx
import assert from 'node:assert'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { resolvePaseoHomePath, resolvePaseoWorktreesDir } from '../src/commands/worktree/ls.js'
import assert from "node:assert";
import { homedir } from "node:os";
import { join } from "node:path";
import { resolvePaseoHomePath, resolvePaseoWorktreesDir } from "../src/commands/worktree/ls.js";
console.log('=== Worktree LS Path Helper Tests ===\n')
console.log("=== Worktree LS Path Helper Tests ===\n");
const originalPaseoHome = process.env.PASEO_HOME
const originalPaseoHome = process.env.PASEO_HOME;
try {
{
console.log('Test 1: resolves explicit PASEO_HOME when set')
process.env.PASEO_HOME = '/tmp/paseo-explicit-home'
console.log("Test 1: resolves explicit PASEO_HOME when set");
process.env.PASEO_HOME = "/tmp/paseo-explicit-home";
assert.strictEqual(resolvePaseoHomePath(), '/tmp/paseo-explicit-home')
assert.strictEqual(resolvePaseoWorktreesDir(), '/tmp/paseo-explicit-home/worktrees')
console.log('\u2713 explicit PASEO_HOME is respected\n')
assert.strictEqual(resolvePaseoHomePath(), "/tmp/paseo-explicit-home");
assert.strictEqual(resolvePaseoWorktreesDir(), "/tmp/paseo-explicit-home/worktrees");
console.log("\u2713 explicit PASEO_HOME is respected\n");
}
{
console.log('Test 2: falls back to homedir/.paseo when PASEO_HOME is unset')
delete process.env.PASEO_HOME
console.log("Test 2: falls back to homedir/.paseo when PASEO_HOME is unset");
delete process.env.PASEO_HOME;
assert.strictEqual(resolvePaseoHomePath(), join(homedir(), '.paseo'))
assert.strictEqual(resolvePaseoWorktreesDir(), join(homedir(), '.paseo', 'worktrees'))
console.log('\u2713 fallback home path is derived from os.homedir()\n')
assert.strictEqual(resolvePaseoHomePath(), join(homedir(), ".paseo"));
assert.strictEqual(resolvePaseoWorktreesDir(), join(homedir(), ".paseo", "worktrees"));
console.log("\u2713 fallback home path is derived from os.homedir()\n");
}
} finally {
if (originalPaseoHome === undefined) {
delete process.env.PASEO_HOME
delete process.env.PASEO_HOME;
} else {
process.env.PASEO_HOME = originalPaseoHome
process.env.PASEO_HOME = originalPaseoHome;
}
}
console.log('=== All worktree ls path helper tests passed ===')
console.log("=== All worktree ls path helper tests passed ===");
@@ -1,126 +1,126 @@
#!/usr/bin/env npx tsx
import assert from 'node:assert'
import assert from "node:assert";
import {
resolveProviderAndModel,
resolveStructuredResponseMessage,
type StructuredResponseTimelineClient,
} from '../src/commands/agent/run.ts'
} from "../src/commands/agent/run.ts";
type TimelineEntry = {
item: {
type: string
text?: string
}
}
type: string;
text?: string;
};
};
function createClient(options: {
entries?: TimelineEntry[]
throwOnFetch?: boolean
onFetch?: () => void
entries?: TimelineEntry[];
throwOnFetch?: boolean;
onFetch?: () => void;
}): StructuredResponseTimelineClient {
return {
fetchAgentTimeline: async () => {
options.onFetch?.()
options.onFetch?.();
if (options.throwOnFetch) {
throw new Error('timeline unavailable')
throw new Error("timeline unavailable");
}
return {
entries: options.entries ?? [],
} as Awaited<ReturnType<StructuredResponseTimelineClient['fetchAgentTimeline']>>
} as Awaited<ReturnType<StructuredResponseTimelineClient["fetchAgentTimeline"]>>;
},
}
};
}
console.log('=== Run Output Schema Helper Tests ===\n')
console.log("=== Run Output Schema Helper Tests ===\n");
// Test 1: Direct lastMessage is returned without timeline fetch.
{
let fetchCount = 0
const client = createClient({ onFetch: () => fetchCount += 1 })
let fetchCount = 0;
const client = createClient({ onFetch: () => (fetchCount += 1) });
const result = await resolveStructuredResponseMessage({
client,
agentId: 'agent-1',
agentId: "agent-1",
lastMessage: ' {"summary":"ok"} ',
})
assert.strictEqual(result, '{"summary":"ok"}')
assert.strictEqual(fetchCount, 0, 'should not fetch timeline when lastMessage exists')
console.log('✓ returns direct lastMessage when present')
});
assert.strictEqual(result, '{"summary":"ok"}');
assert.strictEqual(fetchCount, 0, "should not fetch timeline when lastMessage exists");
console.log("✓ returns direct lastMessage when present");
}
// Test 2: Falls back to latest assistant message from timeline.
{
const client = createClient({
entries: [
{ item: { type: 'user_message', text: 'prompt' } },
{ item: { type: 'assistant_message', text: '' } },
{ item: { type: 'assistant_message', text: ' {"summary":"from timeline"} ' } },
{ item: { type: "user_message", text: "prompt" } },
{ item: { type: "assistant_message", text: "" } },
{ item: { type: "assistant_message", text: ' {"summary":"from timeline"} ' } },
],
})
});
const result = await resolveStructuredResponseMessage({
client,
agentId: 'agent-2',
agentId: "agent-2",
lastMessage: null,
})
assert.strictEqual(result, '{"summary":"from timeline"}')
console.log('✓ falls back to latest assistant timeline entry')
});
assert.strictEqual(result, '{"summary":"from timeline"}');
console.log("✓ falls back to latest assistant timeline entry");
}
// Test 3: Returns null when timeline has no assistant messages.
{
const client = createClient({
entries: [
{ item: { type: 'user_message', text: 'prompt' } },
{ item: { type: 'reasoning', text: 'thinking' } },
{ item: { type: "user_message", text: "prompt" } },
{ item: { type: "reasoning", text: "thinking" } },
],
})
});
const result = await resolveStructuredResponseMessage({
client,
agentId: 'agent-3',
agentId: "agent-3",
lastMessage: null,
})
assert.strictEqual(result, null)
console.log('✓ returns null when no assistant messages exist')
});
assert.strictEqual(result, null);
console.log("✓ returns null when no assistant messages exist");
}
// Test 4: Returns null if timeline fetch fails.
{
const client = createClient({ throwOnFetch: true })
const client = createClient({ throwOnFetch: true });
const result = await resolveStructuredResponseMessage({
client,
agentId: 'agent-4',
agentId: "agent-4",
lastMessage: null,
})
assert.strictEqual(result, null)
console.log('✓ returns null when timeline fetch throws')
});
assert.strictEqual(result, null);
console.log("✓ returns null when timeline fetch throws");
}
// Test 5: Provider/model slash syntax resolves both values.
{
const result = resolveProviderAndModel({ provider: 'codex/gpt-5.4' })
const result = resolveProviderAndModel({ provider: "codex/gpt-5.4" });
assert.deepStrictEqual(result, {
provider: 'codex',
model: 'gpt-5.4',
})
console.log('✓ resolves provider/model slash syntax')
provider: "codex",
model: "gpt-5.4",
});
console.log("✓ resolves provider/model slash syntax");
}
// Test 6: Explicit matching --model coexists with slash syntax.
{
const result = resolveProviderAndModel({ provider: 'codex/gpt-5.4', model: 'gpt-5.4' })
const result = resolveProviderAndModel({ provider: "codex/gpt-5.4", model: "gpt-5.4" });
assert.deepStrictEqual(result, {
provider: 'codex',
model: 'gpt-5.4',
})
console.log('✓ accepts matching explicit model with slash syntax')
provider: "codex",
model: "gpt-5.4",
});
console.log("✓ accepts matching explicit model with slash syntax");
}
// Test 7: Conflicting --model is rejected.
{
assert.throws(() => resolveProviderAndModel({ provider: 'codex/gpt-5.4', model: 'gpt-5.5' }), {
message: 'Conflicting model values provided',
})
console.log('✓ rejects conflicting explicit model with slash syntax')
assert.throws(() => resolveProviderAndModel({ provider: "codex/gpt-5.4", model: "gpt-5.5" }), {
message: "Conflicting model values provided",
});
console.log("✓ rejects conflicting explicit model with slash syntax");
}
console.log('\n=== All helper tests passed ===')
console.log("\n=== All helper tests passed ===");
@@ -5,223 +5,235 @@
* without allowing daemon-runner to respawn a new worker process.
*/
import assert from 'node:assert'
import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { $ } from 'zx'
import { getAvailablePort } from './helpers/network.ts'
import assert from "node:assert";
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false
$.verbose = false;
const pollIntervalMs = 100
const pollIntervalMs = 100;
const testEnv = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isProcessRunning(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false
return false;
}
try {
process.kill(pid, 0)
return true
process.kill(pid, 0);
return true;
} catch {
return false
return false;
}
}
type PidLockState = {
pid: number | null
}
pid: number | null;
};
async function readPidLockState(paseoHome: string): Promise<PidLockState> {
const pidPath = join(paseoHome, 'paseo.pid')
const pidPath = join(paseoHome, "paseo.pid");
try {
const content = await readFile(pidPath, 'utf-8')
const parsed = JSON.parse(content) as { pid?: unknown }
const pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null
return { pid }
const content = await readFile(pidPath, "utf-8");
const parsed = JSON.parse(content) as { pid?: unknown };
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { pid };
} catch {
return { pid: null }
return { pid: null };
}
}
function readProcessCommand(pid: number): string | null {
const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8' })
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
if (result.status !== 0 || result.error) {
return null
return null;
}
const command = result.stdout.trim()
return command.length > 0 ? command : null
const command = result.stdout.trim();
return command.length > 0 ? command : null;
}
type DaemonStatus = {
status: string | null
pid: number | null
}
status: string | null;
pid: number | null;
};
async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow()
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null }
return { status: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown }
const status = typeof parsed.status === 'string' ? parsed.status : null
const pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null
return { status, pid }
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
} catch {
return { status: null, pid: null }
return { status: null, pid: null };
}
}
async function waitFor(
check: () => Promise<boolean> | boolean,
timeoutMs: number,
message: string
message: string,
): Promise<void> {
const deadline = Date.now() + timeoutMs
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return
return;
}
await sleep(pollIntervalMs)
await sleep(pollIntervalMs);
}
throw new Error(message)
throw new Error(message);
}
console.log('=== Daemon Stop (supervisor regression) ===\n')
console.log("=== Daemon Stop (supervisor regression) ===\n");
const port = await getAvailablePort()
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-stop-supervisor-'))
const cliRoot = join(import.meta.dirname, '..')
const port = await getAvailablePort();
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-stop-supervisor-"));
const cliRoot = join(import.meta.dirname, "..");
let supervisorProcess: ChildProcess | null = null
let recentSupervisorLogs = ''
let supervisorProcess: ChildProcess | null = null;
let recentSupervisorLogs = "";
try {
console.log('Test 1: start daemon-runner in dev mode with isolated PASEO_HOME')
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn('npx', ['tsx', '../server/scripts/daemon-runner.ts', '--dev'], {
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `127.0.0.1:${port}`,
PASEO_RELAY_ENABLED: 'false',
CI: 'true',
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ['ignore', 'pipe', 'pipe'],
})
stdio: ["ignore", "pipe", "pipe"],
});
supervisorProcess.stdout?.on('data', (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000)
})
supervisorProcess.stderr?.on('data', (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000)
})
supervisorProcess.stdout?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
});
supervisorProcess.stderr?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
});
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome)
return status.status === 'running' && status.pid !== null && isProcessRunning(status.pid)
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
},
120000,
'daemon did not become running in time'
)
"daemon did not become running in time",
);
const statusBeforeStop = await readDaemonStatus(paseoHome)
const daemonPid = statusBeforeStop.pid
assert.strictEqual(statusBeforeStop.status, 'running', 'daemon should be running before stop')
assert(daemonPid !== null, 'daemon pid should exist once daemon starts')
assert(isProcessRunning(daemonPid), 'daemon process should be running')
const pidLockBeforeStop = await readPidLockState(paseoHome)
assert.strictEqual(pidLockBeforeStop.pid, daemonPid, 'pid lock should match status pid')
const command = readProcessCommand(daemonPid)
assert(command !== null, 'pid lock pid should resolve to a running process command')
const statusBeforeStop = await readDaemonStatus(paseoHome);
const daemonPid = statusBeforeStop.pid;
assert.strictEqual(statusBeforeStop.status, "running", "daemon should be running before stop");
assert(daemonPid !== null, "daemon pid should exist once daemon starts");
assert(isProcessRunning(daemonPid), "daemon process should be running");
const pidLockBeforeStop = await readPidLockState(paseoHome);
assert.strictEqual(pidLockBeforeStop.pid, daemonPid, "pid lock should match status pid");
const command = readProcessCommand(daemonPid);
assert(command !== null, "pid lock pid should resolve to a running process command");
assert(
command.includes('daemon-runner.ts') || command.includes('daemon-runner.js'),
`pid lock pid should be daemon-runner process, got: ${command}`
)
console.log(`✓ dev daemon started with daemon pid ${daemonPid}\n`)
command.includes("daemon-runner.ts") || command.includes("daemon-runner.js"),
`pid lock pid should be daemon-runner process, got: ${command}`,
);
console.log(`✓ dev daemon started with daemon pid ${daemonPid}\n`);
console.log('Test 2: `paseo daemon stop` should stop without respawn')
console.log("Test 2: `paseo daemon stop` should stop without respawn");
const stopResult =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --json`.nothrow()
assert.strictEqual(stopResult.exitCode, 0, `stop should succeed: ${stopResult.stderr}`)
const stopJson = JSON.parse(stopResult.stdout) as { action?: unknown }
assert.strictEqual(stopJson.action, 'stopped', 'stop should report stopped action')
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --json`.nothrow();
assert.strictEqual(stopResult.exitCode, 0, `stop should succeed: ${stopResult.stderr}`);
const stopJson = JSON.parse(stopResult.stdout) as { action?: unknown };
assert.strictEqual(stopJson.action, "stopped", "stop should report stopped action");
await waitFor(async () => {
const status = await readDaemonStatus(paseoHome)
return status.status === 'stopped'
}, 15000, 'daemon status did not transition to stopped after stop')
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "stopped";
},
15000,
"daemon status did not transition to stopped after stop",
);
if (supervisorProcess.pid) {
await waitFor(
() => !isProcessRunning(supervisorProcess!.pid ?? -1),
15000,
'daemon-runner supervisor remained running after stop'
)
"daemon-runner supervisor remained running after stop",
);
}
await sleep(1000)
await sleep(1000);
const pidAfterStop = await readPidLockState(paseoHome)
const respawned = pidAfterStop.pid !== null && isProcessRunning(pidAfterStop.pid)
const pidAfterStop = await readPidLockState(paseoHome);
const respawned = pidAfterStop.pid !== null && isProcessRunning(pidAfterStop.pid);
assert.strictEqual(
respawned,
false,
`daemon respawned after stop (pid: ${pidAfterStop.pid ?? 'unknown'})`
)
`daemon respawned after stop (pid: ${pidAfterStop.pid ?? "unknown"})`,
);
const statusAfterStop = await readDaemonStatus(paseoHome)
assert.strictEqual(statusAfterStop.status, 'stopped', 'daemon should remain stopped after stop command')
const statusAfterStop = await readDaemonStatus(paseoHome);
assert.strictEqual(
statusAfterStop.status,
"stopped",
"daemon should remain stopped after stop command",
);
assert(
recentSupervisorLogs.includes('Shutdown requested by worker. Stopping worker...'),
`stop should request lifecycle shutdown from daemon worker, logs:\n${recentSupervisorLogs}`
)
recentSupervisorLogs.includes("Shutdown requested by worker. Stopping worker..."),
`stop should request lifecycle shutdown from daemon worker, logs:\n${recentSupervisorLogs}`,
);
assert(
!recentSupervisorLogs.includes('cli_shutdown'),
`supervisor logs should not route shutdown by reason string:\n${recentSupervisorLogs}`
)
console.log('✓ stop leaves supervised daemon stopped (no respawn)\n')
!recentSupervisorLogs.includes("cli_shutdown"),
`supervisor logs should not route shutdown by reason string:\n${recentSupervisorLogs}`,
);
console.log("✓ stop leaves supervised daemon stopped (no respawn)\n");
} finally {
if (supervisorProcess?.pid && isProcessRunning(supervisorProcess.pid)) {
supervisorProcess.kill('SIGTERM')
await waitFor(() => !isProcessRunning(supervisorProcess!.pid ?? -1), 5000, 'supervisor cleanup timed out').catch(
() => {
supervisorProcess?.kill('SIGKILL')
}
)
supervisorProcess.kill("SIGTERM");
await waitFor(
() => !isProcessRunning(supervisorProcess!.pid ?? -1),
5000,
"supervisor cleanup timed out",
).catch(() => {
supervisorProcess?.kill("SIGKILL");
});
}
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --force`.nothrow()
await rm(paseoHome, { recursive: true, force: true })
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --force`.nothrow();
await rm(paseoHome, { recursive: true, force: true });
}
if (recentSupervisorLogs.trim().length === 0) {
// Keep output stable while still surfacing that logs were captured when needed.
console.log('(no supervisor logs captured)')
console.log("(no supervisor logs captured)");
}
console.log('=== Supervisor stop regression test passed ===')
console.log("=== Supervisor stop regression test passed ===");
@@ -5,213 +5,224 @@
* graceful daemon lifecycle shutdown to complete (no early forced exit path).
*/
import assert from 'node:assert'
import { spawn, type ChildProcess } from 'node:child_process'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { $ } from 'zx'
import { getAvailablePort } from './helpers/network.ts'
import assert from "node:assert";
import { spawn, type ChildProcess } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false
$.verbose = false;
const pollIntervalMs = 100
const pollIntervalMs = 100;
const testEnv = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isProcessRunning(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false
return false;
}
try {
process.kill(pid, 0)
return true
process.kill(pid, 0);
return true;
} catch {
return false
return false;
}
}
function signalProcessGroup(pid: number, signal: NodeJS.Signals): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false
return false;
}
if (process.platform === 'win32') {
if (process.platform === "win32") {
try {
process.kill(pid, signal)
return true
process.kill(pid, signal);
return true;
} catch {
return false
return false;
}
}
try {
process.kill(-pid, signal)
return true
process.kill(-pid, signal);
return true;
} catch {
return false
return false;
}
}
type DaemonStatus = {
status: string | null
pid: number | null
}
status: string | null;
pid: number | null;
};
async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow()
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null }
return { status: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown }
const status = typeof parsed.status === 'string' ? parsed.status : null
const pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null
return { status, pid }
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
} catch {
return { status: null, pid: null }
return { status: null, pid: null };
}
}
async function waitFor(
check: () => Promise<boolean> | boolean,
timeoutMs: number,
message: string
message: string,
): Promise<void> {
const deadline = Date.now() + timeoutMs
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return
return;
}
await sleep(pollIntervalMs)
await sleep(pollIntervalMs);
}
throw new Error(message)
throw new Error(message);
}
type ExitResult = {
code: number | null
signal: NodeJS.Signals | null
}
code: number | null;
signal: NodeJS.Signals | null;
};
function waitForProcessExit(processRef: ChildProcess, timeoutMs: number): Promise<ExitResult> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('timed out waiting for process exit'))
}, timeoutMs)
reject(new Error("timed out waiting for process exit"));
}, timeoutMs);
processRef.once('exit', (code, signal) => {
clearTimeout(timeout)
resolve({ code, signal })
})
})
processRef.once("exit", (code, signal) => {
clearTimeout(timeout);
resolve({ code, signal });
});
});
}
console.log('=== Daemon SIGINT (supervisor regression) ===\n')
console.log("=== Daemon SIGINT (supervisor regression) ===\n");
const port = await getAvailablePort()
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-sigint-supervisor-'))
const cliRoot = join(import.meta.dirname, '..')
const port = await getAvailablePort();
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-sigint-supervisor-"));
const cliRoot = join(import.meta.dirname, "..");
let supervisorProcess: ChildProcess | null = null
let recentSupervisorLogs = ''
let supervisorProcess: ChildProcess | null = null;
let recentSupervisorLogs = "";
try {
console.log('Test 1: start daemon-runner in dev mode with isolated PASEO_HOME')
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn('npx', ['tsx', '../server/scripts/daemon-runner.ts', '--dev'], {
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `127.0.0.1:${port}`,
PASEO_RELAY_ENABLED: 'false',
CI: 'true',
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ['ignore', 'pipe', 'pipe'],
detached: process.platform !== 'win32',
})
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
});
supervisorProcess.stdout?.on('data', (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000)
})
supervisorProcess.stderr?.on('data', (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000)
})
supervisorProcess.stdout?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
});
supervisorProcess.stderr?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
});
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome)
return status.status === 'running' && status.pid !== null && isProcessRunning(status.pid)
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
},
120000,
'daemon did not become running in time'
)
"daemon did not become running in time",
);
console.log('✓ supervised daemon started\n')
console.log("✓ supervised daemon started\n");
console.log('Test 2: single SIGINT should shutdown gracefully without forced exit')
const exitPromise = waitForProcessExit(supervisorProcess, 30000)
const signaledGroup = signalProcessGroup(supervisorProcess.pid ?? -1, 'SIGINT')
console.log("Test 2: single SIGINT should shutdown gracefully without forced exit");
const exitPromise = waitForProcessExit(supervisorProcess, 30000);
const signaledGroup = signalProcessGroup(supervisorProcess.pid ?? -1, "SIGINT");
if (!signaledGroup) {
supervisorProcess.kill('SIGINT')
supervisorProcess.kill("SIGINT");
}
const exit = await exitPromise
assert.strictEqual(exit.signal, null, `supervisor should exit cleanly, got signal=${exit.signal}`)
assert.strictEqual(exit.code, 0, `supervisor should exit with status 0, got code=${exit.code}`)
const exit = await exitPromise;
assert.strictEqual(
exit.signal,
null,
`supervisor should exit cleanly, got signal=${exit.signal}`,
);
assert.strictEqual(exit.code, 0, `supervisor should exit with status 0, got code=${exit.code}`);
await waitFor(async () => {
const status = await readDaemonStatus(paseoHome)
return status.status === 'stopped'
}, 15000, 'daemon status did not transition to stopped after SIGINT')
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "stopped";
},
15000,
"daemon status did not transition to stopped after SIGINT",
);
assert(
!recentSupervisorLogs.includes('Forcing exit...'),
`worker entered forced-exit path during single SIGINT:\n${recentSupervisorLogs}`
)
!recentSupervisorLogs.includes("Forcing exit..."),
`worker entered forced-exit path during single SIGINT:\n${recentSupervisorLogs}`,
);
assert(
!recentSupervisorLogs.includes("Forcing shutdown - HTTP server didn't close in time"),
`worker hit shutdown timeout during single SIGINT:\n${recentSupervisorLogs}`
)
`worker hit shutdown timeout during single SIGINT:\n${recentSupervisorLogs}`,
);
console.log('✓ single SIGINT completed graceful shutdown without forced exit\n')
console.log("✓ single SIGINT completed graceful shutdown without forced exit\n");
} finally {
if (supervisorProcess?.pid && isProcessRunning(supervisorProcess.pid)) {
const signaledGroup = signalProcessGroup(supervisorProcess.pid, 'SIGTERM')
const signaledGroup = signalProcessGroup(supervisorProcess.pid, "SIGTERM");
if (!signaledGroup) {
supervisorProcess.kill('SIGTERM')
supervisorProcess.kill("SIGTERM");
}
await waitFor(() => !isProcessRunning(supervisorProcess!.pid ?? -1), 5000, 'supervisor cleanup timed out').catch(
() => {
const killedGroup = signalProcessGroup(supervisorProcess!.pid ?? -1, 'SIGKILL')
if (!killedGroup) {
supervisorProcess?.kill('SIGKILL')
}
await waitFor(
() => !isProcessRunning(supervisorProcess!.pid ?? -1),
5000,
"supervisor cleanup timed out",
).catch(() => {
const killedGroup = signalProcessGroup(supervisorProcess!.pid ?? -1, "SIGKILL");
if (!killedGroup) {
supervisorProcess?.kill("SIGKILL");
}
)
});
}
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --force`.nothrow()
await rm(paseoHome, { recursive: true, force: true })
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --force`.nothrow();
await rm(paseoHome, { recursive: true, force: true });
}
if (recentSupervisorLogs.trim().length === 0) {
console.log('(no supervisor logs captured)')
console.log("(no supervisor logs captured)");
}
console.log('=== Supervisor SIGINT regression test passed ===')
console.log("=== Supervisor SIGINT regression test passed ===");
@@ -5,111 +5,115 @@
* must not discover/kill processes via home-scoped `ps` command heuristics.
*/
import assert from 'node:assert'
import { spawn, type ChildProcess } from 'node:child_process'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { $ } from 'zx'
import assert from "node:assert";
import { spawn, type ChildProcess } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
$.verbose = false
$.verbose = false;
const testEnv = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isProcessRunning(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false
return false;
}
try {
process.kill(pid, 0)
return true
process.kill(pid, 0);
return true;
} catch {
return false
return false;
}
}
async function waitForRunning(pid: number, timeoutMs: number): Promise<void> {
const deadline = Date.now() + timeoutMs
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (isProcessRunning(pid)) {
return
return;
}
await sleep(50)
await sleep(50);
}
throw new Error(`Process ${pid} did not become running in time`)
throw new Error(`Process ${pid} did not become running in time`);
}
console.log('=== Daemon Stop Ownership Regression ===\n')
console.log("=== Daemon Stop Ownership Regression ===\n");
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-stop-ownership-'))
let decoyProcess: ChildProcess | null = null
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-stop-ownership-"));
let decoyProcess: ChildProcess | null = null;
try {
console.log('Test 1: start decoy process with daemon-like command markers')
console.log("Test 1: start decoy process with daemon-like command markers");
decoyProcess = spawn(
process.execPath,
[
'-e',
"-e",
// Keep the process alive long enough for stop command assertions.
'setInterval(() => {}, 1000)',
'daemon-runner.ts',
"setInterval(() => {}, 1000)",
"daemon-runner.ts",
],
{
env: {
...process.env,
PASEO_HOME: paseoHome,
},
stdio: 'ignore',
detached: process.platform !== 'win32',
}
)
decoyProcess.unref()
stdio: "ignore",
detached: process.platform !== "win32",
},
);
decoyProcess.unref();
const decoyPid = decoyProcess.pid
assert(Number.isInteger(decoyPid) && (decoyPid ?? 0) > 0, 'decoy pid should exist')
await waitForRunning(decoyPid!, 5000)
console.log(`✓ decoy process started (${decoyPid})\n`)
const decoyPid = decoyProcess.pid;
assert(Number.isInteger(decoyPid) && (decoyPid ?? 0) > 0, "decoy pid should exist");
await waitForRunning(decoyPid!, 5000);
console.log(`✓ decoy process started (${decoyPid})\n`);
console.log('Test 2: daemon stop should report not_running and leave decoy untouched')
console.log("Test 2: daemon stop should report not_running and leave decoy untouched");
const stopResult =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --json`.nothrow()
assert.strictEqual(stopResult.exitCode, 0, `stop should succeed: ${stopResult.stderr}`)
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --json`.nothrow();
assert.strictEqual(stopResult.exitCode, 0, `stop should succeed: ${stopResult.stderr}`);
const parsed = JSON.parse(stopResult.stdout) as { action?: unknown }
assert.strictEqual(parsed.action, 'not_running', `stop should not target decoy process: ${stopResult.stdout}`)
assert(isProcessRunning(decoyPid!), 'decoy process must remain alive after stop')
const parsed = JSON.parse(stopResult.stdout) as { action?: unknown };
assert.strictEqual(
parsed.action,
"not_running",
`stop should not target decoy process: ${stopResult.stdout}`,
);
assert(isProcessRunning(decoyPid!), "decoy process must remain alive after stop");
console.log('✓ stop is ownership-driven and does not kill decoy process\n')
console.log("✓ stop is ownership-driven and does not kill decoy process\n");
} finally {
if (decoyProcess?.pid && isProcessRunning(decoyProcess.pid)) {
try {
process.kill(decoyProcess.pid, 'SIGTERM')
process.kill(decoyProcess.pid, "SIGTERM");
} catch {
// ignore
}
await sleep(100)
await sleep(100);
if (isProcessRunning(decoyProcess.pid)) {
try {
process.kill(decoyProcess.pid, 'SIGKILL')
process.kill(decoyProcess.pid, "SIGKILL");
} catch {
// ignore
}
}
}
await $`PASEO_HOME=${paseoHome} npx paseo daemon stop --home ${paseoHome} --force`.nothrow()
await rm(paseoHome, { recursive: true, force: true })
await $`PASEO_HOME=${paseoHome} npx paseo daemon stop --home ${paseoHome} --force`.nothrow();
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== Daemon stop ownership regression test passed ===')
console.log("=== Daemon stop ownership regression test passed ===");
@@ -5,210 +5,240 @@
* (worker PID changes) while keeping the daemon healthy.
*/
import assert from 'node:assert'
import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { $ } from 'zx'
import { tryConnectToDaemon } from '../src/utils/client.ts'
import { getAvailablePort } from './helpers/network.ts'
import assert from "node:assert";
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
import { tryConnectToDaemon } from "../src/utils/client.ts";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false
$.verbose = false;
const pollIntervalMs = 100
const pollIntervalMs = 100;
const testEnv = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isProcessRunning(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false
return false;
}
try {
process.kill(pid, 0)
return true
process.kill(pid, 0);
return true;
} catch {
return false
return false;
}
}
function readWorkerPid(supervisorPid: number): number | null {
if (!Number.isInteger(supervisorPid) || supervisorPid <= 0) {
return null
return null;
}
const result = spawnSync('ps', ['ax', '-o', 'pid=,ppid='], { encoding: 'utf8' })
const result = spawnSync("ps", ["ax", "-o", "pid=,ppid="], { encoding: "utf8" });
if (result.status !== 0 || result.error) {
return null
return null;
}
for (const line of result.stdout.split('\n')) {
const trimmed = line.trim()
for (const line of result.stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed) {
continue
continue;
}
const [pidToken, ppidToken] = trimmed.split(/\s+/)
const pid = Number.parseInt(pidToken ?? '', 10)
const ppid = Number.parseInt(ppidToken ?? '', 10)
const [pidToken, ppidToken] = trimmed.split(/\s+/);
const pid = Number.parseInt(pidToken ?? "", 10);
const ppid = Number.parseInt(ppidToken ?? "", 10);
if (!Number.isInteger(pid) || !Number.isInteger(ppid)) {
continue
continue;
}
if (ppid === supervisorPid && pid > 0) {
return pid
return pid;
}
}
return null
return null;
}
type DaemonStatus = {
status: string | null
pid: number | null
}
status: string | null;
pid: number | null;
};
async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow()
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null }
return { status: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown }
const status = typeof parsed.status === 'string' ? parsed.status : null
const pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null
return { status, pid }
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
} catch {
return { status: null, pid: null }
return { status: null, pid: null };
}
}
async function waitFor(
check: () => Promise<boolean> | boolean,
timeoutMs: number,
message: string
message: string,
): Promise<void> {
const deadline = Date.now() + timeoutMs
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return
return;
}
await sleep(pollIntervalMs)
await sleep(pollIntervalMs);
}
throw new Error(message)
throw new Error(message);
}
console.log('=== Daemon Restart (supervisor regression) ===\n')
console.log("=== Daemon Restart (supervisor regression) ===\n");
const port = await getAvailablePort()
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-restart-supervisor-'))
const cliRoot = join(import.meta.dirname, '..')
const host = `127.0.0.1:${port}`
const port = await getAvailablePort();
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-restart-supervisor-"));
const cliRoot = join(import.meta.dirname, "..");
const host = `127.0.0.1:${port}`;
let supervisorProcess: ChildProcess | null = null
let recentSupervisorLogs = ''
let supervisorProcess: ChildProcess | null = null;
let recentSupervisorLogs = "";
try {
console.log('Test 1: start daemon-runner in dev mode with isolated PASEO_HOME')
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn('npx', ['tsx', '../server/scripts/daemon-runner.ts', '--dev'], {
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: 'false',
CI: 'true',
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ['ignore', 'pipe', 'pipe'],
})
stdio: ["ignore", "pipe", "pipe"],
});
supervisorProcess.stdout?.on('data', (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000)
})
supervisorProcess.stderr?.on('data', (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000)
})
supervisorProcess.stdout?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
});
supervisorProcess.stderr?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
});
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome)
return status.status === 'running' && status.pid !== null && isProcessRunning(status.pid)
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
},
120000,
'daemon did not become running in time'
)
"daemon did not become running in time",
);
const statusBeforeRestart = await readDaemonStatus(paseoHome)
const supervisorPid = statusBeforeRestart.pid
assert.strictEqual(statusBeforeRestart.status, 'running', 'daemon should be running before restart')
assert(supervisorPid !== null, 'supervisor pid should exist once daemon starts')
assert(isProcessRunning(supervisorPid), 'supervisor process should be running')
const workerPidBeforeRestart = readWorkerPid(supervisorPid)
assert(workerPidBeforeRestart !== null, 'supervisor should have a worker process before restart')
assert(isProcessRunning(workerPidBeforeRestart), 'worker process should be running before restart')
console.log(`✓ daemon running with supervisor ${supervisorPid} and worker ${workerPidBeforeRestart}\n`)
const statusBeforeRestart = await readDaemonStatus(paseoHome);
const supervisorPid = statusBeforeRestart.pid;
assert.strictEqual(
statusBeforeRestart.status,
"running",
"daemon should be running before restart",
);
assert(supervisorPid !== null, "supervisor pid should exist once daemon starts");
assert(isProcessRunning(supervisorPid), "supervisor process should be running");
const workerPidBeforeRestart = readWorkerPid(supervisorPid);
assert(workerPidBeforeRestart !== null, "supervisor should have a worker process before restart");
assert(
isProcessRunning(workerPidBeforeRestart),
"worker process should be running before restart",
);
console.log(
`✓ daemon running with supervisor ${supervisorPid} and worker ${workerPidBeforeRestart}\n`,
);
console.log('Test 2: app-style restart request should restart worker and keep daemon healthy')
const client = await tryConnectToDaemon({ host, timeout: 5000 })
assert(client, 'daemon client should connect')
console.log("Test 2: app-style restart request should restart worker and keep daemon healthy");
const client = await tryConnectToDaemon({ host, timeout: 5000 });
assert(client, "daemon client should connect");
try {
const restartAck = await client.restartServer('settings_update')
assert.strictEqual(restartAck.status, 'restart_requested', 'restart request should be acknowledged')
const restartAck = await client.restartServer("settings_update");
assert.strictEqual(
restartAck.status,
"restart_requested",
"restart request should be acknowledged",
);
} finally {
await client?.close().catch(() => undefined)
await client?.close().catch(() => undefined);
}
await waitFor(() => {
const workerPid = readWorkerPid(supervisorPid)
return workerPid !== null && workerPid !== workerPidBeforeRestart && isProcessRunning(workerPid)
}, 20000, 'worker pid did not change after restart request')
await waitFor(
() => {
const workerPid = readWorkerPid(supervisorPid);
return (
workerPid !== null && workerPid !== workerPidBeforeRestart && isProcessRunning(workerPid)
);
},
20000,
"worker pid did not change after restart request",
);
const workerPidAfterRestart = readWorkerPid(supervisorPid)
assert(workerPidAfterRestart !== null, 'worker process should exist after restart')
const workerPidAfterRestart = readWorkerPid(supervisorPid);
assert(workerPidAfterRestart !== null, "worker process should exist after restart");
assert.notStrictEqual(
workerPidAfterRestart,
workerPidBeforeRestart,
'worker pid should change after restart'
)
"worker pid should change after restart",
);
const statusAfterRestart = await readDaemonStatus(paseoHome)
assert.strictEqual(statusAfterRestart.status, 'running', 'daemon should stay running after restart')
assert.strictEqual(statusAfterRestart.pid, supervisorPid, 'supervisor pid should remain stable across restart')
const statusAfterRestart = await readDaemonStatus(paseoHome);
assert.strictEqual(
statusAfterRestart.status,
"running",
"daemon should stay running after restart",
);
assert.strictEqual(
statusAfterRestart.pid,
supervisorPid,
"supervisor pid should remain stable across restart",
);
assert(
recentSupervisorLogs.includes('Restart requested by worker. Stopping worker for restart...'),
`restart should route through supervisor restart intent, logs:\n${recentSupervisorLogs}`
)
console.log('✓ app-style restart keeps daemon healthy and restarts worker\n')
recentSupervisorLogs.includes("Restart requested by worker. Stopping worker for restart..."),
`restart should route through supervisor restart intent, logs:\n${recentSupervisorLogs}`,
);
console.log("✓ app-style restart keeps daemon healthy and restarts worker\n");
} finally {
if (supervisorProcess?.pid && isProcessRunning(supervisorProcess.pid)) {
supervisorProcess.kill('SIGTERM')
await waitFor(() => !isProcessRunning(supervisorProcess!.pid ?? -1), 5000, 'supervisor cleanup timed out').catch(
() => {
supervisorProcess?.kill('SIGKILL')
}
)
supervisorProcess.kill("SIGTERM");
await waitFor(
() => !isProcessRunning(supervisorProcess!.pid ?? -1),
5000,
"supervisor cleanup timed out",
).catch(() => {
supervisorProcess?.kill("SIGKILL");
});
}
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --force`.nothrow()
await rm(paseoHome, { recursive: true, force: true })
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon stop --home ${paseoHome} --force`.nothrow();
await rm(paseoHome, { recursive: true, force: true });
}
if (recentSupervisorLogs.trim().length === 0) {
console.log('(no supervisor logs captured)')
console.log("(no supervisor logs captured)");
}
console.log('=== Supervisor restart regression test passed ===')
console.log("=== Supervisor restart regression test passed ===");
@@ -5,198 +5,217 @@
* so an external owner can decide whether to respawn.
*/
import assert from 'node:assert'
import { spawn, type ChildProcess } from 'node:child_process'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { $ } from 'zx'
import { tryConnectToDaemon } from '../src/utils/client.ts'
import { getAvailablePort } from './helpers/network.ts'
import assert from "node:assert";
import { spawn, type ChildProcess } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
import { tryConnectToDaemon } from "../src/utils/client.ts";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false
$.verbose = false;
const pollIntervalMs = 100
const pollIntervalMs = 100;
const testEnv = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isProcessRunning(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false
return false;
}
try {
process.kill(pid, 0)
return true
process.kill(pid, 0);
return true;
} catch {
return false
return false;
}
}
type DaemonStatus = {
status: string | null
pid: number | null
}
status: string | null;
pid: number | null;
};
async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow()
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null }
return { status: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown }
const status = typeof parsed.status === 'string' ? parsed.status : null
const pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null
return { status, pid }
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
} catch {
return { status: null, pid: null }
return { status: null, pid: null };
}
}
async function waitFor(
check: () => Promise<boolean> | boolean,
timeoutMs: number,
message: string
message: string,
): Promise<void> {
const deadline = Date.now() + timeoutMs
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return
return;
}
await sleep(pollIntervalMs)
await sleep(pollIntervalMs);
}
throw new Error(message)
throw new Error(message);
}
type ExitResult = {
code: number | null
signal: NodeJS.Signals | null
}
code: number | null;
signal: NodeJS.Signals | null;
};
function waitForProcessExit(processRef: ChildProcess, timeoutMs: number): Promise<ExitResult> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('timed out waiting for process exit'))
}, timeoutMs)
reject(new Error("timed out waiting for process exit"));
}, timeoutMs);
processRef.once('exit', (code, signal) => {
clearTimeout(timeout)
resolve({ code, signal })
})
})
processRef.once("exit", (code, signal) => {
clearTimeout(timeout);
resolve({ code, signal });
});
});
}
async function readPidLockPid(paseoHome: string): Promise<number | null> {
const pidPath = join(paseoHome, 'paseo.pid')
const pidPath = join(paseoHome, "paseo.pid");
try {
const content = await readFile(pidPath, 'utf-8')
const parsed = JSON.parse(content) as { pid?: unknown }
if (typeof parsed.pid !== 'number' || !Number.isInteger(parsed.pid) || parsed.pid <= 0) {
return null
const content = await readFile(pidPath, "utf-8");
const parsed = JSON.parse(content) as { pid?: unknown };
if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) {
return null;
}
return parsed.pid
return parsed.pid;
} catch {
return null
return null;
}
}
console.log('=== Daemon Restart (unsupervised regression) ===\n')
console.log("=== Daemon Restart (unsupervised regression) ===\n");
const port = await getAvailablePort()
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-restart-unsupervised-'))
const cliRoot = join(import.meta.dirname, '..')
const host = `127.0.0.1:${port}`
const port = await getAvailablePort();
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-restart-unsupervised-"));
const cliRoot = join(import.meta.dirname, "..");
const host = `127.0.0.1:${port}`;
let daemonProcess: ChildProcess | null = null
let daemonProcess: ChildProcess | null = null;
try {
console.log('Test 1: start unsupervised daemon worker directly')
console.log("Test 1: start unsupervised daemon worker directly");
daemonProcess = spawn(
process.execPath,
[...process.execArgv, '--import', 'tsx', '../server/src/server/index.ts'],
[...process.execArgv, "--import", "tsx", "../server/src/server/index.ts"],
{
cwd: cliRoot,
env: {
...process.env,
...testEnv,
// This test validates direct unsupervised worker ownership semantics.
// Agent-orchestrated shells may export PASEO_PID_LOCK_MODE=external,
// which would delegate lock ownership away from this process and make
// daemon status checks fail to observe a running owner PID.
PASEO_PID_LOCK_MODE: 'self',
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: 'false',
CI: 'true',
cwd: cliRoot,
env: {
...process.env,
...testEnv,
// This test validates direct unsupervised worker ownership semantics.
// Agent-orchestrated shells may export PASEO_PID_LOCK_MODE=external,
// which would delegate lock ownership away from this process and make
// daemon status checks fail to observe a running owner PID.
PASEO_PID_LOCK_MODE: "self",
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ["ignore", "pipe", "pipe"],
},
stdio: ['ignore', 'pipe', 'pipe'],
}
)
);
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome)
return status.status === 'running' && status.pid !== null && isProcessRunning(status.pid)
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
},
120000,
'daemon did not become running in time'
)
"daemon did not become running in time",
);
const statusBeforeRestart = await readDaemonStatus(paseoHome)
assert.strictEqual(statusBeforeRestart.status, 'running', 'daemon should be running before restart')
assert(daemonProcess.pid, 'unsupervised daemon process pid should exist')
assert.strictEqual(statusBeforeRestart.pid, daemonProcess.pid, 'status pid should match daemon process pid')
const lockPid = await readPidLockPid(paseoHome)
assert.strictEqual(lockPid, daemonProcess.pid, 'unsupervised worker should own pid lock')
console.log(`✓ unsupervised daemon started with pid ${daemonProcess.pid}\n`)
const statusBeforeRestart = await readDaemonStatus(paseoHome);
assert.strictEqual(
statusBeforeRestart.status,
"running",
"daemon should be running before restart",
);
assert(daemonProcess.pid, "unsupervised daemon process pid should exist");
assert.strictEqual(
statusBeforeRestart.pid,
daemonProcess.pid,
"status pid should match daemon process pid",
);
const lockPid = await readPidLockPid(paseoHome);
assert.strictEqual(lockPid, daemonProcess.pid, "unsupervised worker should own pid lock");
console.log(`✓ unsupervised daemon started with pid ${daemonProcess.pid}\n`);
console.log('Test 2: restart request should gracefully stop and exit code 0')
const client = await tryConnectToDaemon({ host, timeout: 5000 })
assert(client, 'daemon client should connect')
console.log("Test 2: restart request should gracefully stop and exit code 0");
const client = await tryConnectToDaemon({ host, timeout: 5000 });
assert(client, "daemon client should connect");
const exitPromise = waitForProcessExit(daemonProcess, 30000)
const exitPromise = waitForProcessExit(daemonProcess, 30000);
try {
const restartAck = await client.restartServer('settings_update')
assert.strictEqual(restartAck.status, 'restart_requested', 'restart request should be acknowledged')
const restartAck = await client.restartServer("settings_update");
assert.strictEqual(
restartAck.status,
"restart_requested",
"restart request should be acknowledged",
);
} finally {
await client?.close().catch(() => undefined)
await client?.close().catch(() => undefined);
}
const exit = await exitPromise
assert.strictEqual(exit.signal, null, `daemon should exit cleanly, got signal=${exit.signal}`)
assert.strictEqual(exit.code, 0, `daemon should exit with status 0, got code=${exit.code}`)
const exit = await exitPromise;
assert.strictEqual(exit.signal, null, `daemon should exit cleanly, got signal=${exit.signal}`);
assert.strictEqual(exit.code, 0, `daemon should exit with status 0, got code=${exit.code}`);
await waitFor(async () => {
const status = await readDaemonStatus(paseoHome)
return status.status === 'stopped'
}, 15000, 'daemon status did not transition to stopped after unsupervised restart request')
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "stopped";
},
15000,
"daemon status did not transition to stopped after unsupervised restart request",
);
console.log('✓ unsupervised restart exited cleanly with code 0\n')
console.log("✓ unsupervised restart exited cleanly with code 0\n");
} finally {
if (daemonProcess?.pid && isProcessRunning(daemonProcess.pid)) {
daemonProcess.kill('SIGTERM')
await waitFor(() => !isProcessRunning(daemonProcess!.pid ?? -1), 5000, 'daemon cleanup timed out').catch(
() => {
daemonProcess?.kill('SIGKILL')
}
)
daemonProcess.kill("SIGTERM");
await waitFor(
() => !isProcessRunning(daemonProcess!.pid ?? -1),
5000,
"daemon cleanup timed out",
).catch(() => {
daemonProcess?.kill("SIGKILL");
});
}
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== Unsupervised restart regression test passed ===')
console.log("=== Unsupervised restart regression test passed ===");
+72 -72
View File
@@ -10,115 +10,115 @@
* - All flags are accepted
*/
import assert from 'node:assert'
import { $ } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import assert from "node:assert";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
$.verbose = false
$.verbose = false;
console.log('=== Delete Command Tests ===\n')
console.log("=== Delete Command Tests ===\n");
const cliRoot = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(cliRoot, '..', '..', '..')
const port = 10000 + Math.floor(Math.random() * 50000)
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-delete-test-home-'))
const cliRoot = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(cliRoot, "..", "..", "..");
const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-delete-test-home-"));
async function runCli(args: string[]) {
return $`npm --prefix ${repoRoot} run cli -- ${args}`.nothrow()
return $`npm --prefix ${repoRoot} run cli -- ${args}`.nothrow();
}
async function runDelete(args: string[]) {
return $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm --prefix ${repoRoot} run cli -- delete ${args}`.nothrow()
return $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm --prefix ${repoRoot} run cli -- delete ${args}`.nothrow();
}
try {
{
console.log('Test 1: delete --help shows options')
const result = await runDelete(['--help'])
assert.strictEqual(result.exitCode, 0, 'delete --help should exit 0')
assert(result.stdout.includes('--all'), 'help should mention --all flag')
assert(result.stdout.includes('--cwd'), 'help should mention --cwd option')
assert(result.stdout.includes('--host'), 'help should mention --host option')
assert(result.stdout.includes('[id]'), 'help should mention optional id argument')
console.log('✓ delete --help shows options\n')
console.log("Test 1: delete --help shows options");
const result = await runDelete(["--help"]);
assert.strictEqual(result.exitCode, 0, "delete --help should exit 0");
assert(result.stdout.includes("--all"), "help should mention --all flag");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("[id]"), "help should mention optional id argument");
console.log("✓ delete --help shows options\n");
}
{
console.log('Test 2: delete requires ID, --all, or --cwd')
const result = await runDelete([])
assert.notStrictEqual(result.exitCode, 0, 'should fail without id, --all, or --cwd')
const output = result.stdout + result.stderr
console.log("Test 2: delete requires ID, --all, or --cwd");
const result = await runDelete([]);
assert.notStrictEqual(result.exitCode, 0, "should fail without id, --all, or --cwd");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('missing') ||
output.toLowerCase().includes('required') ||
output.toLowerCase().includes('argument') ||
output.toLowerCase().includes('id')
assert(hasError, 'error should mention missing argument')
console.log('✓ delete requires ID, --all, or --cwd\n')
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument") ||
output.toLowerCase().includes("id");
assert(hasError, "error should mention missing argument");
console.log("✓ delete requires ID, --all, or --cwd\n");
}
{
console.log('Test 3: delete handles daemon not running')
const result = await runDelete(['abc123'])
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
const output = result.stdout + result.stderr
console.log("Test 3: delete handles daemon not running");
const result = await runDelete(["abc123"]);
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes('daemon') ||
output.toLowerCase().includes('connect') ||
output.toLowerCase().includes('cannot')
assert(hasError, 'error message should mention connection issue')
console.log('✓ delete handles daemon not running\n')
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ delete handles daemon not running\n");
}
{
console.log('Test 4: delete --all flag is accepted')
const result = await runDelete(['--all'])
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --all flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ delete --all flag is accepted\n')
console.log("Test 4: delete --all flag is accepted");
const result = await runDelete(["--all"]);
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --all flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ delete --all flag is accepted\n");
}
{
console.log('Test 5: delete --cwd flag is accepted')
const result = await runDelete(['--cwd', '/tmp'])
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --cwd flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ delete --cwd flag is accepted\n')
console.log("Test 5: delete --cwd flag is accepted");
const result = await runDelete(["--cwd", "/tmp"]);
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --cwd flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ delete --cwd flag is accepted\n");
}
{
console.log('Test 6: delete with ID and --host flag is accepted')
const result = await runDelete(['abc123', '--host', `localhost:${port}`])
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept --host flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ delete with ID and --host flag is accepted\n')
console.log("Test 6: delete with ID and --host flag is accepted");
const result = await runDelete(["abc123", "--host", `localhost:${port}`]);
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ delete with ID and --host flag is accepted\n");
}
{
console.log('Test 7: paseo --help shows delete command')
const result = await runCli(['--help'])
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
assert(result.stdout.includes('delete'), 'help should mention delete command')
console.log('✓ paseo --help shows delete command\n')
console.log("Test 7: paseo --help shows delete command");
const result = await runCli(["--help"]);
assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0");
assert(result.stdout.includes("delete"), "help should mention delete command");
console.log("✓ paseo --help shows delete command\n");
}
{
console.log('Test 8: -q (quiet) flag is accepted with delete')
console.log("Test 8: -q (quiet) flag is accepted with delete");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm --prefix ${repoRoot} run cli -- -q delete abc123`.nothrow()
const output = result.stdout + result.stderr
assert(!output.includes('unknown option'), 'should accept -q flag')
assert(!output.includes('error: option'), 'should not have option parsing error')
console.log('✓ -q (quiet) flag is accepted with delete\n')
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm --prefix ${repoRoot} run cli -- -q delete abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with delete\n");
}
} finally {
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
console.log('=== All delete tests passed ===')
console.log("=== All delete tests passed ===");
@@ -1,111 +1,111 @@
#!/usr/bin/env npx tsx
import assert from 'node:assert'
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import assert from "node:assert";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import {
getDaemonHost,
normalizeDaemonHost,
resolveDaemonTarget,
resolveDefaultDaemonHosts,
} from '../src/utils/client.js'
} from "../src/utils/client.js";
console.log('=== CLI IPC Target Helpers ===\n')
console.log("=== CLI IPC Target Helpers ===\n");
{
console.log('Test 1: unix hosts resolve to ws+unix URLs')
const target = resolveDaemonTarget('unix:///tmp/paseo.sock')
console.log("Test 1: unix hosts resolve to ws+unix URLs");
const target = resolveDaemonTarget("unix:///tmp/paseo.sock");
assert.deepStrictEqual(target, {
type: 'ipc',
url: 'ws+unix:///tmp/paseo.sock:/ws',
socketPath: '/tmp/paseo.sock',
})
console.log('✓ unix hosts resolve to ws+unix URLs\n')
type: "ipc",
url: "ws+unix:///tmp/paseo.sock:/ws",
socketPath: "/tmp/paseo.sock",
});
console.log("✓ unix hosts resolve to ws+unix URLs\n");
}
{
console.log('Test 2: pipe hosts preserve the Node socketPath transport form')
const target = resolveDaemonTarget('pipe://\\\\.\\pipe\\paseo-managed-test')
console.log("Test 2: pipe hosts preserve the Node socketPath transport form");
const target = resolveDaemonTarget("pipe://\\\\.\\pipe\\paseo-managed-test");
assert.deepStrictEqual(target, {
type: 'ipc',
url: 'ws://localhost/ws',
socketPath: '\\\\.\\pipe\\paseo-managed-test',
})
console.log('✓ pipe hosts preserve Node socketPath transport form\n')
type: "ipc",
url: "ws://localhost/ws",
socketPath: "\\\\.\\pipe\\paseo-managed-test",
});
console.log("✓ pipe hosts preserve Node socketPath transport form\n");
}
{
console.log('Test 3: local unix socket paths normalize into IPC daemon targets')
assert.strictEqual(normalizeDaemonHost('/tmp/paseo.sock'), 'unix:///tmp/paseo.sock')
console.log('✓ local unix socket paths normalize into IPC daemon targets\n')
console.log("Test 3: local unix socket paths normalize into IPC daemon targets");
assert.strictEqual(normalizeDaemonHost("/tmp/paseo.sock"), "unix:///tmp/paseo.sock");
console.log("✓ local unix socket paths normalize into IPC daemon targets\n");
}
{
console.log('Test 4: default host resolution tries local IPC first, then localhost fallback')
const paseoHome = mkdtempSync(path.join(os.tmpdir(), 'paseo-client-targets-'))
console.log("Test 4: default host resolution tries local IPC first, then localhost fallback");
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-client-targets-"));
try {
mkdirSync(paseoHome, { recursive: true })
mkdirSync(paseoHome, { recursive: true });
writeFileSync(
path.join(paseoHome, 'paseo.pid'),
JSON.stringify({ pid: process.pid, listen: '/tmp/paseo-from-pid.sock' })
)
path.join(paseoHome, "paseo.pid"),
JSON.stringify({ pid: process.pid, listen: "/tmp/paseo-from-pid.sock" }),
);
assert.deepStrictEqual(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome }), [
'unix:///tmp/paseo-from-pid.sock',
'localhost:6767',
])
const previousHome = process.env.PASEO_HOME
const previousHost = process.env.PASEO_HOST
process.env.PASEO_HOME = paseoHome
delete process.env.PASEO_HOST
assert.strictEqual(getDaemonHost(), 'unix:///tmp/paseo-from-pid.sock')
if (previousHome === undefined) delete process.env.PASEO_HOME
else process.env.PASEO_HOME = previousHome
if (previousHost === undefined) delete process.env.PASEO_HOST
else process.env.PASEO_HOST = previousHost
"unix:///tmp/paseo-from-pid.sock",
"localhost:6767",
]);
const previousHome = process.env.PASEO_HOME;
const previousHost = process.env.PASEO_HOST;
process.env.PASEO_HOME = paseoHome;
delete process.env.PASEO_HOST;
assert.strictEqual(getDaemonHost(), "unix:///tmp/paseo-from-pid.sock");
if (previousHome === undefined) delete process.env.PASEO_HOME;
else process.env.PASEO_HOME = previousHome;
if (previousHost === undefined) delete process.env.PASEO_HOST;
else process.env.PASEO_HOST = previousHost;
} finally {
rmSync(paseoHome, { recursive: true, force: true })
rmSync(paseoHome, { recursive: true, force: true });
}
console.log('✓ default host resolution tries local IPC first, then localhost fallback\n')
console.log("✓ default host resolution tries local IPC first, then localhost fallback\n");
}
{
console.log('Test 5: configured TCP host is preserved before the localhost fallback')
const paseoHome = mkdtempSync(path.join(os.tmpdir(), 'paseo-client-targets-tcp-'))
console.log("Test 5: configured TCP host is preserved before the localhost fallback");
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-client-targets-tcp-"));
try {
assert.deepStrictEqual(
resolveDefaultDaemonHosts({
PASEO_HOME: paseoHome,
PASEO_LISTEN: '127.0.0.1:7777',
PASEO_LISTEN: "127.0.0.1:7777",
}),
['127.0.0.1:7777', 'localhost:6767']
)
["127.0.0.1:7777", "localhost:6767"],
);
} finally {
rmSync(paseoHome, { recursive: true, force: true })
rmSync(paseoHome, { recursive: true, force: true });
}
console.log('✓ configured TCP host is preserved before the localhost fallback\n')
console.log("✓ configured TCP host is preserved before the localhost fallback\n");
}
{
console.log('Test 6: local IPC still takes priority over configured TCP hosts')
const paseoHome = mkdtempSync(path.join(os.tmpdir(), 'paseo-client-targets-order-'))
console.log("Test 6: local IPC still takes priority over configured TCP hosts");
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-client-targets-order-"));
try {
mkdirSync(paseoHome, { recursive: true })
mkdirSync(paseoHome, { recursive: true });
writeFileSync(
path.join(paseoHome, 'paseo.pid'),
JSON.stringify({ pid: process.pid, listen: '/tmp/paseo-priority.sock' })
)
path.join(paseoHome, "paseo.pid"),
JSON.stringify({ pid: process.pid, listen: "/tmp/paseo-priority.sock" }),
);
assert.deepStrictEqual(
resolveDefaultDaemonHosts({
PASEO_HOME: paseoHome,
PASEO_LISTEN: '127.0.0.1:7777',
PASEO_LISTEN: "127.0.0.1:7777",
}),
['unix:///tmp/paseo-priority.sock', '127.0.0.1:7777', 'localhost:6767']
)
["unix:///tmp/paseo-priority.sock", "127.0.0.1:7777", "localhost:6767"],
);
} finally {
rmSync(paseoHome, { recursive: true, force: true })
rmSync(paseoHome, { recursive: true, force: true });
}
console.log('✓ local IPC still takes priority over configured TCP hosts\n')
console.log("✓ local IPC still takes priority over configured TCP hosts\n");
}
console.log('=== All CLI IPC target tests passed ===')
console.log("=== All CLI IPC target tests passed ===");
@@ -1,44 +1,44 @@
#!/usr/bin/env npx tsx
import assert from 'node:assert'
import { resolveAgentId } from '../src/utils/client.js'
import assert from "node:assert";
import { resolveAgentId } from "../src/utils/client.js";
console.log('=== Agent ID Resolution Tests ===\n')
console.log("=== Agent ID Resolution Tests ===\n");
const agents = [
{ id: 'abc1234-full-agent-id', title: 'Build docs' },
{ id: 'def5678-other-agent-id', title: 'Fix archive flow' },
{ id: '987zyxw-third-agent-id', title: 'Review PR' },
]
{ id: "abc1234-full-agent-id", title: "Build docs" },
{ id: "def5678-other-agent-id", title: "Fix archive flow" },
{ id: "987zyxw-third-agent-id", title: "Review PR" },
];
{
console.log('Test 1: exact ID match resolves')
assert.strictEqual(resolveAgentId('abc1234-full-agent-id', agents), 'abc1234-full-agent-id')
console.log('✓ exact ID match resolves\n')
console.log("Test 1: exact ID match resolves");
assert.strictEqual(resolveAgentId("abc1234-full-agent-id", agents), "abc1234-full-agent-id");
console.log("✓ exact ID match resolves\n");
}
{
console.log('Test 2: ID prefix resolves')
assert.strictEqual(resolveAgentId('def5678', agents), 'def5678-other-agent-id')
console.log('✓ ID prefix resolves\n')
console.log("Test 2: ID prefix resolves");
assert.strictEqual(resolveAgentId("def5678", agents), "def5678-other-agent-id");
console.log("✓ ID prefix resolves\n");
}
{
console.log('Test 3: exact name match resolves case-insensitively')
assert.strictEqual(resolveAgentId('fix archive flow', agents), 'def5678-other-agent-id')
console.log('✓ exact name match resolves case-insensitively\n')
console.log("Test 3: exact name match resolves case-insensitively");
assert.strictEqual(resolveAgentId("fix archive flow", agents), "def5678-other-agent-id");
console.log("✓ exact name match resolves case-insensitively\n");
}
{
console.log('Test 4: partial name match resolves when unique')
assert.strictEqual(resolveAgentId('review', agents), '987zyxw-third-agent-id')
console.log('✓ partial name match resolves when unique\n')
console.log("Test 4: partial name match resolves when unique");
assert.strictEqual(resolveAgentId("review", agents), "987zyxw-third-agent-id");
console.log("✓ partial name match resolves when unique\n");
}
{
console.log('Test 5: missing agent returns null')
assert.strictEqual(resolveAgentId('does not exist', agents), null)
console.log('✓ missing agent returns null\n')
console.log("Test 5: missing agent returns null");
assert.strictEqual(resolveAgentId("does not exist", agents), null);
console.log("✓ missing agent returns null\n");
}
console.log('=== All agent ID resolution tests passed ===')
console.log("=== All agent ID resolution tests passed ===");
+139 -127
View File
@@ -22,238 +22,250 @@
* - Clean up resources after test completes
*/
import assert from 'node:assert'
import { createE2ETestContext, type TestDaemonContext } from '../helpers/test-daemon.ts'
import assert from "node:assert";
import { createE2ETestContext, type TestDaemonContext } from "../helpers/test-daemon.ts";
interface E2EContext extends TestDaemonContext {
paseo: (args: string[], opts?: { timeout?: number; cwd?: string }) => Promise<{
exitCode: number
stdout: string
stderr: string
}>
paseo: (
args: string[],
opts?: { timeout?: number; cwd?: string },
) => Promise<{
exitCode: number;
stdout: string;
stderr: string;
}>;
}
let ctx: E2EContext
let ctx: E2EContext;
async function setup(): Promise<void> {
console.log('Setting up E2E test context...')
console.log('Starting test daemon on random port (this may take a few seconds)...')
console.log("Setting up E2E test context...");
console.log("Starting test daemon on random port (this may take a few seconds)...");
try {
ctx = await createE2ETestContext({ timeout: 45000 })
console.log(`Test daemon started on port ${ctx.port}`)
console.log(`PASEO_HOME: ${ctx.paseoHome}`)
console.log(`Work directory: ${ctx.workDir}`)
ctx = await createE2ETestContext({ timeout: 45000 });
console.log(`Test daemon started on port ${ctx.port}`);
console.log(`PASEO_HOME: ${ctx.paseoHome}`);
console.log(`Work directory: ${ctx.workDir}`);
} catch (err) {
console.error('Failed to start test daemon:', err)
throw err
console.error("Failed to start test daemon:", err);
throw err;
}
}
async function cleanup(): Promise<void> {
console.log('\nCleaning up...')
console.log("\nCleaning up...");
if (ctx) {
await ctx.stop()
console.log('Test daemon stopped and temp directories removed')
await ctx.stop();
console.log("Test daemon stopped and temp directories removed");
}
}
async function test_agent_ls_empty(): Promise<void> {
console.log('\n--- Test: agent ls with empty list ---')
console.log("\n--- Test: agent ls with empty list ---");
const result = await ctx.paseo(['ls', '--json'])
const result = await ctx.paseo(["ls", "--json"]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent ls should succeed')
assert.strictEqual(result.exitCode, 0, "agent ls should succeed");
const agents = JSON.parse(result.stdout.trim())
assert(Array.isArray(agents), 'Output should be JSON array')
assert.strictEqual(agents.length, 0, 'Should have no agents initially')
const agents = JSON.parse(result.stdout.trim());
assert(Array.isArray(agents), "Output should be JSON array");
assert.strictEqual(agents.length, 0, "Should have no agents initially");
console.log('PASS: agent ls returns empty list')
console.log("PASS: agent ls returns empty list");
}
async function test_agent_run_detached(): Promise<string> {
console.log('\n--- Test: agent run detached ---')
console.log("\n--- Test: agent run detached ---");
// Use quiet mode to get just the agent ID
// CRITICAL: Use haiku model for fast, cheap tests
// CRITICAL: Use bypassPermissions mode so agent doesn't wait for permission approvals
const result = await ctx.paseo(
[
'-q',
'run',
'-d',
'--provider',
'claude',
'--model',
'claude-3-5-haiku-20241022',
'--mode',
'bypassPermissions',
'--name',
'E2E Test Agent',
'Say hello world',
"-q",
"run",
"-d",
"--provider",
"claude",
"--model",
"claude-3-5-haiku-20241022",
"--mode",
"bypassPermissions",
"--name",
"E2E Test Agent",
"Say hello world",
],
{ timeout: 60000 }
)
{ timeout: 60000 },
);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent run should succeed')
assert.strictEqual(result.exitCode, 0, "agent run should succeed");
const agentId = result.stdout.trim()
assert(agentId.length > 0, 'Should return agent ID')
assert(agentId.match(/^[a-z0-9-]+$/), `Agent ID should be alphanumeric: ${agentId}`)
const agentId = result.stdout.trim();
assert(agentId.length > 0, "Should return agent ID");
assert(agentId.match(/^[a-z0-9-]+$/), `Agent ID should be alphanumeric: ${agentId}`);
console.log(`PASS: agent created with ID: ${agentId}`)
return agentId
console.log(`PASS: agent created with ID: ${agentId}`);
return agentId;
}
async function test_agent_ls_shows_agent(agentId: string): Promise<void> {
console.log('\n--- Test: agent ls shows created agent ---')
console.log("\n--- Test: agent ls shows created agent ---");
const result = await ctx.paseo(['ls', '--json'])
const result = await ctx.paseo(["ls", "--json"]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
assert.strictEqual(result.exitCode, 0, 'agent ls should succeed')
assert.strictEqual(result.exitCode, 0, "agent ls should succeed");
const agents = JSON.parse(result.stdout.trim()) as Array<{ id: string; title?: string; status?: string }>
assert(Array.isArray(agents), 'Output should be JSON array')
assert(agents.length >= 1, 'Should have at least one agent')
const agents = JSON.parse(result.stdout.trim()) as Array<{
id: string;
title?: string;
status?: string;
}>;
assert(Array.isArray(agents), "Output should be JSON array");
assert(agents.length >= 1, "Should have at least one agent");
// Find our agent by ID prefix
const ourAgent = agents.find((a) => agentId.startsWith(a.id) || a.id.startsWith(agentId.slice(0, 7)))
assert(ourAgent, `Our agent ${agentId} should be in the list`)
const ourAgent = agents.find(
(a) => agentId.startsWith(a.id) || a.id.startsWith(agentId.slice(0, 7)),
);
assert(ourAgent, `Our agent ${agentId} should be in the list`);
console.log('Agent found:', ourAgent)
console.log('PASS: agent ls shows created agent')
console.log("Agent found:", ourAgent);
console.log("PASS: agent ls shows created agent");
}
async function test_agent_wait(agentId: string): Promise<void> {
console.log('\n--- Test: agent wait ---')
console.log("\n--- Test: agent wait ---");
// Wait for agent to become idle (with generous timeout for haiku model)
const result = await ctx.paseo(['wait', '--timeout', '120s', agentId], { timeout: 130000 })
const result = await ctx.paseo(["wait", "--timeout", "120s", agentId], { timeout: 130000 });
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent wait should succeed')
assert.strictEqual(result.exitCode, 0, "agent wait should succeed");
console.log('PASS: agent wait completed successfully')
console.log("PASS: agent wait completed successfully");
}
async function test_agent_inspect(agentId: string): Promise<void> {
console.log('\n--- Test: agent inspect ---')
console.log("\n--- Test: agent inspect ---");
const result = await ctx.paseo(['inspect', agentId])
const result = await ctx.paseo(["inspect", agentId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
assert.strictEqual(result.exitCode, 0, 'agent inspect should succeed')
assert.strictEqual(result.exitCode, 0, "agent inspect should succeed");
// Check that output contains expected fields (table format)
const output = result.stdout
assert(output.includes('Id'), 'Output should contain Id field')
assert(output.includes('Provider'), 'Output should contain Provider field')
assert(output.includes('Status'), 'Output should contain Status field')
assert(output.includes('claude'), 'Output should mention claude provider')
const output = result.stdout;
assert(output.includes("Id"), "Output should contain Id field");
assert(output.includes("Provider"), "Output should contain Provider field");
assert(output.includes("Status"), "Output should contain Status field");
assert(output.includes("claude"), "Output should mention claude provider");
console.log('PASS: agent inspect shows expected fields')
console.log("PASS: agent inspect shows expected fields");
}
async function test_agent_logs(agentId: string): Promise<void> {
console.log('\n--- Test: agent logs ---')
console.log("\n--- Test: agent logs ---");
const result = await ctx.paseo(['logs', '--tail', '20', agentId])
const result = await ctx.paseo(["logs", "--tail", "20", agentId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout length:', result.stdout.length)
console.log("Exit code:", result.exitCode);
console.log("Stdout length:", result.stdout.length);
// Don't print full logs as they can be verbose
assert.strictEqual(result.exitCode, 0, 'agent logs should succeed')
assert.strictEqual(result.exitCode, 0, "agent logs should succeed");
// Logs should have some content (agent was asked to say hello)
assert(result.stdout.length > 0, 'Logs should have some content')
assert(result.stdout.length > 0, "Logs should have some content");
console.log('PASS: agent logs returns content')
console.log("PASS: agent logs returns content");
}
async function test_agent_stop(agentId: string): Promise<void> {
console.log('\n--- Test: agent stop ---')
console.log("\n--- Test: agent stop ---");
const result = await ctx.paseo(['stop', agentId])
const result = await ctx.paseo(["stop", agentId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent stop should succeed')
assert.strictEqual(result.exitCode, 0, "agent stop should succeed");
const inspectResult = await ctx.paseo(['inspect', agentId])
assert.strictEqual(inspectResult.exitCode, 0, 'agent should remain inspectable after stop')
assert(!inspectResult.stdout.toLowerCase().includes('running'), 'Agent should not be running after stop')
const inspectResult = await ctx.paseo(["inspect", agentId]);
assert.strictEqual(inspectResult.exitCode, 0, "agent should remain inspectable after stop");
assert(
!inspectResult.stdout.toLowerCase().includes("running"),
"Agent should not be running after stop",
);
console.log('PASS: agent stop interrupted without deleting the agent')
console.log("PASS: agent stop interrupted without deleting the agent");
}
async function test_agent_delete(agentId: string): Promise<void> {
console.log('\n--- Test: agent delete ---')
console.log("\n--- Test: agent delete ---");
const result = await ctx.paseo(['delete', agentId])
const result = await ctx.paseo(["delete", agentId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent delete should succeed')
assert.strictEqual(result.exitCode, 0, "agent delete should succeed");
const inspectResult = await ctx.paseo(['inspect', agentId])
assert.notStrictEqual(inspectResult.exitCode, 0, 'deleted agent should not be inspectable')
const inspectResult = await ctx.paseo(["inspect", agentId]);
assert.notStrictEqual(inspectResult.exitCode, 0, "deleted agent should not be inspectable");
console.log('PASS: agent delete removed the agent')
console.log("PASS: agent delete removed the agent");
}
async function main(): Promise<void> {
console.log('=== E2E Test: Agent Lifecycle ===\n')
console.log('This test creates REAL agents with REAL daemons.')
console.log('It may take some time as it waits for agent responses.\n')
console.log("=== E2E Test: Agent Lifecycle ===\n");
console.log("This test creates REAL agents with REAL daemons.");
console.log("It may take some time as it waits for agent responses.\n");
try {
await setup()
await setup();
// Run tests in sequence
await test_agent_ls_empty()
await test_agent_ls_empty();
const agentId = await test_agent_run_detached()
const agentId = await test_agent_run_detached();
await test_agent_ls_shows_agent(agentId)
await test_agent_ls_shows_agent(agentId);
await test_agent_wait(agentId)
await test_agent_wait(agentId);
await test_agent_inspect(agentId)
await test_agent_inspect(agentId);
await test_agent_logs(agentId)
await test_agent_logs(agentId);
await test_agent_stop(agentId)
await test_agent_delete(agentId)
await test_agent_stop(agentId);
await test_agent_delete(agentId);
console.log('\n=== All E2E tests passed! ===')
console.log("\n=== All E2E tests passed! ===");
} catch (err) {
console.error('\n=== E2E test FAILED ===')
console.error(err)
process.exitCode = 1
console.error("\n=== E2E test FAILED ===");
console.error(err);
process.exitCode = 1;
} finally {
await cleanup()
await cleanup();
}
}
main()
main();
+99 -95
View File
@@ -21,185 +21,189 @@
* - Clean up resources after test completes
*/
import assert from 'node:assert'
import { createE2ETestContext, type TestDaemonContext } from '../helpers/test-daemon.ts'
import assert from "node:assert";
import { createE2ETestContext, type TestDaemonContext } from "../helpers/test-daemon.ts";
interface E2EContext extends TestDaemonContext {
paseo: (args: string[], opts?: { timeout?: number; cwd?: string }) => Promise<{
exitCode: number
stdout: string
stderr: string
}>
paseo: (
args: string[],
opts?: { timeout?: number; cwd?: string },
) => Promise<{
exitCode: number;
stdout: string;
stderr: string;
}>;
}
let ctx: E2EContext
let ctx: E2EContext;
async function setup(): Promise<void> {
console.log('Setting up E2E test context...')
console.log('Starting test daemon on random port (this may take a few seconds)...')
console.log("Setting up E2E test context...");
console.log("Starting test daemon on random port (this may take a few seconds)...");
try {
ctx = await createE2ETestContext({ timeout: 45000 })
console.log(`Test daemon started on port ${ctx.port}`)
console.log(`PASEO_HOME: ${ctx.paseoHome}`)
console.log(`Work directory: ${ctx.workDir}`)
ctx = await createE2ETestContext({ timeout: 45000 });
console.log(`Test daemon started on port ${ctx.port}`);
console.log(`PASEO_HOME: ${ctx.paseoHome}`);
console.log(`Work directory: ${ctx.workDir}`);
} catch (err) {
console.error('Failed to start test daemon:', err)
throw err
console.error("Failed to start test daemon:", err);
throw err;
}
}
async function cleanup(): Promise<void> {
console.log('\nCleaning up...')
console.log("\nCleaning up...");
if (ctx) {
await ctx.stop()
console.log('Test daemon stopped and temp directories removed')
await ctx.stop();
console.log("Test daemon stopped and temp directories removed");
}
}
async function test_create_agent(): Promise<string> {
console.log('\n--- Test: Create agent in detached mode ---')
console.log("\n--- Test: Create agent in detached mode ---");
// CRITICAL: Use haiku model for fast, cheap tests
// CRITICAL: Use bypassPermissions mode so agent doesn't wait for permission approvals
const result = await ctx.paseo(
[
'-q',
'run',
'-d',
'--provider',
'claude',
'--model',
'claude-3-5-haiku-20241022',
'--mode',
'bypassPermissions',
'--name',
'Send Test Agent',
"-q",
"run",
"-d",
"--provider",
"claude",
"--model",
"claude-3-5-haiku-20241022",
"--mode",
"bypassPermissions",
"--name",
"Send Test Agent",
'Say "initial task complete"',
],
{ timeout: 60000 }
)
{ timeout: 60000 },
);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent run should succeed')
assert.strictEqual(result.exitCode, 0, "agent run should succeed");
const agentId = result.stdout.trim()
assert(agentId.length > 0, 'Should return agent ID')
assert(agentId.match(/^[a-z0-9-]+$/), `Agent ID should be alphanumeric: ${agentId}`)
const agentId = result.stdout.trim();
assert(agentId.length > 0, "Should return agent ID");
assert(agentId.match(/^[a-z0-9-]+$/), `Agent ID should be alphanumeric: ${agentId}`);
console.log(`PASS: agent created with ID: ${agentId}`)
return agentId
console.log(`PASS: agent created with ID: ${agentId}`);
return agentId;
}
async function test_wait_for_initial_task(agentId: string): Promise<void> {
console.log('\n--- Test: Wait for initial task to complete ---')
console.log("\n--- Test: Wait for initial task to complete ---");
const result = await ctx.paseo(['wait', '--timeout', '120s', agentId], { timeout: 130000 })
const result = await ctx.paseo(["wait", "--timeout", "120s", agentId], { timeout: 130000 });
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent wait should succeed')
assert.strictEqual(result.exitCode, 0, "agent wait should succeed");
console.log('PASS: Initial task completed')
console.log("PASS: Initial task completed");
}
async function test_agent_send(agentId: string): Promise<void> {
console.log('\n--- Test: Send follow-up message ---')
console.log("\n--- Test: Send follow-up message ---");
// Send a follow-up message to the agent
const result = await ctx.paseo(
['send', agentId, 'Now say "follow-up task complete"'],
{ timeout: 180000 }
)
const result = await ctx.paseo(["send", agentId, 'Now say "follow-up task complete"'], {
timeout: 180000,
});
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent send should succeed')
assert.strictEqual(result.exitCode, 0, "agent send should succeed");
// Verify output contains expected status
assert(
result.stdout.includes('completed') || result.stdout.includes('sent'),
'Should indicate message was sent or completed'
)
result.stdout.includes("completed") || result.stdout.includes("sent"),
"Should indicate message was sent or completed",
);
console.log('PASS: Follow-up message sent successfully')
console.log("PASS: Follow-up message sent successfully");
}
async function test_verify_logs(agentId: string): Promise<void> {
console.log('\n--- Test: Verify agent processed both messages ---')
console.log("\n--- Test: Verify agent processed both messages ---");
const result = await ctx.paseo(['logs', '--tail', '50', agentId])
const result = await ctx.paseo(["logs", "--tail", "50", agentId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout length:', result.stdout.length)
console.log("Exit code:", result.exitCode);
console.log("Stdout length:", result.stdout.length);
assert.strictEqual(result.exitCode, 0, 'agent logs should succeed')
assert.strictEqual(result.exitCode, 0, "agent logs should succeed");
// Logs should have content from both tasks
assert(result.stdout.length > 0, 'Logs should have content')
assert(result.stdout.length > 0, "Logs should have content");
// The agent was asked to say specific things, check if either appears in logs
// This is a loose check since exact log format may vary
const logsLower = result.stdout.toLowerCase()
const hasInitialTask = logsLower.includes('initial') || logsLower.includes('hello') || logsLower.includes('task')
const hasFollowUp = logsLower.includes('follow') || logsLower.includes('complete') || logsLower.includes('task')
const logsLower = result.stdout.toLowerCase();
const hasInitialTask =
logsLower.includes("initial") || logsLower.includes("hello") || logsLower.includes("task");
const hasFollowUp =
logsLower.includes("follow") || logsLower.includes("complete") || logsLower.includes("task");
// At minimum, there should be log entries (we can't guarantee exact content)
assert(result.stdout.split('\n').length > 3, 'Should have multiple log entries from both tasks')
assert(result.stdout.split("\n").length > 3, "Should have multiple log entries from both tasks");
console.log('PASS: Agent logs show activity from both tasks')
console.log("PASS: Agent logs show activity from both tasks");
}
async function test_agent_delete(agentId: string): Promise<void> {
console.log('\n--- Test: Delete agent ---')
console.log("\n--- Test: Delete agent ---");
const result = await ctx.paseo(['delete', agentId])
const result = await ctx.paseo(["delete", agentId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent delete should succeed')
assert.strictEqual(result.exitCode, 0, "agent delete should succeed");
console.log('PASS: Agent deleted successfully')
console.log("PASS: Agent deleted successfully");
}
async function main(): Promise<void> {
console.log('=== E2E Test: Agent Send Command ===\n')
console.log('This test verifies the `send` command with REAL agents.')
console.log('It may take some time as it waits for agent responses.\n')
console.log("=== E2E Test: Agent Send Command ===\n");
console.log("This test verifies the `send` command with REAL agents.");
console.log("It may take some time as it waits for agent responses.\n");
try {
await setup()
await setup();
// Create agent and wait for initial task
const agentId = await test_create_agent()
await test_wait_for_initial_task(agentId)
const agentId = await test_create_agent();
await test_wait_for_initial_task(agentId);
// Send follow-up message (this waits for completion by default)
await test_agent_send(agentId)
await test_agent_send(agentId);
// Verify both messages were processed
await test_verify_logs(agentId)
await test_verify_logs(agentId);
// Cleanup agent
await test_agent_delete(agentId)
await test_agent_delete(agentId);
console.log('\n=== All agent-send E2E tests passed! ===')
console.log("\n=== All agent-send E2E tests passed! ===");
} catch (err) {
console.error('\n=== E2E test FAILED ===')
console.error(err)
process.exitCode = 1
console.error("\n=== E2E test FAILED ===");
console.error(err);
process.exitCode = 1;
} finally {
await cleanup()
await cleanup();
}
}
main()
main();
+139 -126
View File
@@ -21,106 +21,111 @@
* - Clean up resources after test completes
*/
import assert from 'node:assert'
import { createE2ETestContext, type TestDaemonContext } from '../helpers/test-daemon.ts'
import assert from "node:assert";
import { createE2ETestContext, type TestDaemonContext } from "../helpers/test-daemon.ts";
interface E2EContext extends TestDaemonContext {
paseo: (args: string[], opts?: { timeout?: number; cwd?: string }) => Promise<{
exitCode: number
stdout: string
stderr: string
}>
paseo: (
args: string[],
opts?: { timeout?: number; cwd?: string },
) => Promise<{
exitCode: number;
stdout: string;
stderr: string;
}>;
}
let ctx: E2EContext
let ctx: E2EContext;
async function setup(): Promise<void> {
console.log('Setting up E2E test context...')
console.log('Starting test daemon on random port (this may take a few seconds)...')
console.log("Setting up E2E test context...");
console.log("Starting test daemon on random port (this may take a few seconds)...");
try {
ctx = await createE2ETestContext({ timeout: 45000 })
console.log(`Test daemon started on port ${ctx.port}`)
console.log(`PASEO_HOME: ${ctx.paseoHome}`)
console.log(`Work directory: ${ctx.workDir}`)
ctx = await createE2ETestContext({ timeout: 45000 });
console.log(`Test daemon started on port ${ctx.port}`);
console.log(`PASEO_HOME: ${ctx.paseoHome}`);
console.log(`Work directory: ${ctx.workDir}`);
} catch (err) {
console.error('Failed to start test daemon:', err)
throw err
console.error("Failed to start test daemon:", err);
throw err;
}
}
async function cleanup(): Promise<void> {
console.log('\nCleaning up...')
console.log("\nCleaning up...");
if (ctx) {
await ctx.stop()
console.log('Test daemon stopped and temp directories removed')
await ctx.stop();
console.log("Test daemon stopped and temp directories removed");
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function test_create_agent_with_permissions(): Promise<string> {
console.log('\n--- Test: Create agent in default mode (will request permissions) ---')
console.log("\n--- Test: Create agent in default mode (will request permissions) ---");
// Create agent WITHOUT bypassPermissions - it will need to request permissions
// Use a task that is very likely to trigger a tool use (and thus permission request)
const result = await ctx.paseo(
[
'-q',
'agent',
'run',
'-d',
'--provider',
'claude',
'--model',
'claude-3-5-haiku-20241022',
'--name',
'Permission Test Agent',
"-q",
"agent",
"run",
"-d",
"--provider",
"claude",
"--model",
"claude-3-5-haiku-20241022",
"--name",
"Permission Test Agent",
// This prompt should trigger file read or bash command, requiring permission
'List the files in the current directory using ls',
"List the files in the current directory using ls",
],
{ timeout: 60000 }
)
{ timeout: 60000 },
);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent run should succeed')
assert.strictEqual(result.exitCode, 0, "agent run should succeed");
const agentId = result.stdout.trim()
assert(agentId.length > 0, 'Should return agent ID')
assert(agentId.match(/^[a-z0-9-]+$/), `Agent ID should be alphanumeric: ${agentId}`)
const agentId = result.stdout.trim();
assert(agentId.length > 0, "Should return agent ID");
assert(agentId.match(/^[a-z0-9-]+$/), `Agent ID should be alphanumeric: ${agentId}`);
console.log(`PASS: agent created with ID: ${agentId}`)
return agentId
console.log(`PASS: agent created with ID: ${agentId}`);
return agentId;
}
async function test_wait_for_permission_request(agentId: string): Promise<void> {
console.log('\n--- Test: Wait for agent to request permission ---')
console.log("\n--- Test: Wait for agent to request permission ---");
// Poll for permission requests with timeout
const maxWait = 60000 // 60 seconds max
const pollInterval = 1000 // 1 second
const startTime = Date.now()
const maxWait = 60000; // 60 seconds max
const pollInterval = 1000; // 1 second
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
const result = await ctx.paseo(['permit', 'ls', '--json'])
const result = await ctx.paseo(["permit", "ls", "--json"]);
if (result.exitCode === 0) {
try {
const permissions = JSON.parse(result.stdout.trim())
const permissions = JSON.parse(result.stdout.trim());
if (Array.isArray(permissions) && permissions.length > 0) {
// Check if any permission is for our agent
const ourPermission = permissions.find(
(p: { agentId?: string }) => p.agentId?.startsWith(agentId.slice(0, 7)) || agentId.startsWith(p.agentId?.slice(0, 7) || '')
)
(p: { agentId?: string }) =>
p.agentId?.startsWith(agentId.slice(0, 7)) ||
agentId.startsWith(p.agentId?.slice(0, 7) || ""),
);
if (ourPermission) {
console.log('Permission request detected:', ourPermission)
console.log('PASS: Agent requested permission')
return
console.log("Permission request detected:", ourPermission);
console.log("PASS: Agent requested permission");
return;
}
}
} catch {
@@ -128,158 +133,166 @@ async function test_wait_for_permission_request(agentId: string): Promise<void>
}
}
await sleep(pollInterval)
await sleep(pollInterval);
}
// If we get here, check agent status - it might have already completed
const statusResult = await ctx.paseo(['inspect', agentId])
console.log('Agent status:', statusResult.stdout)
const statusResult = await ctx.paseo(["inspect", agentId]);
console.log("Agent status:", statusResult.stdout);
// It's possible the agent completed without needing permissions (e.g., haiku might not use tools)
// In that case, we should skip the permission tests
if (statusResult.stdout.includes('idle') || statusResult.stdout.includes('completed')) {
console.log('SKIP: Agent completed without requiring permissions (model chose not to use tools)')
throw new Error('SKIP_PERMISSION_TEST')
if (statusResult.stdout.includes("idle") || statusResult.stdout.includes("completed")) {
console.log(
"SKIP: Agent completed without requiring permissions (model chose not to use tools)",
);
throw new Error("SKIP_PERMISSION_TEST");
}
throw new Error('Timeout waiting for agent to request permission')
throw new Error("Timeout waiting for agent to request permission");
}
async function test_permit_ls(): Promise<{ agentShortId: string; requestId: string }> {
console.log('\n--- Test: List pending permissions with permit ls ---')
console.log("\n--- Test: List pending permissions with permit ls ---");
const result = await ctx.paseo(['permit', 'ls', '--json'])
const result = await ctx.paseo(["permit", "ls", "--json"]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'permit ls should succeed')
assert.strictEqual(result.exitCode, 0, "permit ls should succeed");
const permissions = JSON.parse(result.stdout.trim())
assert(Array.isArray(permissions), 'Output should be JSON array')
assert(permissions.length > 0, 'Should have at least one pending permission')
const permissions = JSON.parse(result.stdout.trim());
assert(Array.isArray(permissions), "Output should be JSON array");
assert(permissions.length > 0, "Should have at least one pending permission");
const permission = permissions[0]
assert(permission.id, 'Permission should have id')
assert(permission.agentShortId, 'Permission should have agentShortId')
assert(permission.name, 'Permission should have tool name')
const permission = permissions[0];
assert(permission.id, "Permission should have id");
assert(permission.agentShortId, "Permission should have agentShortId");
assert(permission.name, "Permission should have tool name");
console.log(`PASS: Found ${permissions.length} pending permission(s)`)
console.log(` Request ID: ${permission.id}`)
console.log(` Agent: ${permission.agentShortId}`)
console.log(` Tool: ${permission.name}`)
console.log(`PASS: Found ${permissions.length} pending permission(s)`);
console.log(` Request ID: ${permission.id}`);
console.log(` Agent: ${permission.agentShortId}`);
console.log(` Tool: ${permission.name}`);
return {
agentShortId: permission.agentShortId,
requestId: permission.id,
}
};
}
async function test_permit_allow(agentShortId: string, requestId: string): Promise<void> {
console.log('\n--- Test: Allow permission with permit allow ---')
console.log("\n--- Test: Allow permission with permit allow ---");
const result = await ctx.paseo(['permit', 'allow', agentShortId, requestId])
const result = await ctx.paseo(["permit", "allow", agentShortId, requestId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'permit allow should succeed')
assert.strictEqual(result.exitCode, 0, "permit allow should succeed");
// Check that output shows the permission was allowed
const output = result.stdout.toLowerCase()
assert(output.includes('allowed') || output.includes(requestId.slice(0, 8).toLowerCase()), 'Should confirm permission was allowed')
const output = result.stdout.toLowerCase();
assert(
output.includes("allowed") || output.includes(requestId.slice(0, 8).toLowerCase()),
"Should confirm permission was allowed",
);
console.log('PASS: Permission allowed successfully')
console.log("PASS: Permission allowed successfully");
}
async function test_agent_continues(agentId: string): Promise<void> {
console.log('\n--- Test: Verify agent continues after permission granted ---')
console.log("\n--- Test: Verify agent continues after permission granted ---");
// Wait for agent to become idle (should continue after permission)
const result = await ctx.paseo(['wait', '--timeout', '120s', agentId], { timeout: 130000 })
const result = await ctx.paseo(["wait", "--timeout", "120s", agentId], { timeout: 130000 });
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent wait should succeed after permission granted')
assert.strictEqual(result.exitCode, 0, "agent wait should succeed after permission granted");
// Verify agent status
const inspectResult = await ctx.paseo(['inspect', agentId])
console.log('Final agent status:', inspectResult.stdout)
const inspectResult = await ctx.paseo(["inspect", agentId]);
console.log("Final agent status:", inspectResult.stdout);
assert(inspectResult.stdout.includes('idle') || inspectResult.stdout.includes('completed'), 'Agent should be idle after completing')
assert(
inspectResult.stdout.includes("idle") || inspectResult.stdout.includes("completed"),
"Agent should be idle after completing",
);
console.log('PASS: Agent completed after permission was granted')
console.log("PASS: Agent completed after permission was granted");
}
async function test_agent_delete(agentId: string): Promise<void> {
console.log('\n--- Test: Delete agent ---')
console.log("\n--- Test: Delete agent ---");
const result = await ctx.paseo(['delete', agentId])
const result = await ctx.paseo(["delete", agentId]);
console.log('Exit code:', result.exitCode)
console.log('Stdout:', result.stdout)
if (result.stderr) console.log('Stderr:', result.stderr)
console.log("Exit code:", result.exitCode);
console.log("Stdout:", result.stdout);
if (result.stderr) console.log("Stderr:", result.stderr);
assert.strictEqual(result.exitCode, 0, 'agent delete should succeed')
assert.strictEqual(result.exitCode, 0, "agent delete should succeed");
console.log('PASS: Agent deleted successfully')
console.log("PASS: Agent deleted successfully");
}
async function main(): Promise<void> {
console.log('=== E2E Test: Permissions Workflow ===\n')
console.log('This test verifies the permission request/allow workflow.')
console.log('It creates an agent that will request tool permissions.\n')
console.log("=== E2E Test: Permissions Workflow ===\n");
console.log("This test verifies the permission request/allow workflow.");
console.log("It creates an agent that will request tool permissions.\n");
let agentId: string | undefined
let agentId: string | undefined;
try {
await setup()
await setup();
// Create agent that will need permissions
agentId = await test_create_agent_with_permissions()
agentId = await test_create_agent_with_permissions();
try {
// Wait for permission request
await test_wait_for_permission_request(agentId)
await test_wait_for_permission_request(agentId);
// List and verify permissions
const { agentShortId, requestId } = await test_permit_ls()
const { agentShortId, requestId } = await test_permit_ls();
// Allow the permission
await test_permit_allow(agentShortId, requestId)
await test_permit_allow(agentShortId, requestId);
// Verify agent continues and completes
await test_agent_continues(agentId)
await test_agent_continues(agentId);
console.log('\n=== All permissions E2E tests passed! ===')
console.log("\n=== All permissions E2E tests passed! ===");
} catch (err) {
if (err instanceof Error && err.message === 'SKIP_PERMISSION_TEST') {
console.log('\n=== Permissions test skipped (agent completed without tool use) ===')
console.log('This can happen when the model chooses not to use tools.')
if (err instanceof Error && err.message === "SKIP_PERMISSION_TEST") {
console.log("\n=== Permissions test skipped (agent completed without tool use) ===");
console.log("This can happen when the model chooses not to use tools.");
// Still considered a pass - the CLI works, just the model behavior varies
} else {
throw err
throw err;
}
}
} catch (err) {
console.error('\n=== E2E test FAILED ===')
console.error(err)
process.exitCode = 1
console.error("\n=== E2E test FAILED ===");
console.error(err);
process.exitCode = 1;
} finally {
// Always try to delete the agent if it was created
if (agentId) {
try {
await test_agent_delete(agentId)
await test_agent_delete(agentId);
} catch {
// Ignore errors during cleanup
}
}
await cleanup()
await cleanup();
}
}
main()
main();
@@ -1,141 +1,144 @@
#!/usr/bin/env npx tsx
import assert from 'node:assert'
import { createE2ETestContext, type TestDaemonContext } from '../helpers/test-daemon.ts'
import assert from "node:assert";
import { createE2ETestContext, type TestDaemonContext } from "../helpers/test-daemon.ts";
interface E2EContext extends TestDaemonContext {
paseo: (args: string[], opts?: { timeout?: number; cwd?: string }) => Promise<{
exitCode: number
stdout: string
stderr: string
}>
paseo: (
args: string[],
opts?: { timeout?: number; cwd?: string },
) => Promise<{
exitCode: number;
stdout: string;
stderr: string;
}>;
}
const schema = JSON.stringify({
type: 'object',
type: "object",
properties: {
summary: { type: 'string' },
summary: { type: "string" },
},
required: ['summary'],
required: ["summary"],
additionalProperties: false,
})
});
const impossibleSchema = JSON.stringify({
type: 'object',
type: "object",
properties: {
summary: { type: 'string' },
summary: { type: "string" },
},
required: ['summary'],
required: ["summary"],
allOf: [
{ properties: { summary: { type: 'string' } } },
{ properties: { summary: { type: 'number' } } },
{ properties: { summary: { type: "string" } } },
{ properties: { summary: { type: "number" } } },
],
})
});
const OPEN_CODE_STRUCTURED_MODEL = 'opencode/gpt-5-nano'
const OPEN_CODE_STRUCTURED_MODEL = "opencode/gpt-5-nano";
let ctx: E2EContext
let ctx: E2EContext;
async function setup(): Promise<void> {
ctx = await createE2ETestContext({ timeout: 180000 })
ctx = await createE2ETestContext({ timeout: 180000 });
}
async function cleanup(): Promise<void> {
if (ctx) {
await ctx.stop()
await ctx.stop();
}
}
async function runProviderCase(input: {
provider: 'claude' | 'codex' | 'opencode'
mode: string
model: string
provider: "claude" | "codex" | "opencode";
mode: string;
model: string;
}): Promise<void> {
const result = await ctx.paseo(
[
'run',
'--provider',
"run",
"--provider",
input.provider,
'--mode',
"--mode",
input.mode,
'--model',
"--model",
input.model,
'--output-schema',
"--output-schema",
schema,
`Return valid JSON with a short summary for provider ${input.provider}.`,
],
{ timeout: 180000 }
)
{ timeout: 180000 },
);
assert.strictEqual(
result.exitCode,
0,
`expected ${input.provider} run to succeed, got ${result.exitCode}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`
)
`expected ${input.provider} run to succeed, got ${result.exitCode}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
const parsed = JSON.parse(result.stdout.trim()) as { summary?: unknown }
const parsed = JSON.parse(result.stdout.trim()) as { summary?: unknown };
assert.strictEqual(
typeof parsed.summary,
'string',
`expected ${input.provider} output to contain string summary, got: ${result.stdout}`
)
assert(parsed.summary && parsed.summary.length > 0, 'summary must not be empty')
"string",
`expected ${input.provider} output to contain string summary, got: ${result.stdout}`,
);
assert(parsed.summary && parsed.summary.length > 0, "summary must not be empty");
}
async function test_all_providers_return_structured_output(): Promise<void> {
await runProviderCase({
provider: 'claude',
mode: 'bypassPermissions',
model: 'haiku',
})
provider: "claude",
mode: "bypassPermissions",
model: "haiku",
});
await runProviderCase({
provider: 'codex',
mode: 'full-access',
model: 'gpt-5.3-codex',
})
provider: "codex",
mode: "full-access",
model: "gpt-5.3-codex",
});
await runProviderCase({
provider: 'opencode',
mode: 'default',
provider: "opencode",
mode: "default",
model: OPEN_CODE_STRUCTURED_MODEL,
})
});
}
async function test_schema_validation_is_enforced(): Promise<void> {
const result = await ctx.paseo(
[
'run',
'--provider',
'claude',
'--mode',
'bypassPermissions',
'--model',
'haiku',
'--output-schema',
"run",
"--provider",
"claude",
"--mode",
"bypassPermissions",
"--model",
"haiku",
"--output-schema",
impossibleSchema,
'Return exactly {"summary":"ok"} and nothing else.',
],
{ timeout: 180000 }
)
{ timeout: 180000 },
);
assert.notStrictEqual(result.exitCode, 0, 'expected impossible schema to fail')
const output = `${result.stdout}\n${result.stderr}`
assert.notStrictEqual(result.exitCode, 0, "expected impossible schema to fail");
const output = `${result.stdout}\n${result.stderr}`;
assert(
output.includes('OUTPUT_SCHEMA_FAILED'),
`expected OUTPUT_SCHEMA_FAILED for impossible schema\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`
)
output.includes("OUTPUT_SCHEMA_FAILED"),
`expected OUTPUT_SCHEMA_FAILED for impossible schema\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
async function main(): Promise<void> {
try {
await setup()
await test_all_providers_return_structured_output()
await test_schema_validation_is_enforced()
await setup();
await test_all_providers_return_structured_output();
await test_schema_validation_is_enforced();
} catch (error) {
console.error(error)
process.exitCode = 1
console.error(error);
process.exitCode = 1;
} finally {
await cleanup()
await cleanup();
}
}
main()
main();
+5 -5
View File
@@ -1,9 +1,9 @@
import { $, ProcessPromise } from 'zx'
import { join } from 'node:path'
import { $, ProcessPromise } from "zx";
import { join } from "node:path";
const CLI_ENTRY = join(import.meta.dirname, '..', '..', 'dist', 'index.js')
const CLI_ENTRY = join(import.meta.dirname, "..", "..", "dist", "index.js");
export function runLocalPaseo(args: string[], env: NodeJS.ProcessEnv = {}): ProcessPromise {
$.verbose = false
return $({ env: { ...process.env, ...env } })`${process.execPath} ${CLI_ENTRY} ${args}`.nothrow()
$.verbose = false;
return $({ env: { ...process.env, ...env } })`${process.execPath} ${CLI_ENTRY} ${args}`.nothrow();
}
+17 -17
View File
@@ -1,4 +1,4 @@
import { createServer } from 'node:net'
import { createServer } from "node:net";
/**
* Reserve and release an ephemeral TCP port, returning the port number.
@@ -6,27 +6,27 @@ import { createServer } from 'node:net'
*/
export async function getAvailablePort(): Promise<number> {
return new Promise<number>((resolve, reject) => {
const server = createServer()
server.unref()
const server = createServer();
server.unref();
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
const address = server.address()
if (!address || typeof address === 'string') {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close(() => {
reject(new Error('Failed to resolve an available TCP port'))
})
return
reject(new Error("Failed to resolve an available TCP port"));
});
return;
}
const { port } = address
const { port } = address;
server.close((error) => {
if (error) {
reject(error)
return
reject(error);
return;
}
resolve(port)
})
})
})
resolve(port);
});
});
});
}
+174 -175
View File
@@ -12,139 +12,139 @@
* 5. Cleanup: Kill daemon and remove temp dirs after each test
*/
import { mkdtemp, rm, mkdir } from 'fs/promises'
import { existsSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { ChildProcess, spawn } from 'child_process'
import { getAvailablePort } from './network.ts'
import { mkdtemp, rm, mkdir } from "fs/promises";
import { existsSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { ChildProcess, spawn } from "child_process";
import { getAvailablePort } from "./network.ts";
export interface TestDaemonContext {
/** Available local port for test daemon (never 6767) */
port: number
port: number;
/** WebSocket URL for connecting to daemon */
wsUrl: string
wsUrl: string;
/** Temp directory for PASEO_HOME */
paseoHome: string
paseoHome: string;
/** Temp directory for agent working directory */
workDir: string
workDir: string;
/** Running daemon process */
process: ChildProcess | null
process: ChildProcess | null;
/** Whether the daemon is ready to accept connections */
isReady: boolean
isReady: boolean;
/** Stop the daemon and clean up resources */
stop: () => Promise<void>
stop: () => Promise<void>;
}
const TEST_DAEMON_ENV_DEFAULTS: Record<string, string> = {
PASEO_RELAY_ENABLED: 'false',
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
const TEST_DAEMON_HOST = '127.0.0.1'
PASEO_RELAY_ENABLED: "false",
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
const TEST_DAEMON_HOST = "127.0.0.1";
const DEFAULT_OUTPUT_CAPTURE_LIMIT = 256 * 1024
const DEFAULT_OUTPUT_CAPTURE_LIMIT = 256 * 1024;
const TEST_OUTPUT_CAPTURE_LIMIT = Number.parseInt(
process.env.PASEO_TEST_OUTPUT_CAPTURE_BYTES ?? `${DEFAULT_OUTPUT_CAPTURE_LIMIT}`,
10
)
10,
);
type OutputCapture = {
value: string
truncated: boolean
}
value: string;
truncated: boolean;
};
function createOutputCapture(): OutputCapture {
return { value: '', truncated: false }
return { value: "", truncated: false };
}
function appendOutputCapture(target: OutputCapture, chunk: Buffer): void {
const next = target.value + chunk.toString()
const next = target.value + chunk.toString();
if (next.length <= TEST_OUTPUT_CAPTURE_LIMIT) {
target.value = next
return
target.value = next;
return;
}
target.truncated = true
target.value = next.slice(next.length - TEST_OUTPUT_CAPTURE_LIMIT)
target.truncated = true;
target.value = next.slice(next.length - TEST_OUTPUT_CAPTURE_LIMIT);
}
function formatOutputCapture(target: OutputCapture): string {
if (!target.truncated) {
return target.value
return target.value;
}
return `[truncated; showing last ${TEST_OUTPUT_CAPTURE_LIMIT} chars]\n${target.value}`
return `[truncated; showing last ${TEST_OUTPUT_CAPTURE_LIMIT} chars]\n${target.value}`;
}
function readNodeErrnoCode(error: unknown): string | undefined {
if (typeof error !== 'object' || error === null || !('code' in error)) {
return undefined
if (typeof error !== "object" || error === null || !("code" in error)) {
return undefined;
}
return typeof (error as { code?: unknown }).code === 'string'
? ((error as { code: string }).code)
: undefined
return typeof (error as { code?: unknown }).code === "string"
? (error as { code: string }).code
: undefined;
}
function signalProcessTree(pid: number, signal: NodeJS.Signals): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false
return false;
}
if (process.platform !== 'win32') {
if (process.platform !== "win32") {
try {
process.kill(-pid, signal)
return true
process.kill(-pid, signal);
return true;
} catch (error) {
const code = readNodeErrnoCode(error)
if (code === 'ESRCH') {
return false
const code = readNodeErrnoCode(error);
if (code === "ESRCH") {
return false;
}
}
}
try {
process.kill(pid, signal)
return true
process.kill(pid, signal);
return true;
} catch (error) {
const code = readNodeErrnoCode(error)
if (code === 'ESRCH') {
return false
const code = readNodeErrnoCode(error);
if (code === "ESRCH") {
return false;
}
throw error
throw error;
}
}
async function terminateProcessTree(processRef: ChildProcess, timeoutMs: number): Promise<void> {
const pid = processRef.pid
const pid = processRef.pid;
if (!Number.isInteger(pid) || pid <= 0) {
return
return;
}
if (processRef.exitCode !== null || processRef.signalCode !== null) {
return
return;
}
signalProcessTree(pid, 'SIGTERM')
signalProcessTree(pid, "SIGTERM");
await new Promise<void>((resolve) => {
let settled = false
let settled = false;
const finish = () => {
if (settled) {
return
return;
}
settled = true
clearTimeout(timeoutId)
resolve()
}
settled = true;
clearTimeout(timeoutId);
resolve();
};
const timeoutId = setTimeout(() => {
signalProcessTree(pid, 'SIGKILL')
finish()
}, timeoutMs)
signalProcessTree(pid, "SIGKILL");
finish();
}, timeoutMs);
processRef.once('exit', () => {
finish()
})
})
processRef.once("exit", () => {
finish();
});
});
}
/**
@@ -153,32 +153,29 @@ async function terminateProcessTree(processRef: ChildProcess, timeoutMs: number)
* NEVER uses 6767 (user's running daemon)
*/
export function getRandomPort(): number {
return 20000 + Math.floor(Math.random() * 10000)
return 20000 + Math.floor(Math.random() * 10000);
}
/**
* Create isolated temp directories for testing
*/
export async function createTempDirs(): Promise<{ paseoHome: string; workDir: string }> {
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-e2e-home-'))
const workDir = await mkdtemp(join(tmpdir(), 'paseo-e2e-work-'))
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-e2e-home-"));
const workDir = await mkdtemp(join(tmpdir(), "paseo-e2e-work-"));
// Create the agents directory that the daemon expects
const agentsDir = join(paseoHome, 'agents')
await mkdir(agentsDir, { recursive: true })
const agentsDir = join(paseoHome, "agents");
await mkdir(agentsDir, { recursive: true });
return { paseoHome, workDir }
return { paseoHome, workDir };
}
/**
* Wait for daemon to be ready by running `paseo agent ls`
* This connects via WebSocket and ensures the daemon is responsive
*/
async function waitForDaemonReady(
port: number,
timeout = 30000
): Promise<void> {
const start = Date.now()
async function waitForDaemonReady(port: number, timeout = 30000): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
@@ -186,28 +183,28 @@ async function waitForDaemonReady(
{
port,
wsUrl: `ws://${TEST_DAEMON_HOST}:${port}`,
paseoHome: '',
workDir: '',
paseoHome: "",
workDir: "",
process: null,
isReady: false,
stop: async () => {}
stop: async () => {},
},
['agent', 'ls']
)
["agent", "ls"],
);
if (exitCode === 0) {
return // Daemon is ready
return; // Daemon is ready
}
} catch {
// Connection failed, keep trying
}
await sleep(100)
await sleep(100);
}
throw new Error(`Daemon failed to become ready on port ${port} within ${timeout}ms`)
throw new Error(`Daemon failed to become ready on port ${port} within ${timeout}ms`);
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
@@ -217,57 +214,58 @@ function sleep(ms: number): Promise<void> {
* with isolated PASEO_HOME and PASEO_LISTEN environment variables.
*/
export async function startTestDaemon(options?: {
port?: number
paseoHome?: string
workDir?: string
timeout?: number
port?: number;
paseoHome?: string;
workDir?: string;
timeout?: number;
}): Promise<TestDaemonContext> {
const port = options?.port ?? await getAvailablePort()
const { paseoHome, workDir } = options?.paseoHome && options?.workDir
? { paseoHome: options.paseoHome, workDir: options.workDir }
: await createTempDirs()
const timeout = options?.timeout ?? 30000
const port = options?.port ?? (await getAvailablePort());
const { paseoHome, workDir } =
options?.paseoHome && options?.workDir
? { paseoHome: options.paseoHome, workDir: options.workDir }
: await createTempDirs();
const timeout = options?.timeout ?? 30000;
const wsUrl = `ws://${TEST_DAEMON_HOST}:${port}`
const wsUrl = `ws://${TEST_DAEMON_HOST}:${port}`;
// Find the CLI entry point - use the source file directly with tsx
const cliDir = join(import.meta.dirname, '..', '..')
const cliSrcPath = join(cliDir, 'src', 'index.ts')
const cliDir = join(import.meta.dirname, "..", "..");
const cliSrcPath = join(cliDir, "src", "index.ts");
// Start daemon process using tsx to run TypeScript directly
const daemonProcess = spawn('npx', ['tsx', cliSrcPath, 'daemon', 'start', '--foreground'], {
const daemonProcess = spawn("npx", ["tsx", cliSrcPath, "daemon", "start", "--foreground"], {
env: {
...process.env,
...TEST_DAEMON_ENV_DEFAULTS,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `${TEST_DAEMON_HOST}:${port}`,
// Force no TTY to prevent QR code output
CI: 'true',
CI: "true",
},
stdio: ['ignore', 'pipe', 'pipe'],
detached: process.platform !== 'win32',
})
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
});
const stdout = createOutputCapture()
const stderr = createOutputCapture()
const stdout = createOutputCapture();
const stderr = createOutputCapture();
daemonProcess.stdout?.on('data', (data) => {
appendOutputCapture(stdout, data)
})
daemonProcess.stdout?.on("data", (data) => {
appendOutputCapture(stdout, data);
});
daemonProcess.stderr?.on('data', (data) => {
appendOutputCapture(stderr, data)
})
daemonProcess.stderr?.on("data", (data) => {
appendOutputCapture(stderr, data);
});
const cleanup = async () => {
if (daemonProcess) {
await terminateProcessTree(daemonProcess, 5000)
await terminateProcessTree(daemonProcess, 5000);
}
// Clean up temp directories
try {
if (existsSync(paseoHome)) {
await rm(paseoHome, { recursive: true, force: true })
await rm(paseoHome, { recursive: true, force: true });
}
} catch {
// Ignore cleanup errors
@@ -275,27 +273,27 @@ export async function startTestDaemon(options?: {
try {
if (existsSync(workDir)) {
await rm(workDir, { recursive: true, force: true })
await rm(workDir, { recursive: true, force: true });
}
} catch {
// Ignore cleanup errors
}
}
};
// Handle process errors
daemonProcess.on('error', (err) => {
console.error('Daemon process error:', err)
})
daemonProcess.on("error", (err) => {
console.error("Daemon process error:", err);
});
daemonProcess.on('exit', (code) => {
daemonProcess.on("exit", (code) => {
if (code !== 0 && code !== null) {
console.error(`Daemon process exited with code ${code}`)
const stderrText = formatOutputCapture(stderr)
console.error(`Daemon process exited with code ${code}`);
const stderrText = formatOutputCapture(stderr);
if (stderrText) {
console.error('Daemon stderr:', stderrText)
console.error("Daemon stderr:", stderrText);
}
}
})
});
const ctx: TestDaemonContext = {
port,
@@ -305,22 +303,22 @@ export async function startTestDaemon(options?: {
process: daemonProcess,
isReady: false,
stop: cleanup,
}
};
// Wait for daemon to be ready
try {
await waitForDaemonReady(port, timeout)
ctx.isReady = true
await waitForDaemonReady(port, timeout);
ctx.isReady = true;
} catch (err) {
// Daemon failed to start - clean up and rethrow
await cleanup()
const message = err instanceof Error ? err.message : String(err)
await cleanup();
const message = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to start test daemon: ${message}\nStdout: ${formatOutputCapture(stdout)}\nStderr: ${formatOutputCapture(stderr)}`
)
`Failed to start test daemon: ${message}\nStdout: ${formatOutputCapture(stdout)}\nStderr: ${formatOutputCapture(stderr)}`,
);
}
return ctx
return ctx;
}
/**
@@ -333,18 +331,18 @@ export async function runPaseoCli(
ctx: TestDaemonContext,
args: string[],
options?: {
timeout?: number
cwd?: string
}
timeout?: number;
cwd?: string;
},
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
const timeout = options?.timeout ?? 60000
const cwd = options?.cwd ?? ctx.workDir
const timeout = options?.timeout ?? 60000;
const cwd = options?.cwd ?? ctx.workDir;
const cliDir = join(import.meta.dirname, '..', '..')
const cliSrcPath = join(cliDir, 'src', 'index.ts')
const cliDir = join(import.meta.dirname, "..", "..");
const cliSrcPath = join(cliDir, "src", "index.ts");
return new Promise((resolve, reject) => {
const proc = spawn('npx', ['tsx', cliSrcPath, ...args], {
const proc = spawn("npx", ["tsx", cliSrcPath, ...args], {
env: {
...process.env,
...TEST_DAEMON_ENV_DEFAULTS,
@@ -352,42 +350,42 @@ export async function runPaseoCli(
PASEO_HOME: ctx.paseoHome,
},
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
detached: process.platform !== 'win32',
})
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
});
const stdout = createOutputCapture()
const stderr = createOutputCapture()
const stdout = createOutputCapture();
const stderr = createOutputCapture();
proc.stdout?.on('data', (data) => {
appendOutputCapture(stdout, data)
})
proc.stdout?.on("data", (data) => {
appendOutputCapture(stdout, data);
});
proc.stderr?.on('data', (data) => {
appendOutputCapture(stderr, data)
})
proc.stderr?.on("data", (data) => {
appendOutputCapture(stderr, data);
});
const timeoutId = setTimeout(() => {
if (proc.pid) {
signalProcessTree(proc.pid, 'SIGKILL')
signalProcessTree(proc.pid, "SIGKILL");
}
reject(new Error(`CLI command timed out after ${timeout}ms: paseo ${args.join(' ')}`))
}, timeout)
reject(new Error(`CLI command timed out after ${timeout}ms: paseo ${args.join(" ")}`));
}, timeout);
proc.on('exit', (code) => {
clearTimeout(timeoutId)
proc.on("exit", (code) => {
clearTimeout(timeoutId);
resolve({
exitCode: code ?? 1,
stdout: formatOutputCapture(stdout),
stderr: formatOutputCapture(stderr),
})
})
});
});
proc.on('error', (err) => {
clearTimeout(timeoutId)
reject(err)
})
})
proc.on("error", (err) => {
clearTimeout(timeoutId);
reject(err);
});
});
}
/**
@@ -396,25 +394,26 @@ export async function runPaseoCli(
*
* This is the main entry point for E2E tests.
*/
export async function createE2ETestContext(options?: {
timeout?: number
}): Promise<
export async function createE2ETestContext(options?: { timeout?: number }): Promise<
TestDaemonContext & {
/** Run a paseo CLI command against this daemon */
paseo: (args: string[], opts?: { timeout?: number; cwd?: string }) => Promise<{
exitCode: number
stdout: string
stderr: string
}>
paseo: (
args: string[],
opts?: { timeout?: number; cwd?: string },
) => Promise<{
exitCode: number;
stdout: string;
stderr: string;
}>;
}
> {
const ctx = await startTestDaemon({ timeout: options?.timeout })
const ctx = await startTestDaemon({ timeout: options?.timeout });
const paseo = (args: string[], opts?: { timeout?: number; cwd?: string }) =>
runPaseoCli(ctx, args, opts)
runPaseoCli(ctx, args, opts);
return {
...ctx,
paseo,
}
};
}
+86 -87
View File
@@ -7,48 +7,48 @@
* Each test is a separate .ts file that can also be run independently.
*/
import { $ } from 'zx'
import { readdir, writeFile } from 'fs/promises'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { $ } from "zx";
import { readdir, writeFile } from "fs/promises";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url))
const args = process.argv.slice(2)
const __dirname = dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
const testEnvDefaults = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
let jsonOutputPath: string | null = null
let jsonOutputPath: string | null = null;
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg === '--json-output') {
const value = args[i + 1]
const arg = args[i];
if (arg === "--json-output") {
const value = args[i + 1];
if (!value) {
throw new Error('--json-output requires a file path')
throw new Error("--json-output requires a file path");
}
jsonOutputPath = value
i++
continue
jsonOutputPath = value;
i++;
continue;
}
}
$.verbose = false
$.verbose = false;
type Failure = { test: string; error: string }
type Failure = { test: string; error: string };
async function runCommand(label: string, command: string): Promise<void> {
console.log(`\n${'─'.repeat(50)}`)
console.log(`🔧 ${label}...`)
console.log('─'.repeat(50))
console.log(`\n${"─".repeat(50)}`);
console.log(`🔧 ${label}...`);
console.log("─".repeat(50));
const result = await $`bash -lc ${command}`.nothrow()
const result = await $`bash -lc ${command}`.nothrow();
if (result.exitCode !== 0) {
const error = result.stderr || result.stdout || `Exit code: ${result.exitCode}`
console.error(`\n❌ ${label} failed`)
console.error(error)
throw new Error(error)
const error = result.stderr || result.stdout || `Exit code: ${result.exitCode}`;
console.error(`\n❌ ${label} failed`);
console.error(error);
throw new Error(error);
}
}
@@ -57,20 +57,20 @@ async function writeJsonSummary({
failed,
failures,
}: {
passed: number
failed: number
failures: Failure[]
passed: number;
failed: number;
failures: Failure[];
}) {
if (!jsonOutputPath) {
return
return;
}
await writeFile(
jsonOutputPath,
JSON.stringify(
{
suite: 'cli-local',
command: 'npm run test:local --workspace=@getpaseo/cli',
suite: "cli-local",
command: "npm run test:local --workspace=@getpaseo/cli",
counts: {
passed,
failed,
@@ -79,96 +79,95 @@ async function writeJsonSummary({
skippedTests: [],
failures: failures.map(({ test, error }) => ({
test,
error: error.split('\n')[0] ?? '',
error: error.split("\n")[0] ?? "",
})),
},
null,
2
) + '\n'
)
2,
) + "\n",
);
}
console.log('🧪 Paseo CLI E2E Test Runner\n')
console.log('='.repeat(50))
console.log("🧪 Paseo CLI E2E Test Runner\n");
console.log("=".repeat(50));
// Discover all test files
const files = await readdir(__dirname)
const testFiles = files
.filter(f => f.match(/^\d{2}-.*\.test\.ts$/))
.sort()
const files = await readdir(__dirname);
const testFiles = files.filter((f) => f.match(/^\d{2}-.*\.test\.ts$/)).sort();
if (testFiles.length === 0) {
console.log('❌ No test files found')
await writeJsonSummary({ passed: 0, failed: 0, failures: [] })
process.exit(1)
console.log("❌ No test files found");
await writeJsonSummary({ passed: 0, failed: 0, failures: [] });
process.exit(1);
}
console.log(`Found ${testFiles.length} test file(s):\n`)
console.log(`Found ${testFiles.length} test file(s):\n`);
for (const file of testFiles) {
console.log(` - ${file}`)
console.log(` - ${file}`);
}
console.log()
console.log();
let passed = 0
let failed = 0
const failures: Failure[] = []
let passed = 0;
let failed = 0;
const failures: Failure[] = [];
await runCommand('Building relay', 'npm run build --workspace=@getpaseo/relay')
await runCommand('Building server', 'npm run build --workspace=@getpaseo/server')
await runCommand('Building CLI', 'npm run build --workspace=@getpaseo/cli')
await runCommand("Building relay", "npm run build --workspace=@getpaseo/relay");
await runCommand("Building server", "npm run build --workspace=@getpaseo/server");
await runCommand("Building CLI", "npm run build --workspace=@getpaseo/cli");
for (const testFile of testFiles) {
const testPath = join(__dirname, testFile)
const testName = testFile.replace(/\.test\.ts$/, '')
const testPath = join(__dirname, testFile);
const testName = testFile.replace(/\.test\.ts$/, "");
console.log(`\n${'─'.repeat(50)}`)
console.log(`📋 Running ${testName}...`)
console.log('─'.repeat(50))
console.log(`\n${"─".repeat(50)}`);
console.log(`📋 Running ${testName}...`);
console.log("─".repeat(50));
try {
const result = await $`PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnvDefaults.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnvDefaults.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnvDefaults.PASEO_VOICE_MODE_ENABLED} npx tsx ${testPath}`.nothrow()
const result =
await $`PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnvDefaults.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnvDefaults.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnvDefaults.PASEO_VOICE_MODE_ENABLED} npx tsx ${testPath}`.nothrow();
if (result.exitCode === 0) {
console.log(`\n✅ ${testName} PASSED`)
passed++
console.log(`\n✅ ${testName} PASSED`);
passed++;
} else {
console.log(`\n❌ ${testName} FAILED (exit code: ${result.exitCode})`)
console.log(`\n❌ ${testName} FAILED (exit code: ${result.exitCode})`);
if (result.stderr) {
console.log('stderr:', result.stderr)
console.log("stderr:", result.stderr);
}
failed++
failures.push({ test: testName, error: result.stderr || `Exit code: ${result.exitCode}` })
break
failed++;
failures.push({ test: testName, error: result.stderr || `Exit code: ${result.exitCode}` });
break;
}
} catch (e) {
const error = e instanceof Error ? e.message : String(e)
console.log(`\n❌ ${testName} FAILED`)
console.log('Error:', error)
failed++
failures.push({ test: testName, error })
break
const error = e instanceof Error ? e.message : String(e);
console.log(`\n❌ ${testName} FAILED`);
console.log("Error:", error);
failed++;
failures.push({ test: testName, error });
break;
}
}
// Summary
console.log('\n' + '='.repeat(50))
console.log('📊 Test Results')
console.log('='.repeat(50))
console.log(` ✅ Passed: ${passed}`)
console.log(` ❌ Failed: ${failed}`)
console.log(` 📝 Total: ${passed + failed}`)
console.log("\n" + "=".repeat(50));
console.log("📊 Test Results");
console.log("=".repeat(50));
console.log(` ✅ Passed: ${passed}`);
console.log(` ❌ Failed: ${failed}`);
console.log(` 📝 Total: ${passed + failed}`);
if (failures.length > 0) {
console.log('\n❌ Failed tests:')
console.log("\n❌ Failed tests:");
for (const { test, error } of failures) {
console.log(` - ${test}`)
console.log(` - ${test}`);
if (error) {
console.log(` ${error.split('\n')[0]}`)
console.log(` ${error.split("\n")[0]}`);
}
}
}
console.log()
console.log();
await writeJsonSummary({ passed, failed, failures })
await writeJsonSummary({ passed, failed, failures });
process.exit(failed > 0 ? 1 : 0)
process.exit(failed > 0 ? 1 : 0);
+65 -67
View File
@@ -9,57 +9,57 @@
* 5. Cleanup: Kill daemon and remove temp dirs after each test
*/
import { $, ProcessPromise, sleep } from 'zx'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { $, ProcessPromise, sleep } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
const TEST_ENV_DEFAULTS = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? '0',
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? '0',
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? '0',
}
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0",
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
function killPidTree(pid: number, signal: NodeJS.Signals): void {
if (!Number.isInteger(pid) || pid <= 0) {
return
return;
}
if (process.platform !== 'win32') {
if (process.platform !== "win32") {
try {
process.kill(-pid, signal)
return
process.kill(-pid, signal);
return;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code
if (code === 'ESRCH') {
return
const code = (error as NodeJS.ErrnoException).code;
if (code === "ESRCH") {
return;
}
}
}
try {
process.kill(pid, signal)
process.kill(pid, signal);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code
if (code !== 'ESRCH') {
throw error
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ESRCH") {
throw error;
}
}
}
export interface TestContext {
/** Random port for test daemon (never 6767) */
port: number
port: number;
/** Temp directory for PASEO_HOME */
paseoHome: string
paseoHome: string;
/** Temp directory for agent working directory */
workDir: string
workDir: string;
/** Running daemon process */
daemon: ProcessPromise | null
daemon: ProcessPromise | null;
/** Run a paseo CLI command against the test daemon */
paseo: (args: string[]) => ProcessPromise
paseo: (args: string[]) => ProcessPromise;
/** Clean up all resources */
cleanup: () => Promise<void>
cleanup: () => Promise<void>;
}
/**
@@ -67,16 +67,16 @@ export interface TestContext {
* NEVER uses 6767 (user's running daemon)
*/
export function getRandomPort(): number {
return 10000 + Math.floor(Math.random() * 50000)
return 10000 + Math.floor(Math.random() * 50000);
}
/**
* Create isolated temp directories for testing
*/
export async function createTempDirs(): Promise<{ paseoHome: string; workDir: string }> {
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
const workDir = await mkdtemp(join(tmpdir(), 'paseo-test-work-'))
return { paseoHome, workDir }
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
const workDir = await mkdtemp(join(tmpdir(), "paseo-test-work-"));
return { paseoHome, workDir };
}
/**
@@ -84,58 +84,56 @@ export async function createTempDirs(): Promise<{ paseoHome: string; workDir: st
* Uses `paseo agent ls` which connects via WebSocket
*/
export async function waitForDaemon(port: number, timeout = 30000): Promise<void> {
const start = Date.now()
const start = Date.now();
while (Date.now() - start < timeout) {
try {
const result = await $`PASEO_HOST=localhost:${port} paseo agent ls`.nothrow()
if (result.exitCode === 0) return
const result = await $`PASEO_HOST=localhost:${port} paseo agent ls`.nothrow();
if (result.exitCode === 0) return;
} catch {
// Connection failed, keep trying
}
await sleep(100)
await sleep(100);
}
throw new Error(`Daemon failed to start on port ${port} within ${timeout}ms`)
throw new Error(`Daemon failed to start on port ${port} within ${timeout}ms`);
}
/**
* Start an isolated test daemon
*/
export async function startDaemon(
port: number,
paseoHome: string
): Promise<ProcessPromise> {
$.verbose = false
const daemon = $`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} PASEO_RELAY_ENABLED=false PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${TEST_ENV_DEFAULTS.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${TEST_ENV_DEFAULTS.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${TEST_ENV_DEFAULTS.PASEO_VOICE_MODE_ENABLED} CI=true paseo daemon start --foreground`.nothrow()
return daemon
export async function startDaemon(port: number, paseoHome: string): Promise<ProcessPromise> {
$.verbose = false;
const daemon =
$`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} PASEO_RELAY_ENABLED=false PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${TEST_ENV_DEFAULTS.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${TEST_ENV_DEFAULTS.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${TEST_ENV_DEFAULTS.PASEO_VOICE_MODE_ENABLED} CI=true paseo daemon start --foreground`.nothrow();
return daemon;
}
/**
* Create a full test context with daemon, temp dirs, and helpers
*/
export async function createTestContext(): Promise<TestContext> {
const port = getRandomPort()
const { paseoHome, workDir } = await createTempDirs()
const port = getRandomPort();
const { paseoHome, workDir } = await createTempDirs();
// Helper to run CLI commands against test daemon
const paseo = (args: string[]): ProcessPromise => {
$.verbose = false
return $`PASEO_HOST=localhost:${port} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${TEST_ENV_DEFAULTS.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${TEST_ENV_DEFAULTS.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${TEST_ENV_DEFAULTS.PASEO_VOICE_MODE_ENABLED} paseo ${args}`.nothrow()
}
$.verbose = false;
return $`PASEO_HOST=localhost:${port} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${TEST_ENV_DEFAULTS.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${TEST_ENV_DEFAULTS.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${TEST_ENV_DEFAULTS.PASEO_VOICE_MODE_ENABLED} paseo ${args}`.nothrow();
};
// Cleanup function
const cleanup = async (): Promise<void> => {
if (ctx.daemon) {
if (typeof ctx.daemon.pid === 'number') {
killPidTree(ctx.daemon.pid, 'SIGTERM')
await sleep(250)
killPidTree(ctx.daemon.pid, 'SIGKILL')
if (typeof ctx.daemon.pid === "number") {
killPidTree(ctx.daemon.pid, "SIGTERM");
await sleep(250);
killPidTree(ctx.daemon.pid, "SIGKILL");
} else {
ctx.daemon.kill()
ctx.daemon.kill();
}
}
await rm(paseoHome, { recursive: true, force: true })
await rm(workDir, { recursive: true, force: true })
}
await rm(paseoHome, { recursive: true, force: true });
await rm(workDir, { recursive: true, force: true });
};
const ctx: TestContext = {
port,
@@ -144,9 +142,9 @@ export async function createTestContext(): Promise<TestContext> {
daemon: null,
paseo,
cleanup,
}
};
return ctx
return ctx;
}
/**
@@ -154,10 +152,10 @@ export async function createTestContext(): Promise<TestContext> {
* Use this for tests that need a running daemon
*/
export async function createTestContextWithDaemon(): Promise<TestContext> {
const ctx = await createTestContext()
ctx.daemon = await startDaemon(ctx.port, ctx.paseoHome)
await waitForDaemon(ctx.port)
return ctx
const ctx = await createTestContext();
ctx.daemon = await startDaemon(ctx.port, ctx.paseoHome);
await waitForDaemon(ctx.port);
return ctx;
}
/**
@@ -165,13 +163,13 @@ export async function createTestContextWithDaemon(): Promise<TestContext> {
*/
export function registerCleanupHandlers(cleanup: () => Promise<void>): void {
const handler = async () => {
await cleanup()
process.exit(0)
}
await cleanup();
process.exit(0);
};
process.on('exit', () => {
process.on("exit", () => {
// Can't await in exit handler, but at least try to kill daemon
})
process.on('SIGINT', handler)
process.on('SIGTERM', handler)
});
process.on("SIGINT", handler);
process.on("SIGTERM", handler);
}
+23 -14
View File
@@ -1,20 +1,29 @@
import { createE2ETestContext } from '/Users/moboudra/.paseo/worktrees/1luy0po7/beefy-parrot/packages/cli/tests/helpers/test-daemon.ts'
import { createE2ETestContext } from "/Users/moboudra/.paseo/worktrees/1luy0po7/beefy-parrot/packages/cli/tests/helpers/test-daemon.ts";
async function main() {
const ctx = await createE2ETestContext({ timeout: 180000 })
const ctx = await createE2ETestContext({ timeout: 180000 });
try {
const run = await ctx.paseo([
'run',
'--provider','opencode',
'--mode','default',
'--model','opencode/kimi-k2.5-free',
'Say hello in one short sentence.'
], { timeout: 180000 })
console.log('EXIT', run.exitCode)
console.log('STDOUT\n' + run.stdout)
console.log('STDERR\n' + run.stderr)
const run = await ctx.paseo(
[
"run",
"--provider",
"opencode",
"--mode",
"default",
"--model",
"opencode/kimi-k2.5-free",
"Say hello in one short sentence.",
],
{ timeout: 180000 },
);
console.log("EXIT", run.exitCode);
console.log("STDOUT\n" + run.stdout);
console.log("STDERR\n" + run.stderr);
} finally {
await ctx.stop()
await ctx.stop();
}
}
main().catch((error) => { console.error(error); process.exitCode = 1 })
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
@@ -1,43 +1,50 @@
import { createE2ETestContext } from '../helpers/test-daemon.ts'
import { createE2ETestContext } from "../helpers/test-daemon.ts";
const schema = JSON.stringify({
type: 'object',
properties: { summary: { type: 'string' } },
required: ['summary'],
type: "object",
properties: { summary: { type: "string" } },
required: ["summary"],
additionalProperties: false,
})
});
async function main() {
const ctx = await createE2ETestContext({ timeout: 180000 })
const ctx = await createE2ETestContext({ timeout: 180000 });
try {
const run = await ctx.paseo([
'run',
'--provider','opencode',
'--mode','default',
'--model','opencode/kimi-k2.5-free',
'--output-schema', schema,
'Return valid JSON with a short summary for provider opencode.'
], { timeout: 180000 })
console.log('RUN EXIT', run.exitCode)
console.log('RUN STDOUT\n' + run.stdout)
console.log('RUN STDERR\n' + run.stderr)
const run = await ctx.paseo(
[
"run",
"--provider",
"opencode",
"--mode",
"default",
"--model",
"opencode/kimi-k2.5-free",
"--output-schema",
schema,
"Return valid JSON with a short summary for provider opencode.",
],
{ timeout: 180000 },
);
console.log("RUN EXIT", run.exitCode);
console.log("RUN STDOUT\n" + run.stdout);
console.log("RUN STDERR\n" + run.stderr);
const ls = await ctx.paseo(['ls','--json'])
console.log('LS\n' + ls.stdout)
const agents = JSON.parse(ls.stdout)
const agentId = agents[0]?.id
const ls = await ctx.paseo(["ls", "--json"]);
console.log("LS\n" + ls.stdout);
const agents = JSON.parse(ls.stdout);
const agentId = agents[0]?.id;
if (agentId) {
const inspect = await ctx.paseo(['inspect', agentId])
console.log('INSPECT\n' + inspect.stdout)
const logs = await ctx.paseo(['logs','--tail','100', agentId], { timeout: 30000 })
console.log('LOGS\n' + logs.stdout)
const inspect = await ctx.paseo(["inspect", agentId]);
console.log("INSPECT\n" + inspect.stdout);
const logs = await ctx.paseo(["logs", "--tail", "100", agentId], { timeout: 30000 });
console.log("LOGS\n" + logs.stdout);
}
} finally {
await ctx.stop()
await ctx.stop();
}
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
console.error(error);
process.exitCode = 1;
});