Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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 ===");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user