test(permission): cover Auto lifecycle and inheritance boundaries
This commit is contained in:
1 parent
cdce3a9137
commit
0861ee11f7
4 files changed
+76
-25
No files matched your search
@@ -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 } },
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user