Merge branch 'codex/tool-json-schema-dsl' into codex/canonical-tool-output

This commit is contained in:
Tianyi Cui
2026-07-22 17:05:38 +08:00
482 changed files with 37413 additions and 1133 deletions

View File

@@ -1,5 +1,5 @@
import { chmod, mkdtemp, mkdir, rm, stat, symlink, utimes, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { mkdtemp, mkdir, rm, stat, symlink, utimes, writeFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
@@ -61,6 +61,7 @@ class RecordingFileSystem extends FileSystem {
entries = new Map<string, { type: FsInfo['type']; content?: string; version?: FsVersion }>()
lstatTypes = new Map<string, FsPathInfo['type']>()
throwOnStat = new Set<string>()
throwOnRead = new Set<string>()
omitSizes = new Set<string>()
readTargets: string[] = []
readTextTargets: string[] = []
@@ -69,7 +70,7 @@ class RecordingFileSystem extends FileSystem {
override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {
if (opts?.signal !== undefined) this.signals.push(opts.signal)
opts?.signal?.throwIfAborted()
const absolute = join(opts?.cwd ?? '/', path)
const absolute = resolve(opts?.cwd ?? '/', path)
return { targetKey: FsTargetKey(absolute), displayPath: absolute }
}
@@ -113,6 +114,7 @@ class RecordingFileSystem extends FileSystem {
if (signal !== undefined) this.signals.push(signal)
signal?.throwIfAborted()
this.readTargets.push(target.targetKey)
if (this.throwOnRead.has(target.targetKey)) throw new Error(`read failed: ${target.displayPath}`)
const content = this.entries.get(target.targetKey)?.content ?? ''
return (async function* () {
const midpoint = Math.ceil(content.length / 2)
@@ -299,8 +301,8 @@ describe('workspace context instruction discovery', () => {
expect(files.map(file => file.displayPath)).toEqual([
'$DSH_HOME/AGENTS.md',
'AGENTS.md',
'packages/CLAUDE.md',
'packages/app/AGENTS.md',
join('packages', 'CLAUDE.md'),
join('packages', 'app', 'AGENTS.md'),
])
expect(files.map(file => file.absolutePath)).not.toContain(join(root, 'CLAUDE.md'))
} finally {
@@ -358,22 +360,25 @@ describe('workspace context instruction discovery', () => {
}
})
it('skips a file that becomes unreadable after discovery without failing the request', async () => {
it('skips a provider file whose read fails after a successful metadata probe', async () => {
const root = await tempRepo()
const home = await tempRepo()
const ctx = new Context()
try {
const cwd = join(root, 'pkg')
await mkdir(join(root, '.git'), { recursive: true })
await mkdir(cwd, { recursive: true })
const leaf = join(cwd, 'AGENTS.md')
await write(leaf, 'secret-ish rule')
await chmod(leaf, 0)
await ctx.plugin(RecordingFileSystem)
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(leaf, { type: 'file', content: 'secret-ish rule' })
fs.throwOnRead.add(leaf)
const loaded = await loadBaselineInstructions({ cwd, dshHome: home, maxBytes: 65536 })
const loaded = await loadBaselineInstructions({ cwd, dshHome: home, maxBytes: 65536 }, fs)
expect(loaded).toBeUndefined()
await chmod(leaf, 0o600)
expect(fs.readTargets).toEqual([leaf])
} finally {
await ctx.fiber.dispose()
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
}
@@ -960,7 +965,7 @@ describe('workspace context request injection', () => {
await composeBaselinePrefix(ctx, agent)
expect(derivedText(agent)).toContain('omitted AGENTS.md')
expect(derivedText(agent)).toContain('Instructions from: pkg/AGENTS.md\n\npackage rule')
expect(derivedText(agent)).toContain(`Instructions from: ${join('pkg', 'AGENTS.md')}\n\npackage rule`)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1140,8 +1145,8 @@ describe('workspace context request injection', () => {
})
it('keeps the direct provider API usable without an operation signal', async () => {
const root = '/virtual/no-signal-repo'
const home = '/virtual/no-signal-home'
const root = resolve('/virtual/no-signal-repo')
const home = resolve('/virtual/no-signal-home')
const ctx = new Context()
try {
await ctx.plugin(RecordingFileSystem)
@@ -1449,7 +1454,7 @@ describe('workspace context request injection', () => {
await composeBaselinePrefix(ctx, agent)
expect(derivedText(agent)).toContain('Instructions from: AGENTS.md\n\nroot schema default rule')
expect(derivedText(agent)).toContain('Instructions from: child/AGENTS.md\n\nchild schema default rule')
expect(derivedText(agent)).toContain(`Instructions from: ${join('child', 'AGENTS.md')}\n\nchild schema default rule`)
await ctx.fiber.dispose()
} finally {
await rm(root, { recursive: true, force: true })
@@ -1754,7 +1759,7 @@ describe('dynamic nested workspace context injection', () => {
changes: [{
action: 'set',
scope: 'pkg',
path: 'pkg/AGENTS.md',
path: join('pkg', 'AGENTS.md'),
}],
})
const meta = workspaceContextOf(result)?.meta
@@ -1768,7 +1773,7 @@ describe('dynamic nested workspace context injection', () => {
const text = blocksText(workspaceContextOf(result)?.content)
expect(text).toBe([
'<system-reminder>',
'Additional instructions from: pkg/AGENTS.md',
`Additional instructions from: ${join('pkg', 'AGENTS.md')}`,
'',
'These instructions apply to work under `pkg`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.',
'',
@@ -1807,7 +1812,7 @@ describe('dynamic nested workspace context injection', () => {
})
const text = blocksText(workspaceContextOf(result)?.content)
expect(text).toContain('Additional instructions from: pkg/CLAUDE.local.md')
expect(text).toContain(`Additional instructions from: ${join('pkg', 'CLAUDE.local.md')}`)
expect(text).toContain('local package rule')
expect(text).not.toContain('native package rule')
} finally {
@@ -1988,11 +1993,11 @@ describe('dynamic nested workspace context injection', () => {
expect(workspaceContextOf(changed)?.meta).toMatchObject({
kind: 'workspace-instructions',
changes: [{ action: 'replace', scope: 'pkg', path: 'pkg/AGENTS.md' }],
changes: [{ action: 'replace', scope: 'pkg', path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(workspaceContextOf(changed)?.content)).toBe([
'<system-reminder>',
'Updated instructions from: pkg/AGENTS.md',
`Updated instructions from: ${join('pkg', 'AGENTS.md')}`,
'',
'This file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.',
'',
@@ -2035,11 +2040,11 @@ describe('dynamic nested workspace context injection', () => {
expect(workspaceContextOf(changed)?.meta).toMatchObject({
changes: [{
action: 'replace', scope: 'pkg', path: 'pkg/CLAUDE.md', previousPath: 'pkg/AGENTS.md',
action: 'replace', scope: 'pkg', path: join('pkg', 'CLAUDE.md'), previousPath: join('pkg', 'AGENTS.md'),
}],
})
expect(blocksText(workspaceContextOf(changed)?.content)).toContain('Updated instructions from: pkg/CLAUDE.md')
expect(blocksText(workspaceContextOf(changed)?.content)).toContain('The instructions previously loaded from `pkg/AGENTS.md` no longer apply. Use the following content for `pkg` instead.')
expect(blocksText(workspaceContextOf(changed)?.content)).toContain(`Updated instructions from: ${join('pkg', 'CLAUDE.md')}`)
expect(blocksText(workspaceContextOf(changed)?.content)).toContain(`The instructions previously loaded from \`${join('pkg', 'AGENTS.md')}\` no longer apply. Use the following content for \`pkg\` instead.`)
expect(blocksText(workspaceContextOf(changed)?.content)).toContain('fallback package rule')
expect(unchanged.additionalContexts).toBeUndefined()
} finally {
@@ -2073,11 +2078,11 @@ describe('dynamic nested workspace context injection', () => {
expect(workspaceContextOf(removed)?.meta).toEqual({
kind: 'workspace-instructions',
version: 1,
changes: [{ action: 'remove', scope: 'pkg', path: 'pkg/AGENTS.md' }],
changes: [{ action: 'remove', scope: 'pkg', path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(workspaceContextOf(removed)?.content)).toBe([
'<system-reminder>',
'Instructions removed: pkg/AGENTS.md',
`Instructions removed: ${join('pkg', 'AGENTS.md')}`,
'',
'The previously loaded instructions from this file no longer apply.',
'</system-reminder>',
@@ -2118,9 +2123,9 @@ describe('dynamic nested workspace context injection', () => {
})
expect(workspaceContextOf(restored)?.meta).toMatchObject({
changes: [{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md' }],
changes: [{ action: 'set', scope: 'pkg', path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('Additional instructions from: pkg/AGENTS.md')
expect(blocksText(workspaceContextOf(restored)?.content)).toContain(`Additional instructions from: ${join('pkg', 'AGENTS.md')}`)
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('restored package rule')
} finally {
await rm(root, { recursive: true, force: true })
@@ -2225,7 +2230,7 @@ describe('dynamic nested workspace context injection', () => {
const update = resumed.session.events.findLast(event => event.type === 'context/message')
expect(update?.type === 'context/message' && update.data.meta).toMatchObject({
changes: [{ action: 'replace', scope: 'pkg', path: 'pkg/AGENTS.md' }],
changes: [{ action: 'replace', scope: 'pkg', path: join('pkg', 'AGENTS.md') }],
})
expect(update?.type === 'context/message' && blocksText(update.data.content)).toContain('new nested rule after resume')
} finally {
@@ -2353,8 +2358,8 @@ describe('dynamic nested workspace context injection', () => {
})
const firstText = blocksText(workspaceContextOf(first)?.content)
expect(firstText).toContain('omitted pkg/AGENTS.md')
expect(firstText).not.toContain('## pkg/AGENTS.md')
expect(firstText).toContain(`omitted ${join('pkg', 'AGENTS.md')}`)
expect(firstText).not.toContain(`## ${join('pkg', 'AGENTS.md')}`)
expect(firstText).toContain('subtree rule')
expect(blocksText(workspaceContextOf(second)?.content)).toContain('parent rule')
} finally {
@@ -2498,14 +2503,19 @@ describe('dynamic nested workspace context injection', () => {
it('skips unreadable nested instruction files without attaching empty context', async () => {
const root = await tempRepo()
const home = await tempRepo()
const ctx = new Context()
try {
await mkdir(join(root, '.git'), { recursive: true })
const nested = join(root, 'pkg/AGENTS.md')
await write(nested, 'nested package rule')
await write(join(root, 'pkg/deep/file.txt'), 'hello')
await chmod(nested, 0)
const ctx = new Context()
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(RecordingFileSystem)
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(nested, { type: 'file', content: 'nested package rule' })
fs.entries.set(join(root, 'pkg/deep/file.txt'), { type: 'file', content: 'hello' })
fs.throwOnRead.add(nested)
await ctx.plugin(ToolFs)
await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
const result = await ctx.tools.execute({
signal: testToolSignal,
@@ -2517,8 +2527,9 @@ describe('dynamic nested workspace context injection', () => {
expect(result.isError).toBe(false)
expect(result.additionalContexts).toBeUndefined()
await chmod(nested, 0o600)
expect(fs.readTargets).toContain(nested)
} finally {
await ctx.fiber.dispose()
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
}
@@ -2568,7 +2579,7 @@ describe('dynamic nested workspace context injection', () => {
expect(workspaceContextOf(result)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(workspaceContextOf(result)?.meta).toMatchObject({
kind: 'workspace-instructions',
changes: [{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md' }],
changes: [{ action: 'set', scope: 'pkg', path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(workspaceContextOf(result)?.content)).toContain('nested package rule')
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')