mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
- `catalogHistory` validated its durable read. `agent.session.events` is a JSONL/SQLite seed on resume or fork, and seed validation guarantees only a source object with a non-empty `kind`; a `skill-catalog` record with missing or wrongly shaped `entries` threw inside the step listener, failing every later turn of that session. It is now skipped as an unrecognizable record, the posture the replaced content digest had, with a regression test over six malformed shapes. - The headless keyless smoke still filtered catalogs by the old plugin source, so the `built-bin-smoke` gate would not have found the catalog message. - Entries record the published description unescaped. The pseudo-XML escaping belongs to the `<available_skills>` frame and is applied when rendering it, so a description containing `<` no longer reaches the card as `<`. `escapeText` is injective, so republish semantics and the model-facing text are unchanged. - Adjacent text blocks join with no separator, matching how provider adapters flatten them; the body no longer shows a line break the model never saw. - Provenance fields are bounded like the text: an unknown producer may record an arbitrarily large value. - Both readers are all-or-nothing, and the row's form marker reports what rendered rather than what was declared, so a partly unreadable record cannot present a confident but incomplete account. - The catalog body consumes `update` as a replacement notice; the digest canonicalizes per entry as JSON, since every separator character is itself legal in a description. - `core.md` documents the form axis with a `ContextForm` type-equiv block, and both projections assert the wiring they duplicate.
88 lines
4.4 KiB
TypeScript
88 lines
4.4 KiB
TypeScript
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { zstdDecompress } from 'node:zlib'
|
|
import { promisify } from 'node:util'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
|
import { PREPARED_ENTRY_FILENAME, prepareDshPlugin } from '@deepseek-ai/dsh-repository-plugin'
|
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
|
|
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
|
const configPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
|
|
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
|
const decompress = promisify(zstdDecompress)
|
|
|
|
describe('headless-agent keyless smoke', () => {
|
|
it('boots the real Loader tree, runs a real bash tool round trip, and persists the turn', async () => {
|
|
let persistedHeader: Record<string, unknown> | undefined
|
|
const { stdout, stderr } = await runLoaderSmoke({
|
|
label: 'headless-agent',
|
|
tempDirPrefix: 'headless-agent-smoke-',
|
|
binScript,
|
|
configPath,
|
|
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'prove the tool path'],
|
|
tsconfigPath,
|
|
inspect: async (cwd) => {
|
|
const files = await readdir(cwd, { recursive: true })
|
|
const relativePath = files.find(file => file.endsWith('.jsonl.zstd'))
|
|
if (relativePath === undefined) return
|
|
const compressed = await readFile(join(cwd, relativePath))
|
|
expect(compressed.subarray(0, 4).toString('hex')).toBe('28b52ffd')
|
|
persistedHeader = JSON.parse((await decompress(compressed)).toString()) as Record<string, unknown>
|
|
},
|
|
})
|
|
const lines = stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
|
|
const events = lines.slice(0, -1).map(line => line['event'] as SessionEvent)
|
|
const result = lines.at(-1)
|
|
expect(stderr).toBe('')
|
|
expect(events.some(event => event.type === 'tool/call' && event.data.name === 'bash')).toBe(true)
|
|
const catalogMessage = events.find(event => event.type === 'user/message'
|
|
&& event.data.source.kind === 'skill-catalog')
|
|
const catalog = catalogMessage?.type === 'user/message'
|
|
? catalogMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('\n')
|
|
: ''
|
|
expect(catalog.split('\n').find(line => line.includes('repository-fixture'))).toMatchInlineSnapshot(
|
|
`
|
|
"- \`repository-fixture\`: Repository fixture skill."
|
|
`,
|
|
)
|
|
const toolResult = events.find(event => event.type === 'tool/result')
|
|
expect(JSON.stringify(toolResult)).toContain('CLI_TOOL_ROUND_TRIP')
|
|
expect(result).toMatchObject({
|
|
type: 'result',
|
|
success: true,
|
|
turn: 1,
|
|
reason: { kind: 'completed' },
|
|
usage: { inputTokens: 18, outputTokens: 8, cacheReadTokens: 2, reasoningTokens: 1 },
|
|
})
|
|
expect(String(result?.['result'])).toContain('CLI_TOOL_ROUND_TRIP')
|
|
expect(persistedHeader).toMatchObject({ type: 'session' })
|
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
|
|
|
it('keeps the checked-in prepared wrapper identical to the generator output for its manifest', async () => {
|
|
// The fixture claims "Generated by dsh-plugin-prepare"; this pin makes the
|
|
// claim true — a wrapper-template change fails here until the fixture is
|
|
// regenerated, so the assembled smoke can never exercise a stale shape.
|
|
const fixture = fileURLToPath(new URL('./fixtures/repository-plugin/', import.meta.url))
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-fixture-drift-'))
|
|
try {
|
|
const plugin = join(root, '.dsh-plugin')
|
|
await mkdir(plugin, { recursive: true })
|
|
await cp(join(fixture, 'dsh-plugin-assets/skills/0'), join(root, 'skills'), { recursive: true })
|
|
await writeFile(join(plugin, 'package.json'), `${JSON.stringify({
|
|
name: 'headless-repository-fixture',
|
|
version: '0.0.0',
|
|
dsh: { skills: ['../skills'] },
|
|
}, undefined, 2)}\n`)
|
|
await prepareDshPlugin(plugin)
|
|
const generated = await readFile(join(plugin, PREPARED_ENTRY_FILENAME), 'utf8')
|
|
const checkedIn = await readFile(join(fixture, PREPARED_ENTRY_FILENAME), 'utf8')
|
|
expect(checkedIn).toBe(generated)
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|