test(permission): cover Auto lifecycle and inheritance boundaries

This commit is contained in:
pku-xht committed 2026-09-06 05:29:42 +08:00
1 parent cdce3a9137
commit 0861ee11f7
4 files changed
+76 -25

No files matched your search

+21 -3
View File
@@ -22,6 +22,7 @@ import type {} from '@deepseek-ai/dsh-permission-presets'
import type {} from '@deepseek-ai/dsh-agent-presets'
import type {} from '@deepseek-ai/dsh-commands'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-terminal'
import { launchWebScaffold, readPersistedEvents, type WebScaffold } from './scaffold.ts'
import { REPO_ROOT } from './support.ts'
@@ -34,7 +35,7 @@ const AUTO_CHILD_OVERLAY_PATH = join(REPO_ROOT, 'apps/web/tests/auto-review-chil
const AUTO_PROVIDER = 'shipped-auto-review-test'
const AUTO_MODEL = 'same-route'
const AUTO_CALL_ID = ToolCallId('shipped-auto-review-denied-delete')
const AUTO_RAW_REASON = ' direct user authorized inspection only\ndeletion scope was not authorized '
const AUTO_RAW_REASON = ` direct user authorized inspection only\nTEST_ONLY_SECRET_${'x'.repeat(16_384)} `
const AUTO_FINAL_TEXT = 'SHIPPED_AUTO_REVIEW_DENIAL_OBSERVED'
const AUTO_CHILD_ONE_SHOT = 'AUTO_CHILD_ONE_SHOT'
const AUTO_CHILD_CONTINUABLE = 'AUTO_CHILD_CONTINUABLE'
@@ -769,6 +770,7 @@ it('routes one browser-authored Auto request through the same model before a rea
const finalModelInput = JSON.stringify(finalMain?.messages)
expect(finalModelInput).toContain('Auto review rejected tool \\"bash\\"; its body was not executed')
expect(finalModelInput).not.toContain('direct user authorized inspection only')
expect(finalModelInput).not.toContain('TEST_ONLY_SECRET_')
const events = agent.session.snapshotEvents()
const prompt = events.find((event): event is Extract<SessionEvent, { type: 'user/message' }> => (
@@ -790,6 +792,7 @@ it('routes one browser-authored Auto request through the same model before a rea
const durableModelResult = JSON.stringify(result?.data.message)
expect(durableModelResult).toContain('Auto review rejected tool \\"bash\\"; its body was not executed')
expect(durableModelResult).not.toContain('direct user authorized inspection only')
expect(durableModelResult).not.toContain('TEST_ONLY_SECRET_')
expect(events.some(event => (
event.type === 'assistant/message'
&& JSON.stringify(event.data.message).includes(AUTO_FINAL_TEXT)
@@ -1035,10 +1038,14 @@ it('withdraws Auto on shipped Loader unload and does not restore migrated live s
if (autoEntry === undefined) throw new Error('shipped Auto review Loader entry is missing')
const handle = await ctx.agents.create({
sessionId: SessionId('shipped-auto-hot-plug'),
meta: { cwd: scaffold.workspaceCwd },
setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
meta: { cwd: scaffold.workspaceCwd, agentPreset: 'minimal' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
const terminals = ctx.agentPresets.serviceFor(handle.agent, 'terminals')
try {
if (terminals === undefined) throw new Error('shipped minimal preset has no terminal registry')
ctx.permissionPresets.set(handle.agent.session, 'danger-full-access')
const terminal = await terminals.spawn(handle.agent, { type: 'shell', cwd: scaffold.workspaceCwd })
ctx.permissionPresets.set(handle.agent.session, 'auto')
expect(ctx.permissionPresets.current(handle.agent.session)).toBe('auto')
@@ -1046,6 +1053,16 @@ it('withdraws Auto on shipped Loader unload and does not restore migrated live s
await ctx.loader.await()
expect(ctx.permissionPresets.names).not.toContain('auto')
expect(ctx.permissionPresets.current(handle.agent.session)).toBe('danger-full-access')
expect(ctx.sandboxPolicy.overrideOf(handle.agent.session)).toBe('danger-full-access')
expect(ctx.approval.overrideOf(handle.agent.session)).toBe('never')
expect(terminals.list(handle.agent)).toMatchObject([
{ sessionId: terminal.sessionId, pid: terminal.pid, status: { kind: 'running' } },
])
const sent = await terminals.startSend(handle.agent, terminal.sessionId, {
text: 'echo AUTO_TERMINAL_SURVIVED', submit: true,
}).done
expect(sent.sessionStatus).toEqual({ kind: 'running' })
expect(sent.viewport).toContain('AUTO_TERMINAL_SURVIVED')
await autoEntry.update({ disabled: false })
await ctx.loader.await()
@@ -1054,4 +1071,5 @@ it('withdraws Auto on shipped Loader unload and does not restore migrated live s
} finally {
await handle.dispose()
}
expect(terminals?.hasOwnerActivity(handle.agent)).toBe(false)
}, 120_000)
@@ -161,7 +161,7 @@ async function mount(ctx: Context, workspace: string, dshHome: string): Promise<
function orchestrate(ctx: Context, real: boolean) {
let script: StreamChunk[][] = []
let expected: Verdict | undefined
let expected: (Verdict & { reason?: string }) | undefined
let mainAgent: Agent | undefined
let offeredTool = ''
let bodyStarts = 0
@@ -194,7 +194,7 @@ function orchestrate(ctx: Context, real: boolean) {
calls: () => reviewCalls,
async action(
agent: Agent, name: string, args: Record<string, unknown>, path: Path,
prompt: string, verdict?: Verdict,
prompt: string, verdict?: Verdict & { reason?: string },
): Promise<{ events: readonly SessionEvent[]; bodies: number }> {
expect(script).toHaveLength(0)
mainAgent = agent
@@ -369,12 +369,19 @@ it.each(['native', 'ptc-inner'] as const)('feeds denial back, re-reviews a new c
})
try {
ctx.permissionPresets.set(handle.agent.session, AUTO_PRESET)
const rawReason = ` TEST_ONLY_SECRET_${'x'.repeat(16_384)}\nexact deletion was not authorized `
for (let attempt = 0; attempt < 2; attempt += 1) {
const result = await runner.action(handle.agent, 'bash', { command: `rm -- ${quote(target)}` }, path,
'Inspect only; do not delete existing files.', { risk: 'medium', decision: 'deny' })
'Inspect only; do not delete existing files.', { risk: 'medium', decision: 'deny', reason: rawReason })
expect(result.bodies).toBe(0)
expect(outcome(result.events, path).denied).toBe(true)
const denials = result.events
.filter(event => event.type === 'tool/result' || event.type === 'tool/code-dispatch')
.map(event => event.data.error)
.filter(error => error?.code === DENIED)
expect(denials).toEqual([{ name: 'AutoReviewDeniedError', code: DENIED, reason: rawReason }])
expect(runner.postToolInputs.at(-1)?.includes('Auto review rejected tool \\"bash\\"; its body was not executed')).toBe(true)
expect(runner.postToolInputs.at(-1)).not.toContain('TEST_ONLY_SECRET_')
}
const result = await runner.action(handle.agent, 'write', { file_path: join(root, 'analysis.txt'), content: 'analysis\n' }, path,
'Write the local analysis instead.', { risk: 'low', decision: 'allow' })
@@ -107,23 +107,30 @@ describe('in-process policy inheritance', () => {
},
)
it('places delegated Full access after an Auto fork prefix', async () => {
it.each([
{ seedPreset: 'auto', preset: 'danger-full-access' },
{ seedPreset: 'danger-full-access', preset: 'auto' },
] as const)('captures $preset before child creation and overrides the $seedPreset fork prefix', async ({ seedPreset, preset }) => {
const { ctx, parent } = await setupWalled([textResponse('child done')])
parent.session.append('permission/preset', { preset: 'auto' })
parent.session.append('permission/preset', { preset: seedPreset })
setSandboxMode(parent.session, 'danger-full-access')
const seed = parent.session.snapshotEvents()
parent.session.append('permission/preset', { preset: 'danger-full-access' })
parent.session.append('permission/preset', { preset })
let currentPreset: 'auto' | 'danger-full-access' = preset
ctx.provide('permissionPresets', {
current: (session: Session) => session === parent.session ? 'danger-full-access' : 'custom',
current: (session: Session) => session === parent.session ? currentPreset : 'custom',
} as never)
const run = await startInProcessRun(spawnRequest(parent), { seed })
const starting = startInProcessRun(spawnRequest(parent), { seed })
currentPreset = seedPreset
parent.session.append('permission/preset', { preset: seedPreset })
const run = await starting
try {
await run.result
const child = run.localAgent as Agent
expect(child.session.snapshotEvents().filter(event => event.type === 'permission/preset')).toMatchObject([
{ data: { preset: 'auto' } },
{ data: { preset: 'danger-full-access' } },
{ data: { preset: seedPreset } },
{ data: { preset } },
])
} finally {
await run.dispose()
@@ -89,15 +89,14 @@ function foldedApprovalPolicy(ctx: Context, id: SessionId, events: readonly Sess
describe('continuable policy inheritance', () => {
it.each(['auto', 'danger-full-access'] as const)(
'persists the parent %s identity for a DSH in-process child',
'persists the delegated %s identity across cold resume without reading the parent again',
{ timeout: 20_000 },
async (preset) => {
const { ctx, parent } = await setup([textResponse('child done')])
const { ctx, parent } = await setup([textResponse('child done'), textResponse('resumed child done')])
parent.session.append('permission/preset', { preset })
setSandboxMode(parent.session, 'danger-full-access')
ctx.provide('permissionPresets', {
current: (session: Session) => session === parent.session ? preset : 'custom',
} as never)
const current = vi.fn((session: Session) => session === parent.session ? preset : 'custom')
ctx.provide('permissionPresets', { current } as never)
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
@@ -112,29 +111,49 @@ describe('continuable policy inheritance', () => {
{ type: 'approval/policy', data: { policy: 'never', source: 'delegation' } },
{ type: 'permission/preset', data: { preset } },
])
expect(current).toHaveBeenCalledExactlyOnceWith(parent.session)
parent.session.append('permission/preset', { preset: preset === 'auto' ? 'danger-full-access' : 'auto' })
current.mockImplementation(() => { throw new Error('cold resume must not read parent permission') })
await queueHostSubagentPrompt(
ctx.subagents, parent, started.childId,
[{ type: 'text', text: 'continue please' }], { kind: 'user' }, new AbortController().signal,
)
await waitNoActivation(ctx, started.childId)
const resumed = await loadStoredSession(ctx.sessionPersistence, started.childId)
expect(resumed.events.filter(event => event.type === 'permission/preset')).toMatchObject([
{ data: { preset } },
])
expect(current).toHaveBeenCalledTimes(1)
},
)
it('places delegated Full access after an Auto fork prefix', { timeout: 20_000 }, async () => {
it.each([
{ seedPreset: 'auto', preset: 'danger-full-access' },
{ seedPreset: 'danger-full-access', preset: 'auto' },
] as const)('captures $preset before child creation and overrides the $seedPreset fork prefix', { timeout: 20_000 }, async ({ seedPreset, preset }) => {
const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('forked child')])
parent.session.append('permission/preset', { preset: 'auto' })
parent.session.append('permission/preset', { preset: seedPreset })
setSandboxMode(parent.session, 'danger-full-access')
parent.followup(createUserMessage({
content: [{ type: 'text', text: 'parent work' }],
source: { kind: 'user' },
}))
await parent.whenIdle()
parent.session.append('permission/preset', { preset: 'danger-full-access' })
parent.session.append('permission/preset', { preset })
let currentPreset: 'auto' | 'danger-full-access' = preset
ctx.provide('permissionPresets', {
current: (session: Session) => session === parent.session ? 'danger-full-access' : 'custom',
current: (session: Session) => session === parent.session ? currentPreset : 'custom',
} as never)
const started = await ctx.subagents.startContinuable(startSpec(parent, 'fork'))
const starting = ctx.subagents.startContinuable(startSpec(parent, 'fork'))
currentPreset = seedPreset
parent.session.append('permission/preset', { preset: seedPreset })
const started = await starting
await waitNoActivation(ctx, started.childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
expect(loaded.events.filter(event => event.type === 'permission/preset')).toMatchObject([
{ data: { preset: 'auto' } },
{ data: { preset: 'danger-full-access' } },
{ data: { preset: seedPreset } },
{ data: { preset } },
])
})