Files
claude-code/tests/integration/dependency-overrides.test.ts
unraid f2e9af4927 feat: harden autonomy lifecycle, OOM bounds, and provider-boundary finalization
This PR consolidates a coordinated batch of fixes around autonomy run/flow lifecycle, scheduled task deduplication, provider-boundary state finalization, and matching memory-bound treatments for adjacent long-running subsystems (REPL fullscreen scrollback, skill-search/skill-learning runtime activation). All changes were developed and reviewed together because they touched the same lifecycle invariants and were uncovered by the same long-running session reproductions.

## Lifecycle correctness

- Queued autonomy prompts are not injected unless the persisted run was successfully claimed; queued run claiming is now terminal-safe so a once-consumed/cancelled/failed run can not slip back into `queued`.
- Autonomy run/flow finalization happens on completion, provider error, generator close, and cancellation — not just the happy path. New `src/__tests__/queryAutonomyProviderBoundary.test.ts` covers these provider-boundary transitions.
- `requestManagedAutonomyFlowCancel` and `resumeManagedAutonomyFlowPrompt` carry `rootDir` and `currentDir` explicitly across detached async boundaries (proactive-tick, cron, daemon restart) instead of inferring from process state.
- Active runs/flows are protected from janitor pruning so a running step can not be garbage-collected mid-flight (`src/utils/autonomyAuthority.ts`).
- Heartbeat parser now ignores fenced code blocks; the two-phase commit window for autonomy state transitions is documented in `docs/internals/autonomy-jira.md`.

## Ownership and dedup

- `src/utils/autonomyRuns.ts`: ownership stamping (run id + rootDir carried end-to-end), source-based dedup against active runs.
- `src/hooks/useScheduledTasks.ts`: scheduled ticks deduplicate against runs already active on the same source label.
- `src/utils/processUserInput/processSlashCommand.tsx`: forked slash commands now thread the autonomy `runId` so completion finalizers can find the originating run for deferred completion.
- New `src/utils/autonomyQueueLifecycle.ts` and tests collect the queue-side lifecycle invariants in one place.

## Memory bounds (related, same review pass)

- `src/screens/REPL.tsx`: caps fullscreen scrollback after the compact boundary and updates trailing progress rows in place. Long-running fullscreen sessions could otherwise retain thousands of post-compaction messages and duplicate progress rows, keeping Ink trees alive long after their useful context had moved on.
- `src/services/skillSearch/*` and `src/services/skillLearning/*`: runtime activation is strictly opt-in via existing env toggles; session caches are capped so long-running processes can not grow them forever. Build presence is preserved so operators can still discover and opt into the slash commands.

## CI / test contract

- `tests/integration/dependency-overrides.test.ts`: smoke test no longer drives Mermaid's browser renderer; it validates the package-resolution contract directly so CI does not regress on unrelated browser timing.
- New `tests/integration/autonomy-lifecycle-user-flow.test.ts`: end-to-end CLI subprocess flow exercising `status --deep`, `flows`, `flow <id>`, `flow resume`, `flow cancel` against persisted state.
- `src/entrypoints/cli.tsx`: `claude autonomy …` routes through an entrypoint fast path that reuses the slash-command formatter without booting the full interactive CLI. Stdout is flushed before forced exit so coverage subprocesses do not terminate with empty stdout.
- `packages/builtin-tools/src/tools/RemoteTriggerTool/__tests__/RemoteTriggerTool.test.ts`: stabilized to prevent audit flake under coverage.

## Tests added

- `src/__tests__/queryAutonomyProviderBoundary.test.ts`
- `src/hooks/__tests__/useScheduledTasks.test.ts`
- `src/utils/__tests__/autonomyAuthority.test.ts`
- `src/utils/__tests__/autonomyFlows.test.ts` (extended)
- `src/utils/__tests__/autonomyPersistence.test.ts` (extended)
- `src/utils/__tests__/autonomyQueueLifecycle.test.ts`
- `src/utils/__tests__/autonomyRuns.test.ts` (extended)
- `src/utils/processUserInput/__tests__/processSlashCommand.test.ts`
- `tests/integration/autonomy-lifecycle-user-flow.test.ts`

## Docs

- `docs/agent/sur-loop-scheduled-oom.md`: System Understanding Report covering the scheduled/loop OOM problem, the call graphs investigated, and the lifecycle invariants this PR establishes.
- `docs/agent/sur-skill-overflow-bugs.md`: SUR for the related skill-overflow context.
- `docs/internals/autonomy-jira.md`: documents the two-phase commit window and ownership stamping invariants.
- `docs/memory-leak-audit.md`: audit notes covering the REPL/scrollback and skill-search bounds.

## Invariants this PR establishes

1. Queued autonomy prompts are not injected unless the persisted run was successfully claimed.
2. Terminal run/flow states are terminal — completion, failure, and cancellation all finalize state regardless of which provider/error path triggered them.
3. Autonomy run/flow `rootDir` is carried explicitly across detached async boundaries instead of inferred from a shared singleton.
4. State-only CLI subcommands (`autonomy status|runs|flows|flow …`) bypass full interactive bootstrap so they do not hold unrelated handles open.
5. REPL fullscreen scrollback and skill-search/skill-learning session caches are explicitly bounded.

## Validation

```bash
bun run typecheck
CI=true GITHUB_ACTIONS=true bun test            # 3996 pass / 0 fail across 305 files
bun test src/__tests__/queryAutonomyProviderBoundary.test.ts \
         src/hooks/__tests__/useScheduledTasks.test.ts \
         src/utils/__tests__/autonomy{Runs,Flows,Authority,QueueLifecycle,Persistence}.test.ts \
         src/utils/processUserInput/__tests__/processSlashCommand.test.ts \
         tests/integration/autonomy-lifecycle-user-flow.test.ts
```

