Merge branch 'fix-tests-suite-loop'
# Conflicts: # package-lock.json # packages/server/src/server/speech/speech-config-resolver.ts # packages/server/src/shared/tool-call-display.ts
This commit is contained in:
@@ -349,6 +349,7 @@ export async function runRunCommand(
|
||||
model: resolvedProviderModel.model,
|
||||
thinkingOptionId,
|
||||
initialPrompt: structuredPrompt,
|
||||
outputSchema,
|
||||
images,
|
||||
git,
|
||||
worktreeName: options.worktree,
|
||||
|
||||
@@ -15,12 +15,10 @@
|
||||
*/
|
||||
|
||||
import assert from 'node:assert'
|
||||
import { $ } from 'zx'
|
||||
import { mkdtemp, rm } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
$.verbose = false
|
||||
import { runLocalPaseo } from './helpers/local-cli.ts'
|
||||
|
||||
console.log('=== Daemon Commands ===\n')
|
||||
|
||||
@@ -28,11 +26,15 @@ console.log('=== Daemon Commands ===\n')
|
||||
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 })
|
||||
}
|
||||
|
||||
try {
|
||||
// Test 1: daemon --help shows subcommands
|
||||
{
|
||||
console.log('Test 1: daemon --help shows subcommands')
|
||||
const result = await $`npx paseo daemon --help`.nothrow()
|
||||
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')
|
||||
@@ -45,8 +47,7 @@ try {
|
||||
// Test 2: daemon pair works without daemon process
|
||||
{
|
||||
console.log('Test 2: daemon pair prints local pairing URL')
|
||||
const result =
|
||||
await $`PASEO_HOME=${paseoHome} npx paseo daemon pair`.nothrow()
|
||||
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')
|
||||
@@ -56,8 +57,7 @@ try {
|
||||
// Test 3: daemon status reports stopped when daemon not running
|
||||
{
|
||||
console.log('Test 3: daemon status reports stopped when not running')
|
||||
const result =
|
||||
await $`PASEO_HOME=${paseoHome} npx paseo daemon status`.nothrow()
|
||||
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')
|
||||
@@ -68,8 +68,7 @@ try {
|
||||
// Test 4: daemon pair --json outputs valid JSON
|
||||
{
|
||||
console.log('Test 4: daemon pair --json outputs JSON')
|
||||
const result =
|
||||
await $`PASEO_HOME=${paseoHome} npx paseo daemon pair --json`.nothrow()
|
||||
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')
|
||||
@@ -81,8 +80,7 @@ try {
|
||||
// Test 5: daemon status --json outputs valid JSON
|
||||
{
|
||||
console.log('Test 5: daemon status --json outputs JSON')
|
||||
const result =
|
||||
await $`PASEO_HOME=${paseoHome} npx paseo daemon status --json`.nothrow()
|
||||
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')
|
||||
@@ -95,8 +93,7 @@ try {
|
||||
// Test 6: daemon stop handles daemon not running gracefully
|
||||
{
|
||||
console.log('Test 6: daemon stop handles daemon not running')
|
||||
const result =
|
||||
await $`PASEO_HOME=${paseoHome} npx paseo daemon stop`.nothrow()
|
||||
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
|
||||
@@ -110,18 +107,17 @@ try {
|
||||
// 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 $`PASEO_HOME=${paseoHome} npx paseo daemon restart --port ${String(port)}`.nothrow()
|
||||
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 $`PASEO_HOME=${paseoHome} npx paseo daemon stop --force`.nothrow()
|
||||
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 $`PASEO_HOME=${paseoHome} npx paseo daemon stop --force`.nothrow()
|
||||
await daemonCommand(['stop', '--force'])
|
||||
// Clean up temp directory
|
||||
await rm(paseoHome, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
@@ -20,12 +20,10 @@
|
||||
*/
|
||||
|
||||
import assert from 'node:assert'
|
||||
import { $ } from 'zx'
|
||||
import { mkdtemp, rm } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
$.verbose = false
|
||||
import { runLocalPaseo } from './helpers/local-cli.ts'
|
||||
|
||||
console.log('=== LS Command Tests ===\n')
|
||||
|
||||
@@ -37,7 +35,7 @@ try {
|
||||
// Test 1: paseo --help shows ls command
|
||||
{
|
||||
console.log('Test 1: paseo --help shows ls command')
|
||||
const result = await $`npx paseo --help`.nothrow()
|
||||
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')
|
||||
@@ -46,7 +44,7 @@ try {
|
||||
// Test 2: paseo ls --help shows options
|
||||
{
|
||||
console.log('Test 2: paseo ls --help shows options')
|
||||
const result = await $`npx paseo ls --help`.nothrow()
|
||||
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')
|
||||
@@ -60,8 +58,10 @@ try {
|
||||
// Test 3: paseo ls returns error when no daemon running
|
||||
{
|
||||
console.log('Test 3: paseo ls handles daemon not running')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls`.nothrow()
|
||||
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
|
||||
@@ -76,8 +76,10 @@ try {
|
||||
// Test 4: paseo ls --json returns valid JSON error
|
||||
{
|
||||
console.log('Test 4: paseo ls --json handles errors')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls --json`.nothrow()
|
||||
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')
|
||||
// But output should be valid JSON if present
|
||||
@@ -98,8 +100,10 @@ try {
|
||||
// Test 5: paseo ls -a flag is accepted
|
||||
{
|
||||
console.log('Test 5: paseo ls -a flag is accepted')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls -a`.nothrow()
|
||||
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
|
||||
@@ -111,8 +115,10 @@ try {
|
||||
// Test 6: paseo ls -g flag is accepted
|
||||
{
|
||||
console.log('Test 6: paseo ls -g flag is accepted')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls -g`.nothrow()
|
||||
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')
|
||||
@@ -122,8 +128,10 @@ try {
|
||||
// Test 7: paseo ls -ag combined flags are accepted
|
||||
{
|
||||
console.log('Test 7: paseo ls -ag combined flags are accepted')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls -ag`.nothrow()
|
||||
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')
|
||||
@@ -133,8 +141,10 @@ try {
|
||||
// Test 8: -q (quiet) flag is accepted globally
|
||||
{
|
||||
console.log('Test 8: -q (quiet) flag is accepted')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q ls`.nothrow()
|
||||
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')
|
||||
@@ -144,8 +154,10 @@ try {
|
||||
// Test 9: paseo ls --ui is rejected (flag removed)
|
||||
{
|
||||
console.log('Test 9: paseo ls --ui is rejected')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls --ui`.nothrow()
|
||||
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')
|
||||
|
||||
@@ -30,6 +30,29 @@ type ProviderModel = {
|
||||
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-6',
|
||||
model: 'Sonnet 4.6',
|
||||
descriptionFragment: 'Best for everyday tasks',
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
model: 'Opus 4.6',
|
||||
descriptionFragment: 'Most capable',
|
||||
},
|
||||
{
|
||||
id: 'claude-haiku-4-5-20251001',
|
||||
model: 'Haiku 4.5',
|
||||
descriptionFragment: 'Fastest',
|
||||
},
|
||||
] as const
|
||||
|
||||
let claudeModelIdsFromJson: string[] = []
|
||||
let claudeModelsFromJson: ProviderModel[] = []
|
||||
|
||||
@@ -60,6 +83,27 @@ async function runProviderModelsJson(
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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`)
|
||||
assert(
|
||||
(actualModel.description ?? '').includes(expectedModel.descriptionFragment),
|
||||
`${expectedModel.id} description should mention ${expectedModel.descriptionFragment}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Test 1: provider --help shows subcommands
|
||||
{
|
||||
@@ -114,25 +158,7 @@ try {
|
||||
{
|
||||
console.log('Test 5: provider models claude lists canonical model aliases')
|
||||
const data = await runProviderModelsJson('claude')
|
||||
assert.strictEqual(data.length, 3, 'should have exactly 3 claude models')
|
||||
const byId = new Map(data.map((model) => [model.id, model]))
|
||||
const ids = [...byId.keys()].sort()
|
||||
assert.deepStrictEqual(ids, ['default', 'haiku', 'opus'], 'claude IDs should be default/opus/haiku')
|
||||
assert.strictEqual(byId.get('default')?.model, 'Sonnet 4.5', 'default claude alias should map to Sonnet 4.5')
|
||||
assert.strictEqual(byId.get('opus')?.model, 'Opus 4.6', 'opus claude alias should map to Opus 4.6')
|
||||
assert.strictEqual(byId.get('haiku')?.model, 'Haiku 4.5', 'haiku claude alias should map to Haiku 4.5')
|
||||
assert(
|
||||
(byId.get('default')?.description ?? '').includes('Best for everyday tasks'),
|
||||
'default claude description should mention everyday tasks'
|
||||
)
|
||||
assert(
|
||||
(byId.get('opus')?.description ?? '').includes('Most capable'),
|
||||
'opus claude description should mention most capable'
|
||||
)
|
||||
assert(
|
||||
(byId.get('haiku')?.description ?? '').includes('Fastest'),
|
||||
'haiku claude description should mention fastest'
|
||||
)
|
||||
assertClaudeModels(data)
|
||||
console.log('✓ provider models claude lists canonical model aliases\n')
|
||||
}
|
||||
|
||||
@@ -184,14 +210,8 @@ try {
|
||||
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.strictEqual(data.length, 3, 'should have exactly 3 models for claude')
|
||||
assert(data.every((m) => m.model && m.id), 'each model should have name and id')
|
||||
const byId = new Map(data.map((model) => [model.id, model]))
|
||||
const ids = [...byId.keys()].sort()
|
||||
assert.deepStrictEqual(ids, ['default', 'haiku', 'opus'], 'claude JSON IDs should be deterministic')
|
||||
assert.strictEqual(byId.get('default')?.model, 'Sonnet 4.5', 'default claude alias should map to Sonnet 4.5')
|
||||
assert.strictEqual(byId.get('opus')?.model, 'Opus 4.6', 'opus claude alias should map to Opus 4.6')
|
||||
assert.strictEqual(byId.get('haiku')?.model, 'Haiku 4.5', 'haiku claude alias should map to Haiku 4.5')
|
||||
assertClaudeModels(data)
|
||||
claudeModelIdsFromJson = data.map((m) => m.id)
|
||||
claudeModelsFromJson = data
|
||||
console.log('✓ provider models --json outputs valid JSON\n')
|
||||
@@ -204,7 +224,7 @@ try {
|
||||
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, 3, 'should have 3 lines')
|
||||
assert.strictEqual(lines.length, EXPECTED_CLAUDE_MODELS.length, 'should have one line per Claude catalog model')
|
||||
assert.deepStrictEqual(
|
||||
[...lines].sort(),
|
||||
[...claudeModelIdsFromJson].sort(),
|
||||
@@ -212,12 +232,12 @@ try {
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
[...lines].sort(),
|
||||
['default', 'haiku', 'opus'],
|
||||
'--quiet should print canonical claude IDs'
|
||||
EXPECTED_CLAUDE_MODELS.map((model) => model.id).sort(),
|
||||
'--quiet should print the current Claude catalog IDs'
|
||||
)
|
||||
assert(
|
||||
claudeModelsFromJson.some((m) => m.id === 'default'),
|
||||
'captured --json output should still include default claude model id'
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -14,19 +14,30 @@ import assert from 'node:assert'
|
||||
import { $ } from 'zx'
|
||||
import { mkdtemp, rm } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { dirname, join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
$.verbose = false
|
||||
|
||||
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-'))
|
||||
|
||||
async function runCli(args: string[]) {
|
||||
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()
|
||||
}
|
||||
|
||||
try {
|
||||
{
|
||||
console.log('Test 1: delete --help shows options')
|
||||
const result = await $`npm run cli -- delete --help`.nothrow()
|
||||
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')
|
||||
@@ -37,8 +48,7 @@ try {
|
||||
|
||||
{
|
||||
console.log('Test 2: delete requires ID, --all, or --cwd')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm run cli -- delete`.nothrow()
|
||||
const result = await runDelete([])
|
||||
assert.notStrictEqual(result.exitCode, 0, 'should fail without id, --all, or --cwd')
|
||||
const output = result.stdout + result.stderr
|
||||
const hasError =
|
||||
@@ -52,8 +62,7 @@ try {
|
||||
|
||||
{
|
||||
console.log('Test 3: delete handles daemon not running')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm run cli -- delete abc123`.nothrow()
|
||||
const result = await runDelete(['abc123'])
|
||||
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
||||
const output = result.stdout + result.stderr
|
||||
const hasError =
|
||||
@@ -66,8 +75,7 @@ try {
|
||||
|
||||
{
|
||||
console.log('Test 4: delete --all flag is accepted')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm run cli -- delete --all`.nothrow()
|
||||
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')
|
||||
@@ -76,8 +84,7 @@ try {
|
||||
|
||||
{
|
||||
console.log('Test 5: delete --cwd flag is accepted')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm run cli -- delete --cwd /tmp`.nothrow()
|
||||
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')
|
||||
@@ -86,8 +93,7 @@ try {
|
||||
|
||||
{
|
||||
console.log('Test 6: delete with ID and --host flag is accepted')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm run cli -- delete abc123 --host localhost:${port}`.nothrow()
|
||||
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')
|
||||
@@ -96,7 +102,7 @@ try {
|
||||
|
||||
{
|
||||
console.log('Test 7: paseo --help shows delete command')
|
||||
const result = await $`npm run cli -- --help`.nothrow()
|
||||
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')
|
||||
@@ -105,7 +111,7 @@ try {
|
||||
{
|
||||
console.log('Test 8: -q (quiet) flag is accepted with delete')
|
||||
const result =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm run cli -- -q delete abc123`.nothrow()
|
||||
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')
|
||||
|
||||
@@ -32,6 +32,8 @@ const impossibleSchema = JSON.stringify({
|
||||
],
|
||||
})
|
||||
|
||||
const OPEN_CODE_STRUCTURED_MODEL = 'opencode/gpt-5-nano'
|
||||
|
||||
let ctx: E2EContext
|
||||
|
||||
async function setup(): Promise<void> {
|
||||
@@ -94,7 +96,7 @@ async function test_all_providers_return_structured_output(): Promise<void> {
|
||||
await runProviderCase({
|
||||
provider: 'opencode',
|
||||
mode: 'default',
|
||||
model: 'opencode/kimi-k2.5-free',
|
||||
model: OPEN_CODE_STRUCTURED_MODEL,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { $, ProcessPromise } from 'zx'
|
||||
import { join } from 'node:path'
|
||||
|
||||
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()
|
||||
}
|
||||
+103
-87
@@ -36,102 +36,35 @@ for (let i = 0; i < args.length; i++) {
|
||||
|
||||
$.verbose = false
|
||||
|
||||
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()
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
console.log('❌ No test files found')
|
||||
if (jsonOutputPath) {
|
||||
await writeFile(
|
||||
jsonOutputPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
suite: 'cli-local',
|
||||
command: 'npm run test:local --workspace=@getpaseo/cli',
|
||||
counts: {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
skippedTests: [],
|
||||
failures: [],
|
||||
error: 'No test files found',
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Found ${testFiles.length} test file(s):\n`)
|
||||
for (const file of testFiles) {
|
||||
console.log(` - ${file}`)
|
||||
}
|
||||
console.log()
|
||||
|
||||
let passed = 0
|
||||
let failed = 0
|
||||
const failures: { test: string; error: string }[] = []
|
||||
|
||||
for (const testFile of testFiles) {
|
||||
const testPath = join(__dirname, testFile)
|
||||
const testName = testFile.replace(/\.test\.ts$/, '')
|
||||
type Failure = { test: string; error: string }
|
||||
|
||||
async function runCommand(label: string, command: string): Promise<void> {
|
||||
console.log(`\n${'─'.repeat(50)}`)
|
||||
console.log(`📋 Running ${testName}...`)
|
||||
console.log(`🔧 ${label}...`)
|
||||
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()
|
||||
if (result.exitCode === 0) {
|
||||
console.log(`\n✅ ${testName} PASSED`)
|
||||
passed++
|
||||
} else {
|
||||
console.log(`\n❌ ${testName} FAILED (exit code: ${result.exitCode})`)
|
||||
if (result.stderr) {
|
||||
console.log('stderr:', result.stderr)
|
||||
}
|
||||
failed++
|
||||
failures.push({ test: testName, error: result.stderr || `Exit code: ${result.exitCode}` })
|
||||
}
|
||||
} 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 })
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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}`)
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ Failed tests:')
|
||||
for (const { test, error } of failures) {
|
||||
console.log(` - ${test}`)
|
||||
if (error) {
|
||||
console.log(` ${error.split('\n')[0]}`)
|
||||
}
|
||||
async function writeJsonSummary({
|
||||
passed,
|
||||
failed,
|
||||
failures,
|
||||
}: {
|
||||
passed: number
|
||||
failed: number
|
||||
failures: Failure[]
|
||||
}) {
|
||||
if (!jsonOutputPath) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
|
||||
if (jsonOutputPath) {
|
||||
await writeFile(
|
||||
jsonOutputPath,
|
||||
JSON.stringify(
|
||||
@@ -155,4 +88,87 @@ if (jsonOutputPath) {
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
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`)
|
||||
for (const file of testFiles) {
|
||||
console.log(` - ${file}`)
|
||||
}
|
||||
console.log()
|
||||
|
||||
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')
|
||||
|
||||
for (const testFile of testFiles) {
|
||||
const testPath = join(__dirname, testFile)
|
||||
const testName = testFile.replace(/\.test\.ts$/, '')
|
||||
|
||||
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()
|
||||
if (result.exitCode === 0) {
|
||||
console.log(`\n✅ ${testName} PASSED`)
|
||||
passed++
|
||||
} else {
|
||||
console.log(`\n❌ ${testName} FAILED (exit code: ${result.exitCode})`)
|
||||
if (result.stderr) {
|
||||
console.log('stderr:', result.stderr)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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}`)
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ Failed tests:')
|
||||
for (const { test, error } of failures) {
|
||||
console.log(` - ${test}`)
|
||||
if (error) {
|
||||
console.log(` ${error.split('\n')[0]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
|
||||
await writeJsonSummary({ passed, failed, failures })
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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 })
|
||||
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)
|
||||
} finally {
|
||||
await ctx.stop()
|
||||
}
|
||||
}
|
||||
main().catch((error) => { console.error(error); process.exitCode = 1 })
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createE2ETestContext } from '../helpers/test-daemon.ts'
|
||||
|
||||
const schema = JSON.stringify({
|
||||
type: 'object',
|
||||
properties: { summary: { type: 'string' } },
|
||||
required: ['summary'],
|
||||
additionalProperties: false,
|
||||
})
|
||||
|
||||
async function main() {
|
||||
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 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)
|
||||
}
|
||||
} finally {
|
||||
await ctx.stop()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
Reference in New Issue
Block a user