Files
deepseek-harness/packages/guard/source-guard/tests/loader-composition.e2e.ts
T

94 lines
4.4 KiB
TypeScript

import { mkdir, readdir, readFile, realpath, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { type SessionEvent } from '@deepseek-ai/dsh-session'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
// The Loader config lives under examples so both launch modes exercise the same
// deployable topology: a local fixture adapter plus bare workspace plugins.
const configPath = fileURLToPath(new URL(
'../../../../examples/headless-agent/tests/fixtures/guard/source-guard/cordis.yml',
import.meta.url,
))
const binScript = fileURLToPath(new URL('../../../examples/cli-demo/src/bin.ts', import.meta.url))
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
/** Every `.jsonl` session log under `dir`. */
async function jsonlFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true })
const paths = await Promise.all(entries.map(async (entry) => {
const path = join(dir, entry.name)
if (entry.isDirectory()) return jsonlFiles(path)
return entry.isFile() && entry.name.endsWith('.jsonl') ? [path] : []
}))
return paths.flat()
}
/**
* Write git metadata mirroring the installer layout — a master clone owning the
* shared git directory and one linked worktree on a staging branch — and return
* the worktree file the model will try to write.
*/
async function stagingFixture(cwd: string): Promise<{ checkout: string; target: string }> {
const gitDir = join(cwd, 'master', '.git')
const worktreeGitDir = join(gitDir, 'worktrees', 'staging')
await mkdir(worktreeGitDir, { recursive: true })
await writeFile(join(gitDir, 'HEAD'), 'ref: refs/heads/master\n')
await writeFile(join(worktreeGitDir, 'HEAD'), 'ref: refs/heads/dsh-staging/20260101T000000Z\n')
const checkout = join(cwd, 'staging')
await mkdir(checkout, { recursive: true })
await writeFile(join(checkout, '.git'), `gitdir: ${worktreeGitDir}\n`)
const target = join(checkout, 'guarded.ts')
await writeFile(target, 'original\n')
return { checkout, target }
}
describe('source-guard through a real headless cordis.yml', () => {
it('denies the model-requested write and leaves the staged file untouched', async () => {
let events: SessionEvent[] = []
let contents = ''
let target = ''
const { stderr } = await runLoaderSmoke({
label: 'source-guard headless smoke',
tempDirPrefix: 'source-guard-e2e-',
binScript,
configPath,
tsconfigPath: repoTsconfig,
binArgs: ['--config', configPath, 'edit the guarded file'],
// The isolated cwd is not known when these options are built, so the
// config and adapter resolve their fixture paths against the child's own
// cwd, which is that directory.
prepare: async (cwd) => {
// macOS puts the temp directory behind the /var -> /private/var
// symlink; the child resolves its cwd, so compare against the same
// real path rather than the symlinked one this process was handed.
target = (await stagingFixture(await realpath(cwd))).target
},
inspect: async (cwd) => {
const logs = await jsonlFiles(join(cwd, '.sessions'))
expect(logs).toHaveLength(1)
const lines = (await readFile(logs[0] as string, 'utf8')).trimEnd().split('\n')
events = lines.slice(1).map(line => JSON.parse(line) as SessionEvent)
contents = await readFile(target, 'utf8')
},
})
expect(stderr).not.toContain('UNHANDLED')
const results = events.filter(
(event): event is SessionEvent<'tool/result'> => event.type === 'tool/result')
expect(results).toHaveLength(1)
const result = results[0]?.data.message.content[0]
expect(result?.isError).toBe(true)
const text = result?.content.map(block => block.type === 'text' ? block.text : '').join('')
expect(text).toBe(
`Error: Editing "${target}" directly is not allowed: it is inside the dsh checkout this session is running from, `
+ 'on branch dsh-staging/20260101T000000Z. Load the dsh-customize skill first and follow it '
+ '— implement in a task worktree, then integrate under the staging lock.',
)
// Enforcement, not advice: the guard denies before dispatch, so the file
// the model targeted still holds its original bytes.
expect(contents).toBe('original\n')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})