Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ async function loadConfig(configPath: string, cwd: string): Promise<AgentestConf
return defineConfig(raw)
}

const jiti = createJiti(import.meta.url, { interopDefault: true })
const { readTsconfigAliases } = await import('./runner/discovery.js')
const alias = readTsconfigAliases(cwd)
const jiti = createJiti(import.meta.url, { interopDefault: true, alias })
const mod = (await jiti.import(absPath)) as {
default?: AgentestConfig | AgentestConfigInput
} & (AgentestConfig | AgentestConfigInput)
Expand Down
53 changes: 47 additions & 6 deletions src/runner/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createJiti } from 'jiti'
import { glob } from 'glob'
import path from 'node:path'
import { readFileSync } from 'node:fs'
import { clearScenarioRegistry, getRegisteredScenarios } from '../scenario/scenario.js'
import type { Scenario } from '../scenario/types.js'
import type { AgentestConfig } from '../config/schema.js'
Expand Down Expand Up @@ -30,15 +31,53 @@ export async function discoverScenarioFiles(
return [...files].sort()
}

/**
* Read tsconfig.json paths and convert them to jiti aliases.
* Maps `"@foo": ["src/foo/index.ts"]` → `{ "@foo": "<cwd>/src/foo/index.ts" }`
*/
export function readTsconfigAliases(cwd: string): Record<string, string> {
const alias: Record<string, string> = {}
try {
const raw = readFileSync(path.join(cwd, 'tsconfig.json'), 'utf-8')
// Strip single-line comments (// ...) so JSON.parse succeeds on tsconfig
const stripped = raw.replace(/\/\/.*$/gm, '')
const tsconfig = JSON.parse(stripped)
const paths: Record<string, string[]> = tsconfig?.compilerOptions?.paths ?? {}
const baseUrl = tsconfig?.compilerOptions?.baseUrl ?? '.'
const base = path.resolve(cwd, baseUrl)
for (const [key, targets] of Object.entries(paths)) {
if (targets.length > 0) {
// Strip trailing /* for wildcard paths
const cleanKey = key.replace(/\/\*$/, '')
const cleanTarget = targets[0].replace(/\/\*$/, '')
alias[cleanKey] = path.resolve(base, cleanTarget)
}
}
} catch {
// No tsconfig or parse error — skip silently
}
return alias
}

// Mutex to prevent concurrent loadScenarioFile calls from clobbering
// the global scenario registry (clear → import → read must be atomic).
let registryLock: Promise<void> = Promise.resolve()

// Reuse a single jiti instance across file loads to benefit from its internal
// module resolution and transform caches.
const jiti = createJiti(import.meta.url, { interopDefault: true })
// Lazily initialized jiti instance — needs cwd for tsconfig alias resolution.
let _jiti: ReturnType<typeof createJiti> | null = null

export async function loadScenarioFile(filePath: string): Promise<Scenario[]> {
function getJiti(cwd: string): ReturnType<typeof createJiti> {
if (!_jiti) {
const alias = readTsconfigAliases(cwd)
_jiti = createJiti(import.meta.url, { interopDefault: true, alias })
}
return _jiti
}

export async function loadScenarioFile(
filePath: string,
cwd: string = process.cwd(),
): Promise<Scenario[]> {
// Chain onto the lock so only one file loads at a time
const release = registryLock
let resolve: () => void
Expand All @@ -51,6 +90,7 @@ export async function loadScenarioFile(filePath: string): Promise<Scenario[]> {
try {
clearScenarioRegistry()

const jiti = getJiti(cwd)
await jiti.import(filePath)

return getRegisteredScenarios()
Expand All @@ -63,11 +103,12 @@ export async function discoverAndLoad(
config: AgentestConfig,
cwd?: string,
): Promise<DiscoveryResult[]> {
const files = await discoverScenarioFiles(config, cwd)
const resolvedCwd = cwd ?? process.cwd()
const files = await discoverScenarioFiles(config, resolvedCwd)
const results: DiscoveryResult[] = []

for (const file of files) {
const scenarios = await loadScenarioFile(file)
const scenarios = await loadScenarioFile(file, resolvedCwd)
if (scenarios.length > 0) {
results.push({ file, scenarios })
}
Expand Down
Loading