## Origin

This PR is the consolidated, upstream-targeted version of two fork-side review PRs (fix/loop-scheduled-autonomy-oom and fix/autonomy-lifecycle). The fork-side review history is preserved at https://github.com/amDosion/claude-code-bast/pull/7 . The fork's own internal `chore: keep fork current with upstream` sync commits and the `docs: update contributors` automation are intentionally not included in this PR.

The autonomy CLI handler `rootDir` threading that the fork added (78f64d8a, 98d04ddb) is intentionally omitted here because upstream `a2cfaf91` (fix: 修复 RemoteTriggerTool 和 autonomy 测试的全量运行失败) already performed the equivalent change with an additional `currentDir` option. Keeping the upstream version avoids regressing that improvement.
2026-04-29 14:04:27 +08:00

146 lines
5.0 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const repoRoot = resolve(import.meta.dir, '..', '..')
const uuidV4Pattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
describe('dependency security overrides', () => {
test('mcpb can load patched inquirer prompts from its package context', async () => {
const mcpbRequire = createRequire(import.meta.resolve('@anthropic-ai/mcpb'))
const promptsPath = mcpbRequire.resolve('@inquirer/prompts')
const prompts = (await import(pathToFileURL(promptsPath).href)) as {
input?: unknown
select?: unknown
}
expect(typeof prompts.input).toBe('function')
expect(typeof prompts.select).toBe('function')
})
test('google auth gaxios multipart boundary still uses a UUID', async () => {
const vertexRequire = createRequire(
import.meta.resolve('@anthropic-ai/vertex-sdk'),
)
const gaxios = vertexRequire('gaxios') as {
request(options: {
adapter(options: {
headers: Headers
url: string
}): Promise<{
config: unknown
data: string
headers: Record<string, string>
request: { responseURL: string }
status: number
statusText: string
}>
multipart: Array<{ body: string; headers: Record<string, string> }>
url: string
}): Promise<{ status: number }>
}
let contentType: string | undefined
const response = await gaxios.request({
url: 'https://example.com/upload',
multipart: [{ body: 'payload', headers: { 'Content-Type': 'text/plain' } }],
adapter: async (options) => {
contentType = options.headers.get('content-type') ?? undefined
return {
config: options,
data: '',
headers: {},
request: { responseURL: options.url },
status: 200,
statusText: 'OK',
}
},
})
expect(response.status).toBe(200)
expect(contentType).toMatch(
/^multipart\/related; boundary=[0-9a-f-]{36}$/,
)
expect(contentType?.split('boundary=')[1]).toMatch(uuidV4Pattern)
})
test('azure identity msal guid generation works through its package context', () => {
const identityRequire = createRequire(import.meta.resolve('@azure/identity'))
const msal = identityRequire('@azure/msal-node') as {
CryptoProvider: new () => { createNewGuid(): string }
}
const cryptoProvider = new msal.CryptoProvider()
expect(cryptoProvider.createNewGuid()).toMatch(uuidV4Pattern)
})
test('remote control markdown renderer resolves streamdown and mermaid', async () => {
const rcsRequire = createRequire(
join(repoRoot, 'packages/remote-control-server/package.json'),
)
const streamdownPath = rcsRequire.resolve('streamdown')
const streamdown = (await import(pathToFileURL(streamdownPath).href)) as {
Streamdown?: unknown
}
const streamdownRequire = createRequire(streamdownPath)
const uuid = (await import(
pathToFileURL(streamdownRequire.resolve('uuid')).href
)) as { v4(): string }
const mermaidPath = streamdownRequire.resolve('mermaid')
const mermaidPackagePath = streamdownRequire.resolve(
'mermaid/package.json',
)
const mermaidPackage = JSON.parse(
readFileSync(mermaidPackagePath, 'utf8'),
) as {
name?: unknown
exports?: { '.'?: { import?: unknown } }
}
expect(streamdown.Streamdown).toBeDefined()
expect(uuid.v4()).toMatch(uuidV4Pattern)
expect(mermaidPackage.name).toBe('mermaid')
expect(mermaidPath).toContain('mermaid.core.mjs')
expect(mermaidPackage.exports?.['.']?.import).toBe(
'./dist/mermaid.core.mjs',
)
})
test('grpc proto-loader keeps its protobuf 7 parser path working', () => {
const exporterRequire = createRequire(
import.meta.resolve('@opentelemetry/exporter-trace-otlp-grpc'),
)
const grpcRequire = createRequire(exporterRequire.resolve('@grpc/grpc-js'))
const protoLoader = grpcRequire('@grpc/proto-loader') as {
loadSync(
path: string,
options?: Record<string, unknown>,
): Record<string, unknown>
}
const tempDir = mkdtempSync(join(tmpdir(), 'proto-loader-smoke-'))
const protoPath = join(tempDir, 'service.proto')
writeFileSync(
protoPath,
[
'syntax = "proto3";',
'package smoke;',
'message Ping { string id = 1; }',
'service PingService { rpc Send(Ping) returns (Ping); }',
].join('\n'),
)
try {
const loaded = protoLoader.loadSync(protoPath, { keepCase: true })
expect(loaded['smoke.Ping']).toBeDefined()
expect(loaded['smoke.PingService']).toBeDefined()
} finally {
rmSync(tempDir, { force: true, recursive: true })
}
})
})