fix: 修复 RemoteTriggerTool 和 autonomy 测试的全量运行失败

RemoteTriggerTool 测试补充了缺失的 mock(log/debug/oauth/growthbook/policyLimits/bun:bundle),
用内存数组替代文件系统写入审计记录,避免路径冲突。autonomy handler 函数增加可选 rootDir 参数,
测试显式传递 rootDir 避免依赖全局 getProjectRoot() 导致并发测试状态污染。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-28 22:29:36 +08:00
parent 9e365f1ffa
commit a2cfaf9111
3 changed files with 76 additions and 34 deletions

View File

@@ -7,9 +7,14 @@ import {
setOriginalCwd, setOriginalCwd,
setProjectRoot, setProjectRoot,
} from 'src/bootstrap/state.js' } from 'src/bootstrap/state.js'
import { logMock } from '../../../../../../tests/mocks/log'
import { debugMock } from '../../../../../../tests/mocks/debug'
let requestStatus = 200 let requestStatus = 200
mock.module('src/utils/log.ts', logMock)
mock.module('src/utils/debug.ts', debugMock)
mock.module('axios', () => ({ mock.module('axios', () => ({
default: { default: {
request: async () => ({ request: async () => ({
@@ -30,16 +35,41 @@ mock.module('src/services/oauth/client.js', () => ({
mock.module('src/constants/oauth.js', () => ({ mock.module('src/constants/oauth.js', () => ({
getOauthConfig: () => ({ BASE_API_URL: 'https://example.test' }), getOauthConfig: () => ({ BASE_API_URL: 'https://example.test' }),
fileSuffixForOauthConfig: () => '',
}))
mock.module('src/services/analytics/growthbook.js', () => ({
getFeatureValue_CACHED_MAY_BE_STALE: () => true,
}))
mock.module('src/services/policyLimits/index.js', () => ({
isPolicyAllowed: () => true,
}))
mock.module('bun:bundle', () => ({
feature: () => false,
})) }))
let cwd = '' let cwd = ''
let previousCwd = '' let previousCwd = ''
let auditRecords: Array<Record<string, unknown>> = []
mock.module('src/utils/remoteTriggerAudit.js', () => ({
appendRemoteTriggerAuditRecord: async (record: Record<string, unknown>) => {
const full = { ...record, auditId: record.auditId ?? 'test-audit-id', createdAt: Date.now() }
auditRecords.push(full)
return full
},
resolveRemoteTriggerAuditPath: () => join(cwd, '.claude', 'remote-trigger-audit.jsonl'),
}))
beforeEach(async () => { beforeEach(async () => {
requestStatus = 200 requestStatus = 200
auditRecords = []
previousCwd = process.cwd() previousCwd = process.cwd()
cwd = join(tmpdir(), `remote-trigger-tool-${Date.now()}-${Math.random().toString(16).slice(2)}`) cwd = join(tmpdir(), `remote-trigger-tool-${Date.now()}-${Math.random().toString(16).slice(2)}`)
await mkdir(cwd, { recursive: true }) await mkdir(cwd, { recursive: true })
await mkdir(join(cwd, '.claude'), { recursive: true })
process.chdir(cwd) process.chdir(cwd)
resetStateForTests() resetStateForTests()
setOriginalCwd(cwd) setOriginalCwd(cwd)
@@ -61,13 +91,10 @@ describe('RemoteTriggerTool audit', () => {
) )
expect(result.data.audit_id).toBeString() expect(result.data.audit_id).toBeString()
const raw = await readFile( expect(auditRecords).toHaveLength(1)
join(cwd, '.claude', 'remote-trigger-audit.jsonl'), expect(auditRecords[0].action).toBe('run')
'utf-8', expect(auditRecords[0].triggerId).toBe('trigger-1')
) expect(auditRecords[0].ok).toBe(true)
expect(raw).toContain('"action":"run"')
expect(raw).toContain('"triggerId":"trigger-1"')
expect(raw).toContain('"ok":true')
}) })
test('writes an audit record before rethrowing validation failures', async () => { test('writes an audit record before rethrowing validation failures', async () => {
@@ -80,12 +107,9 @@ describe('RemoteTriggerTool audit', () => {
), ),
).rejects.toThrow('run requires trigger_id') ).rejects.toThrow('run requires trigger_id')
const raw = await readFile( expect(auditRecords).toHaveLength(1)
join(cwd, '.claude', 'remote-trigger-audit.jsonl'), expect(auditRecords[0].action).toBe('run')
'utf-8', expect(auditRecords[0].ok).toBe(false)
) expect(auditRecords[0].error).toBe('run requires trigger_id')
expect(raw).toContain('"action":"run"')
expect(raw).toContain('"ok":false')
expect(raw).toContain('run requires trigger_id')
}) })
}) })

View File

@@ -57,7 +57,7 @@ describe('autonomy CLI handler', () => {
sourceLabel: 'nightly', sourceLabel: 'nightly',
}) })
const output = await getAutonomyStatusText() const output = await getAutonomyStatusText({ rootDir: tempDir })
expect(output).toContain('Autonomy runs: 1') expect(output).toContain('Autonomy runs: 1')
expect(output).toContain('Queued: 1') expect(output).toContain('Queued: 1')
@@ -77,7 +77,7 @@ describe('autonomy CLI handler', () => {
})}\n`, })}\n`,
) )
const output = await getAutonomyStatusText({ deep: true }) const output = await getAutonomyStatusText({ deep: true, rootDir: tempDir })
expect(output).toContain('# Autonomy Deep Status') expect(output).toContain('# Autonomy Deep Status')
expect(output).toContain('## Workflow Runs') expect(output).toContain('## Workflow Runs')
@@ -87,8 +87,8 @@ describe('autonomy CLI handler', () => {
}) })
test('prints individual deep status sections for panel actions', async () => { test('prints individual deep status sections for panel actions', async () => {
const pipes = await getAutonomyDeepSectionText('pipes') const pipes = await getAutonomyDeepSectionText('pipes', { rootDir: tempDir })
const remoteControl = await getAutonomyDeepSectionText('remote-control') const remoteControl = await getAutonomyDeepSectionText('remote-control', { rootDir: tempDir })
expect(pipes).toContain('# Pipes') expect(pipes).toContain('# Pipes')
expect(pipes).toContain('Pipe registry:') expect(pipes).toContain('Pipe registry:')
@@ -116,17 +116,17 @@ describe('autonomy CLI handler', () => {
}) })
const [waitingFlow] = await listAutonomyFlows(tempDir) const [waitingFlow] = await listAutonomyFlows(tempDir)
expect(await getAutonomyFlowsText()).toContain(waitingFlow!.flowId) expect(await getAutonomyFlowsText(undefined, { rootDir: tempDir })).toContain(waitingFlow!.flowId)
expect(await getAutonomyFlowText(waitingFlow!.flowId)).toContain( expect(await getAutonomyFlowText(waitingFlow!.flowId, { rootDir: tempDir })).toContain(
'Current step: wait', 'Current step: wait',
) )
const resumed = await resumeAutonomyFlowText(waitingFlow!.flowId) const resumed = await resumeAutonomyFlowText(waitingFlow!.flowId, { rootDir: tempDir, currentDir: tempDir })
expect(resumed).toContain('Prepared the next managed step') expect(resumed).toContain('Prepared the next managed step')
expect(resumed).toContain('Prompt:') expect(resumed).toContain('Prompt:')
expect(resumed).toContain('Wait for manual signal') expect(resumed).toContain('Wait for manual signal')
const cancelled = await cancelAutonomyFlowText(waitingFlow!.flowId) const cancelled = await cancelAutonomyFlowText(waitingFlow!.flowId, { rootDir: tempDir })
expect(cancelled).toContain('Cancelled flow') expect(cancelled).toContain('Cancelled flow')
}) })
}) })

