test(workspace-context): make the merged suite's fixtures cross-platform
Path expectations derive from join() per the cross-platform fixtures convention; the fake fs resolves against the host root so drive-letter targets match seeded keys; the unreadable-candidate cases use the provider throwOnRead fixture (host chmod 0 is a no-op for the owner on Windows), keeping the read-failure branch covered there, with a narrow win32 skip only for the host-chmod discovery case.
This commit is contained in:
@@ -58,6 +58,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 }>()
|
||||||
throwOnStat = new Set<string>()
|
throwOnStat = new Set<string>()
|
||||||
|
throwOnRead = new Set<string>()
|
||||||
omitSizes = new Set<string>()
|
omitSizes = new Set<string>()
|
||||||
readTargets: string[] = []
|
readTargets: string[] = []
|
||||||
readTextTargets: string[] = []
|
readTextTargets: string[] = []
|
||||||
@@ -66,7 +67,9 @@ class RecordingFileSystem extends FileSystem {
|
|||||||
override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {
|
override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {
|
||||||
if (opts?.signal !== undefined) this.signals.push(opts.signal)
|
if (opts?.signal !== undefined) this.signals.push(opts.signal)
|
||||||
opts?.signal?.throwIfAborted()
|
opts?.signal?.throwIfAborted()
|
||||||
const absolute = join(opts?.cwd ?? '/', path)
|
// resolve(), not join(): entries are seeded with host join() keys, and on
|
||||||
|
// Windows a joined '/'-rooted prefix would not match a resolved drive path.
|
||||||
|
const absolute = resolve(opts?.cwd ?? '/', path)
|
||||||
return { targetKey: FsTargetKey(absolute), displayPath: absolute }
|
return { targetKey: FsTargetKey(absolute), displayPath: absolute }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +111,7 @@ class RecordingFileSystem extends FileSystem {
|
|||||||
if (signal !== undefined) this.signals.push(signal)
|
if (signal !== undefined) this.signals.push(signal)
|
||||||
signal?.throwIfAborted()
|
signal?.throwIfAborted()
|
||||||
this.readTargets.push(target.targetKey)
|
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 ?? ''
|
const content = this.entries.get(target.targetKey)?.content ?? ''
|
||||||
return (async function* () {
|
return (async function* () {
|
||||||
const midpoint = Math.ceil(content.length / 2)
|
const midpoint = Math.ceil(content.length / 2)
|
||||||
@@ -383,7 +387,8 @@ describe('workspace context instruction discovery', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('skips a file that becomes unreadable after discovery without failing the request', async () => {
|
// POSIX-only fixture: chmod 0 cannot make a file unreadable to its owner on Windows.
|
||||||
|
it.skipIf(process.platform === 'win32')('skips a file that becomes unreadable after discovery without failing the request', async () => {
|
||||||
const root = await tempRepo()
|
const root = await tempRepo()
|
||||||
const home = await tempRepo()
|
const home = await tempRepo()
|
||||||
try {
|
try {
|
||||||
@@ -1818,7 +1823,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
const text = blocksText(workspaceContextOf(result)?.content)
|
const text = blocksText(workspaceContextOf(result)?.content)
|
||||||
expect(text).toBe([
|
expect(text).toBe([
|
||||||
'<system-reminder>',
|
'<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.',
|
'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.',
|
||||||
'',
|
'',
|
||||||
@@ -2115,7 +2120,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(changed)?.content)).toBe([
|
expect(blocksText(workspaceContextOf(changed)?.content)).toBe([
|
||||||
'<system-reminder>',
|
'<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.',
|
'This file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.',
|
||||||
'',
|
'',
|
||||||
@@ -2300,7 +2305,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(removed)?.content)).toBe([
|
expect(blocksText(workspaceContextOf(removed)?.content)).toBe([
|
||||||
'<system-reminder>',
|
'<system-reminder>',
|
||||||
'Instructions removed: pkg/AGENTS.md',
|
`Instructions removed: ${join('pkg', 'AGENTS.md')}`,
|
||||||
'',
|
'',
|
||||||
'The previously loaded instructions from this file no longer apply.',
|
'The previously loaded instructions from this file no longer apply.',
|
||||||
'</system-reminder>',
|
'</system-reminder>',
|
||||||
@@ -2755,16 +2760,23 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('skips unreadable nested instruction files without attaching empty context', async () => {
|
it('skips unreadable nested instruction files without attaching empty context', async () => {
|
||||||
|
// Cross-platform unreadable fixture: the provider read throws (chmod 0
|
||||||
|
// cannot make a file unreadable to its owner on Windows).
|
||||||
const root = await tempRepo()
|
const root = await tempRepo()
|
||||||
const home = await tempRepo()
|
const home = await tempRepo()
|
||||||
|
const ctx = new Context()
|
||||||
try {
|
try {
|
||||||
await mkdir(join(root, '.git'), { recursive: true })
|
|
||||||
const nested = join(root, 'pkg/AGENTS.md')
|
const nested = join(root, 'pkg/AGENTS.md')
|
||||||
await write(nested, 'nested package rule')
|
await ctx.plugin(SystemPrompt)
|
||||||
await write(join(root, 'pkg/deep/file.txt'), 'hello')
|
await ctx.plugin(ToolRegistry)
|
||||||
await chmod(nested, 0)
|
await ctx.plugin(RecordingFileSystem)
|
||||||
const ctx = new Context()
|
const fs = ctx.fs as RecordingFileSystem
|
||||||
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
|
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({
|
const result = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
@@ -2776,8 +2788,9 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
expect(result.isError).toBe(false)
|
expect(result.isError).toBe(false)
|
||||||
expect(result.additionalContexts).toBeUndefined()
|
expect(result.additionalContexts).toBeUndefined()
|
||||||
await chmod(nested, 0o600)
|
expect(fs.readTargets).toContain(nested)
|
||||||
} finally {
|
} finally {
|
||||||
|
await ctx.fiber.dispose()
|
||||||
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 })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user