mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Two clusters of pre-existing failures fixed by aligning tests with the source they were meant to verify (not by changing source): 1. ultrareviewCommand (4 fails) The 4 "preflight integration" tests assumed `call` makes an axios POST and branches on `action: proceed | blocked | confirm`. That integration was removed; the current `call` branches on `checkOverageGate()`'s four `kind` values. Replaced with 6 tests covering each gate branch (`not-enabled`, `low-balance`, `needs-confirm`, `proceed`), arg pass-through to launchRemoteReview, and the null-launch failure path. 2. autonomy-lifecycle-user-flow (2 fails) The Bun.spawn'd subprocess used cwd=tempDir, where Bun couldn't resolve the `src/*` tsconfig path alias (it's resolved from cwd's tsconfig, not the entrypoint file's). Switched the entrypoint to the bundled dist/cli.js (aliases pre-resolved) and added a beforeAll that lazy-builds the bundle if missing — handles the CI ordering where `bun test` runs before `bun run build`. Local: 5345/5345 pass (was 5339/5345). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
// Why we use the BUILT bundle instead of src/entrypoints/cli.tsx:
|
|
// `Bun.spawn` runs the CLI in a fresh process whose cwd is the per-test
|
|
// tempDir. Bun resolves the `src/*` tsconfig path alias from the cwd's
|
|
// nearest tsconfig.json, NOT from the entrypoint file's directory — so a
|
|
// subprocess started with cwd=tempDir cannot resolve `import 'src/bootstrap/
|
|
// state.js'`. The built dist/cli.js has all aliases pre-resolved, which
|
|
// makes it usable from any cwd.
|
|
//
|
|
// CI runs `bun test` BEFORE `bun run build`, so we lazy-build cli.tsx in a
|
|
// `beforeAll` if dist/cli.js is missing. Local runs after `bun run build`
|
|
// just see the file and skip the build.
|
|
import {
|
|
afterEach,
|
|
beforeAll,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
test,
|
|
} from 'bun:test'
|
|
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import {
|
|
resetStateForTests,
|
|
setOriginalCwd,
|
|
setProjectRoot,
|
|
} from '../../src/bootstrap/state'
|
|
import {
|
|
listAutonomyRuns,
|
|
startManagedAutonomyFlowFromHeartbeatTask,
|
|
} from '../../src/utils/autonomyRuns'
|
|
import { listAutonomyFlows } from '../../src/utils/autonomyFlows'
|
|
|
|
const CLI_ENTRYPOINT = resolve(import.meta.dir, '../../dist/cli.js')
|
|
const PROJECT_ROOT = resolve(import.meta.dir, '../..')
|
|
|
|
let tempDir = ''
|
|
let configDir = ''
|
|
let previousConfigDir: string | undefined
|
|
|
|
async function ensureCliBundle(): Promise<void> {
|
|
if (existsSync(CLI_ENTRYPOINT)) return
|
|
const proc = Bun.spawn({
|
|
cmd: [process.execPath, 'run', 'build'],
|
|
cwd: PROJECT_ROOT,
|
|
stdin: 'ignore',
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
})
|
|
const [stderr, exitCode] = await Promise.all([
|
|
new Response(proc.stderr).text(),
|
|
proc.exited,
|
|
])
|
|
if (exitCode !== 0 || !existsSync(CLI_ENTRYPOINT)) {
|
|
throw new Error(
|
|
`Failed to build dist/cli.js for autonomy CLI tests (exit=${exitCode}):\n${stderr}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
await ensureCliBundle()
|
|
}, 120_000)
|
|
|
|
async function runAutonomyCli(args: string[]): Promise<string> {
|
|
const proc = Bun.spawn({
|
|
cmd: [process.execPath, CLI_ENTRYPOINT, 'autonomy', ...args],
|
|
cwd: tempDir,
|
|
env: {
|
|
...process.env,
|
|
CLAUDE_CONFIG_DIR: configDir,
|
|
CI: 'true',
|
|
GITHUB_ACTIONS: 'true',
|
|
NODE_ENV: 'development',
|
|
NO_COLOR: '1',
|
|
},
|
|
stdin: 'ignore',
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
})
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
proc.exited,
|
|
])
|
|
|
|
expect(stderr, `unexpected stderr output:\n${stderr}`).toBe('')
|
|
expect(exitCode, `non-zero exit ${exitCode}; stderr:\n${stderr}`).toBe(0)
|
|
return stdout
|
|
}
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(join(tmpdir(), 'autonomy-user-flow-'))
|
|
configDir = join(tempDir, 'config')
|
|
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
process.env.CLAUDE_CONFIG_DIR = configDir
|
|
resetStateForTests()
|
|
setOriginalCwd(tempDir)
|
|
setProjectRoot(tempDir)
|
|
})
|
|
|
|
afterEach(() => {
|
|
resetStateForTests()
|
|
if (previousConfigDir === undefined) {
|
|
delete process.env.CLAUDE_CONFIG_DIR
|
|
} else {
|
|
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
|
}
|
|
if (tempDir) {
|
|
rmSync(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe('autonomy lifecycle user-equivalent CLI flow', () => {
|
|
test('status --deep works from a clean project without creating autonomy state', async () => {
|
|
const output = await runAutonomyCli(['status', '--deep'])
|
|
|
|
expect(output).toContain('# Autonomy Deep Status')
|
|
expect(output).toContain('Autonomy runs: 0')
|
|
expect(output).toContain('Autonomy flows: 0')
|
|
expect(existsSync(join(tempDir, '.claude', 'autonomy', 'runs.json'))).toBe(
|
|
false,
|
|
)
|
|
expect(existsSync(join(tempDir, '.claude', 'autonomy', 'flows.json'))).toBe(
|
|
false,
|
|
)
|
|
})
|
|
|
|
test('real CLI can inspect, resume, and cancel a persisted managed flow', async () => {
|
|
await startManagedAutonomyFlowFromHeartbeatTask({
|
|
rootDir: tempDir,
|
|
currentDir: tempDir,
|
|
task: {
|
|
name: 'manual-user-flow',
|
|
interval: '1h',
|
|
prompt: 'Manual lifecycle acceptance',
|
|
steps: [
|
|
{
|
|
name: 'approve',
|
|
prompt: 'Wait for manual approval',
|
|
waitFor: 'manual',
|
|
},
|
|
{
|
|
name: 'execute',
|
|
prompt: 'Execute approved work',
|
|
},
|
|
],
|
|
},
|
|
})
|
|
const [waitingFlow] = await listAutonomyFlows(tempDir)
|
|
expect(waitingFlow?.status).toBe('waiting')
|
|
|
|
const status = await runAutonomyCli(['status', '--deep'])
|
|
expect(status).toContain('Autonomy flows: 1')
|
|
expect(status).toContain('Waiting: 1')
|
|
|
|
const flows = await runAutonomyCli(['flows', '5'])
|
|
expect(flows).toContain(waitingFlow!.flowId)
|
|
expect(flows).toContain('waiting')
|
|
|
|
const detailBefore = await runAutonomyCli(['flow', waitingFlow!.flowId])
|
|
expect(detailBefore).toContain('Status: waiting')
|
|
expect(detailBefore).toContain('Current step: approve')
|
|
|
|
const resume = await runAutonomyCli(['flow', 'resume', waitingFlow!.flowId])
|
|
expect(resume).toContain('Prepared the next managed step')
|
|
expect(resume).toContain('Prompt:')
|
|
|
|
const detailAfterResume = await runAutonomyCli([
|
|
'flow',
|
|
waitingFlow!.flowId,
|
|
])
|
|
expect(detailAfterResume).toContain('Status: queued')
|
|
expect(detailAfterResume).toContain('Latest run:')
|
|
|
|
const cancel = await runAutonomyCli(['flow', 'cancel', waitingFlow!.flowId])
|
|
expect(cancel).toContain('Cancelled flow')
|
|
|
|
const [cancelledRun] = await listAutonomyRuns(tempDir)
|
|
const [cancelledFlow] = await listAutonomyFlows(tempDir)
|
|
expect(cancelledRun?.status).toBe('cancelled')
|
|
expect(cancelledFlow?.status).toBe('cancelled')
|
|
|
|
const detailAfterCancel = await runAutonomyCli([
|
|
'flow',
|
|
waitingFlow!.flowId,
|
|
])
|
|
expect(detailAfterCancel).toContain('Status: cancelled')
|
|
}, 30000)
|
|
})
|