View File

@@ -37,10 +37,12 @@ export function parseAutonomyLimit(raw?: string | number): number {
export async function getAutonomyStatusText(options?: { export async function getAutonomyStatusText(options?: {
deep?: boolean deep?: boolean
rootDir?: string
}): Promise<string> { }): Promise<string> {
const rootDir = options?.rootDir
const [runs, flows] = await Promise.all([ const [runs, flows] = await Promise.all([
listAutonomyRuns(), listAutonomyRuns(rootDir),
listAutonomyFlows(), listAutonomyFlows(rootDir),
]) ])
if (options?.deep) { if (options?.deep) {
@@ -55,10 +57,11 @@ export async function getAutonomyStatusText(options?: {
export async function getAutonomyDeepSectionText( export async function getAutonomyDeepSectionText(
sectionId: AutonomyDeepStatusSectionId, sectionId: AutonomyDeepStatusSectionId,
options?: { rootDir?: string },
): Promise<string> { ): Promise<string> {
const [runs, flows] = await Promise.all([ const [runs, flows] = await Promise.all([
listAutonomyRuns(), listAutonomyRuns(options?.rootDir),
listAutonomyFlows(), listAutonomyFlows(options?.rootDir),
]) ])
const sections = await formatAutonomyDeepStatusSections({ runs, flows }) const sections = await formatAutonomyDeepStatusSections({ runs, flows })
const section = sections.find(item => item.id === sectionId) const section = sections.find(item => item.id === sectionId)
@@ -76,9 +79,10 @@ export async function autonomyStatusHandler(options?: {
export async function getAutonomyRunsText( export async function getAutonomyRunsText(
limit?: string | number, limit?: string | number,
options?: { rootDir?: string },
): Promise<string> { ): Promise<string> {
return formatAutonomyRunsList( return formatAutonomyRunsList(
await listAutonomyRuns(), await listAutonomyRuns(options?.rootDir),
parseAutonomyLimit(limit), parseAutonomyLimit(limit),
) )
} }
@@ -91,9 +95,10 @@ export async function autonomyRunsHandler(
export async function getAutonomyFlowsText( export async function getAutonomyFlowsText(
limit?: string | number, limit?: string | number,
options?: { rootDir?: string },
): Promise<string> { ): Promise<string> {
return formatAutonomyFlowsList( return formatAutonomyFlowsList(
await listAutonomyFlows(), await listAutonomyFlows(options?.rootDir),
parseAutonomyLimit(limit), parseAutonomyLimit(limit),
) )
} }
@@ -104,8 +109,11 @@ export async function autonomyFlowsHandler(
process.stdout.write(`${await getAutonomyFlowsText(limit)}\n`) process.stdout.write(`${await getAutonomyFlowsText(limit)}\n`)
} }
export async function getAutonomyFlowText(flowId: string): Promise<string> { export async function getAutonomyFlowText(
return formatAutonomyFlowDetail(await getAutonomyFlowById(flowId)) flowId: string,
options?: { rootDir?: string },
): Promise<string> {
return formatAutonomyFlowDetail(await getAutonomyFlowById(flowId, options?.rootDir))
} }
export async function autonomyFlowHandler(flowId: string): Promise<void> { export async function autonomyFlowHandler(flowId: string): Promise<void> {
@@ -116,9 +124,13 @@ export async function cancelAutonomyFlowText(
flowId: string, flowId: string,
options?: { options?: {
removeQueuedInMemory?: boolean removeQueuedInMemory?: boolean
rootDir?: string
}, },
): Promise<string> { ): Promise<string> {
const cancelled = await requestManagedAutonomyFlowCancel({ flowId }) const cancelled = await requestManagedAutonomyFlowCancel({
flowId,
rootDir: options?.rootDir,
})
if (!cancelled) { if (!cancelled) {
return 'Autonomy flow not found.' return 'Autonomy flow not found.'
} }
@@ -132,12 +144,12 @@ export async function cancelAutonomyFlowText(
removedCount = removed.length removedCount = removed.length
for (const command of removed) { for (const command of removed) {
if (command.autonomy?.runId) { if (command.autonomy?.runId) {
await markAutonomyRunCancelled(command.autonomy.runId) await markAutonomyRunCancelled(command.autonomy.runId, options?.rootDir)
} }
} }
} else { } else {
for (const runId of cancelled.queuedRunIds) { for (const runId of cancelled.queuedRunIds) {
await markAutonomyRunCancelled(runId) await markAutonomyRunCancelled(runId, options?.rootDir)
} }
removedCount = cancelled.queuedRunIds.length removedCount = cancelled.queuedRunIds.length
} }
@@ -155,9 +167,15 @@ export async function resumeAutonomyFlowText(
flowId: string, flowId: string,
options?: { options?: {
enqueueInMemory?: boolean enqueueInMemory?: boolean
rootDir?: string
currentDir?: string
}, },
): Promise<string> { ): Promise<string> {
const command = await resumeManagedAutonomyFlowPrompt({ flowId }) const command = await resumeManagedAutonomyFlowPrompt({
flowId,
rootDir: options?.rootDir,
currentDir: options?.currentDir,
})
if (!command) { if (!command) {
return 'Autonomy flow is not waiting or was not found.' return 'Autonomy flow is not waiting or was not found.'
} }