|
@@ -77,6 +77,7 @@ async function write(path: string, content: string): Promise<void> {
|
|
|
|
|
|
|
|
class RecordingFileSystem extends FileSystem {
|
|
class RecordingFileSystem extends FileSystem {
|
|
|
entries = new Map<string, { type: FsInfo['type']; content?: string; version?: FsVersion }>()
|
|
entries = new Map<string, { type: FsInfo['type']; content?: string; version?: FsVersion }>()
|
|
|
|
|
+ missingOnStat = new Set<string>()
|
|
|
throwOnStat = new Set<string>()
|
|
throwOnStat = new Set<string>()
|
|
|
throwOnRead = new Set<string>()
|
|
throwOnRead = new Set<string>()
|
|
|
omitSizes = new Set<string>()
|
|
omitSizes = new Set<string>()
|
|
@@ -105,6 +106,9 @@ class RecordingFileSystem extends FileSystem {
|
|
|
override async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {
|
|
override async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {
|
|
|
if (signal !== undefined) this.signals.push(signal)
|
|
if (signal !== undefined) this.signals.push(signal)
|
|
|
signal?.throwIfAborted()
|
|
signal?.throwIfAborted()
|
|
|
|
|
+ if (this.missingOnStat.has(target.targetKey)) {
|
|
|
|
|
+ throw Object.assign(new Error(`not found: ${target.displayPath}`), { code: 'FS_NOT_FOUND' })
|
|
|
|
|
+ }
|
|
|
if (this.throwOnStat.has(target.targetKey)) throw new Error(`stat failed: ${target.displayPath}`)
|
|
if (this.throwOnStat.has(target.targetKey)) throw new Error(`stat failed: ${target.displayPath}`)
|
|
|
const entry = this.entries.get(target.targetKey)
|
|
const entry = this.entries.get(target.targetKey)
|
|
|
if (entry === undefined) return undefined
|
|
if (entry === undefined) return undefined
|
|
@@ -2073,6 +2077,30 @@ describe('workspace context request injection', () => {
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ it('continues provider root discovery when a marker is confirmed absent', async () => {
|
|
|
|
|
+ const root = join(await tempRepo(), 'virtual-repo')
|
|
|
|
|
+ const cwd = join(root, 'pkg')
|
|
|
|
|
+ const home = join(await tempRepo(), 'virtual-home')
|
|
|
|
|
+ const ctx = new Context()
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ctx.plugin(RecordingFileSystem)
|
|
|
|
|
+ const fs = ctx.fs as RecordingFileSystem
|
|
|
|
|
+ fs.missingOnStat.add(join(cwd, '.git'))
|
|
|
|
|
+ fs.entries.set(join(root, '.git'), { type: 'directory' })
|
|
|
|
|
+ fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'provider parent rule' })
|
|
|
|
|
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
|
|
|
|
|
+ const agent = await stubAgent(cwd)
|
|
|
|
|
+
|
|
|
|
|
+ await composeBaselinePrefix(ctx, agent)
|
|
|
|
|
+
|
|
|
|
|
+ expect(derivedText(agent)).toContain('provider parent rule')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ await ctx.fiber.dispose()
|
|
|
|
|
+ await rm(dirname(root), { recursive: true, force: true })
|
|
|
|
|
+ await rm(dirname(home), { recursive: true, force: true })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
it('keeps the direct provider API usable without an operation signal', async () => {
|
|
it('keeps the direct provider API usable without an operation signal', async () => {
|
|
|
const root = resolve('/virtual/no-signal-repo')
|
|
const root = resolve('/virtual/no-signal-repo')
|
|
|
const home = resolve('/virtual/no-signal-home')
|
|
const home = resolve('/virtual/no-signal-home')
|
|
@@ -2291,23 +2319,24 @@ describe('workspace context request injection', () => {
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('treats ctx.fs marker lookup failures as absent root markers', async () => {
|
|
|
|
|
|
|
+ it('surfaces ctx.fs marker lookup failures instead of crossing into an ancestor project', async () => {
|
|
|
const root = await tempRepo()
|
|
const root = await tempRepo()
|
|
|
|
|
+ const cwd = join(root, 'pkg')
|
|
|
const home = await tempRepo()
|
|
const home = await tempRepo()
|
|
|
try {
|
|
try {
|
|
|
- await mkdir(join(root, '.git'), { recursive: true })
|
|
|
|
|
- await write(join(root, 'AGENTS.md'), 'repo rule')
|
|
|
|
|
const ctx = new Context()
|
|
const ctx = new Context()
|
|
|
await ctx.plugin(RecordingFileSystem)
|
|
await ctx.plugin(RecordingFileSystem)
|
|
|
const fs = ctx.fs as RecordingFileSystem
|
|
const fs = ctx.fs as RecordingFileSystem
|
|
|
- fs.throwOnStat.add(join(root, '.git'))
|
|
|
|
|
- fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'repo rule' })
|
|
|
|
|
|
|
+ fs.throwOnStat.add(join(cwd, '.git'))
|
|
|
|
|
+ fs.entries.set(join(root, '.git'), { type: 'directory' })
|
|
|
|
|
+ fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'ancestor rule must not load' })
|
|
|
await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
|
|
await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
|
|
|
- const agent = await stubAgent(root)
|
|
|
|
|
|
|
+ const agent = await stubAgent(cwd)
|
|
|
|
|
|
|
|
- await composeBaselinePrefix(ctx, agent)
|
|
|
|
|
|
|
+ await expect(composeBaselinePrefix(ctx, agent))
|
|
|
|
|
+ .rejects.toThrow(`stat failed: ${join(cwd, '.git')}`)
|
|
|
|
|
|
|
|
- expect(derivedText(agent)).toContain('repo rule')
|
|
|
|
|
|
|
+ expectNoDerivedMessages(agent)
|
|
|
} finally {
|
|
} finally {
|
|
|
await rm(root, { recursive: true, force: true })
|
|
await rm(root, { recursive: true, force: true })
|
|
|
await rm(home, { recursive: true, force: true })
|
|
await rm(home, { recursive: true, force: true })
|
|
@@ -2540,6 +2569,42 @@ describe('workspace context request injection', () => {
|
|
|
await rm(home, { recursive: true, force: true })
|
|
await rm(home, { recursive: true, force: true })
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ it('surfaces host marker metadata failures instead of crossing into an ancestor project', async () => {
|
|
|
|
|
+ const root = await tempRepo()
|
|
|
|
|
+ const home = await tempRepo()
|
|
|
|
|
+ try {
|
|
|
|
|
+ const cwd = join(root, 'pkg')
|
|
|
|
|
+ const markerPath = join(cwd, '.git')
|
|
|
|
|
+ const failure = Object.assign(new Error(`permission denied: ${markerPath}`), {
|
|
|
|
|
+ code: 'EACCES',
|
|
|
|
|
+ path: markerPath,
|
|
|
|
|
+ })
|
|
|
|
|
+ await mkdir(join(root, '.git'), { recursive: true })
|
|
|
|
|
+ await write(join(root, 'AGENTS.md'), 'ancestor rule must not load')
|
|
|
|
|
+ await mkdir(cwd, { recursive: true })
|
|
|
|
|
+ vi.resetModules()
|
|
|
|
|
+ vi.doMock('node:fs/promises', async (importOriginal) => {
|
|
|
|
|
+ const actual = await importOriginal<typeof import('node:fs/promises')>()
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...actual,
|
|
|
|
|
+ stat: async (path: string) => {
|
|
|
|
|
+ if (path === markerPath) throw failure
|
|
|
|
|
+ return actual.stat(path)
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const isolated = await import('@deepseek-ai/dsh-agent-instructions')
|
|
|
|
|
+
|
|
|
|
|
+ await expect(isolated.loadBaselineInstructions({ cwd, dshHome: home, maxBytes: 65536 }))
|
|
|
|
|
+ .rejects.toBe(failure)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ vi.doUnmock('node:fs/promises')
|
|
|
|
|
+ vi.resetModules()
|
|
|
|
|
+ await rm(root, { recursive: true, force: true })
|
|
|
|
|
+ await rm(home, { recursive: true, force: true })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('dynamic nested workspace context injection', () => {
|
|
describe('dynamic nested workspace context injection', () => {
|