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:
Mohamed Boudra
2026-03-14 22:06:42 +07:00
50 changed files with 1330 additions and 2544 deletions
+14 -18
View File
@@ -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 })
}
+31 -19
View File
@@ -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')
+51 -31
View File
@@ -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')
}
+20 -14
View File
@@ -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,
})
}
+9
View File
@@ -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
View File
@@ -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)
+20
View File
@@ -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
})