fix(e2e): work from any directory — auto-detect AirCoding repo root
- e2e command resolves AirCoding repo root via AIRCODING_REPO_ROOT env var - All test paths, depcruise paths, tsc paths made absolute - depcruise and tsc processes given cwd=repoRoot for config resolution - air CLI script sets AIRCODING_REPO_ROOT on startup Now `air e2e` works from any directory. 13/13 gates pass from /tmp/air-playground. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,15 +50,30 @@ function runCmd(label: string, cmd: string, args: string[], cwd?: string, timeou
|
||||
/**
|
||||
* Run a bun test suite and return pass/fail.
|
||||
*/
|
||||
function runTest(label: string, testPath: string): { pass: boolean; detail: string } {
|
||||
function runTest(label: string, testPath: string, repoRoot: string): { pass: boolean; detail: string } {
|
||||
const bun = findBun()
|
||||
const paths = testPath.split(' ').filter(p => p.length > 0)
|
||||
const paths = testPath.split(' ').filter(p => p.length > 0).map(p => join(repoRoot, p.replace(/^\.\//, '')))
|
||||
return runCmd(label, bun, ['test', ...paths])
|
||||
}
|
||||
|
||||
export function e2eCommand(): void {
|
||||
const projectRoot = process.cwd()
|
||||
console.log('Running E2E validation suite...\n')
|
||||
// Find AirCoding repo root (where package.json + turbo.json exist)
|
||||
let repoRoot = process.env.AIRCODING_REPO_ROOT || ''
|
||||
if (!repoRoot) {
|
||||
// Walk up from script location to find package.json + turbo.json
|
||||
let dir = __dirname
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (existsSync(join(dir, 'package.json')) && existsSync(join(dir, 'turbo.json'))) {
|
||||
repoRoot = dir
|
||||
break
|
||||
}
|
||||
dir = join(dir, '..')
|
||||
}
|
||||
}
|
||||
if (!repoRoot) repoRoot = process.cwd()
|
||||
|
||||
console.log('Running E2E validation suite...')
|
||||
console.log(` Repo: ${repoRoot}\n`)
|
||||
|
||||
let passed = 0
|
||||
let failed = 0
|
||||
@@ -66,15 +81,15 @@ export function e2eCommand(): void {
|
||||
const gates: Array<{ label: string; fn: () => { pass: boolean; detail: string } }> = [
|
||||
// P0: monorepo structure + depcruise (zero violations) + tsc (zero errors)
|
||||
{ label: 'P0: Monorepo structure', fn: () => {
|
||||
const pkg = existsSync(join(projectRoot, 'package.json')) &&
|
||||
existsSync(join(projectRoot, 'turbo.json')) &&
|
||||
existsSync(join(projectRoot, 'tsconfig.base.json'))
|
||||
const pkg = existsSync(join(repoRoot, 'package.json')) &&
|
||||
existsSync(join(repoRoot, 'turbo.json')) &&
|
||||
existsSync(join(repoRoot, 'tsconfig.base.json'))
|
||||
return { pass: pkg, detail: pkg ? '✅' : '❌ (package.json/turbo.json/tsconfig.base.json missing)' }
|
||||
}},
|
||||
{ label: 'P0: depcruise dependency boundary (INV-4)', fn: () => {
|
||||
try {
|
||||
execFileSync('node_modules/.bin/depcruise', ['--config', '.dependency-cruiser.js', 'packages/cli/src/', 'packages/contracts/src/', 'packages/llm/src/', 'packages/runtime/src/', 'packages/toolchain-cpp/src/', 'packages/tui/src/', 'packages/workers/src/'], {
|
||||
encoding: 'utf-8', stdio: 'pipe', timeout: 60000
|
||||
execFileSync(join(repoRoot, 'node_modules/.bin/depcruise'), ['--config', join(repoRoot, '.dependency-cruiser.js'), join(repoRoot, 'packages/cli/src/'), join(repoRoot, 'packages/contracts/src/'), join(repoRoot, 'packages/llm/src/'), join(repoRoot, 'packages/runtime/src/'), join(repoRoot, 'packages/toolchain-cpp/src/'), join(repoRoot, 'packages/tui/src/'), join(repoRoot, 'packages/workers/src/')], {
|
||||
cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe', timeout: 60000
|
||||
})
|
||||
return { pass: true, detail: '✅' }
|
||||
} catch (err: any) {
|
||||
@@ -82,9 +97,9 @@ export function e2eCommand(): void {
|
||||
}
|
||||
}},
|
||||
{ label: 'P0: tsc strict typecheck (0 errors)', fn: () => {
|
||||
const tsc = findTsc()
|
||||
const tsc = join(repoRoot, 'node_modules/.bin/tsc')
|
||||
try {
|
||||
execFileSync(tsc, ['--noEmit', '-p', 'tsconfig.check.json'], { encoding: 'utf-8', stdio: 'pipe', timeout: 90000 })
|
||||
execFileSync(tsc, ['--noEmit', '-p', join(repoRoot, 'tsconfig.check.json')], { cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe', timeout: 90000 })
|
||||
return { pass: true, detail: '✅' }
|
||||
} catch (err: any) {
|
||||
const stdout = err.stdout || ''
|
||||
@@ -94,35 +109,35 @@ export function e2eCommand(): void {
|
||||
}
|
||||
}},
|
||||
|
||||
// P1: Storage/Events — run all 16 repository tests + migration tests
|
||||
{ label: 'P1: Storage/Events (test)', fn: () => runTest('P1', './packages/runtime/test/storage/ ./packages/runtime/test/regression/transaction-boundary.test.ts ./packages/runtime/test/regression/event-repository-route.test.ts') },
|
||||
// P1: Storage/Events
|
||||
{ label: 'P1: Storage/Events (test)', fn: () => runTest('P1', './packages/runtime/test/storage/ ./packages/runtime/test/regression/transaction-boundary.test.ts ./packages/runtime/test/regression/event-repository-route.test.ts', repoRoot) },
|
||||
|
||||
// P2: Tools/Permission — 28 MVP tool registration tests
|
||||
{ label: 'P2: Tools/Permission (test)', fn: () => runTest('P2', './packages/runtime/test/regression/tool-stubs.test.ts ./packages/runtime/test/regression/permission-engine-actions.test.ts ./packages/runtime/test/regression/path-classifier-categories.test.ts ./packages/runtime/test/regression/command-risk-analyzer.test.ts') },
|
||||
// P2: Tools/Permission
|
||||
{ label: 'P2: Tools/Permission (test)', fn: () => runTest('P2', './packages/runtime/test/regression/tool-stubs.test.ts ./packages/runtime/test/regression/permission-engine-actions.test.ts ./packages/runtime/test/regression/path-classifier-categories.test.ts ./packages/runtime/test/regression/command-risk-analyzer.test.ts', repoRoot) },
|
||||
|
||||
// P3: Provider/Context
|
||||
{ label: 'P3: Provider/Context (test)', fn: () => runTest('P3', './packages/llm/test/ ./packages/runtime/test/regression/context-assembler-layers.test.ts') },
|
||||
{ label: 'P3: Provider/Context (test)', fn: () => runTest('P3', './packages/llm/test/ ./packages/runtime/test/regression/context-assembler-layers.test.ts', repoRoot) },
|
||||
|
||||
// P4: Worker IPC
|
||||
{ label: 'P4: Worker IPC (test)', fn: () => runTest('P4', './packages/runtime/test/e2e/worker-fixture.test.ts ./packages/runtime/test/regression/worker-exit-code.test.ts ./packages/runtime/test/regression/worker-result-envelope.test.ts') },
|
||||
{ label: 'P4: Worker IPC (test)', fn: () => runTest('P4', './packages/runtime/test/e2e/worker-fixture.test.ts ./packages/runtime/test/regression/worker-exit-code.test.ts ./packages/runtime/test/regression/worker-result-envelope.test.ts', repoRoot) },
|
||||
|
||||
// P5: C++ Toolchain
|
||||
{ label: 'P5: C++ Toolchain (test)', fn: () => runTest('P5', './packages/toolchain-cpp/test/') },
|
||||
{ label: 'P5: C++ Toolchain (test)', fn: () => runTest('P5', './packages/toolchain-cpp/test/', repoRoot) },
|
||||
|
||||
// P6: Projection/TUI
|
||||
{ label: 'P6: Projection/TUI', fn: () => runTest('P6', './packages/runtime/test/regression/projection-store-apply.test.ts ./packages/runtime/test/regression/workspace-enum.test.ts') },
|
||||
{ label: 'P6: Projection/TUI', fn: () => runTest('P6', './packages/runtime/test/regression/projection-store-apply.test.ts ./packages/runtime/test/regression/workspace-enum.test.ts', repoRoot) },
|
||||
|
||||
// P7: Agents
|
||||
{ label: 'P7: Agents (test)', fn: () => runTest('P7', './packages/runtime/test/e2e/direct-mode-fixture.test.ts ./packages/runtime/test/e2e/architecture-review-fixture.test.ts ./packages/runtime/test/regression/main-agent-states.test.ts') },
|
||||
{ label: 'P7: Agents (test)', fn: () => runTest('P7', './packages/runtime/test/e2e/direct-mode-fixture.test.ts ./packages/runtime/test/e2e/architecture-review-fixture.test.ts ./packages/runtime/test/regression/main-agent-states.test.ts', repoRoot) },
|
||||
|
||||
// P8: Full regression suite
|
||||
{ label: 'P8: Full regression suite', fn: () => runTest('P8', './packages/runtime/test/regression/') },
|
||||
{ label: 'P8: Full regression suite', fn: () => runTest('P8', './packages/runtime/test/regression/', repoRoot) },
|
||||
|
||||
// Security
|
||||
{ label: 'SEC: Command injection regression', fn: () => runTest('SEC', './packages/toolchain-cpp/test/command-injection.test.ts') },
|
||||
{ label: 'SEC: Command injection regression', fn: () => runTest('SEC', './packages/toolchain-cpp/test/command-injection.test.ts', repoRoot) },
|
||||
|
||||
// Capability trust levels
|
||||
{ label: 'CAP: Capability trust regression', fn: () => runTest('CAP', './packages/runtime/test/regression/capability-trust-level.test.ts') },
|
||||
{ label: 'CAP: Capability trust regression', fn: () => runTest('CAP', './packages/runtime/test/regression/capability-trust-level.test.ts', repoRoot) },
|
||||
]
|
||||
|
||||
for (const gate of gates) {
|
||||
|
||||
Reference in New Issue
Block a user