test(windows): use host path semantics
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { chmod, mkdtemp, mkdir, rm, stat, symlink, utimes, writeFile } from 'node:fs/promises'
|
import { chmod, mkdtemp, mkdir, rm, stat, symlink, utimes, writeFile } from 'node:fs/promises'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join, resolve } from 'node:path'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
@@ -61,7 +61,7 @@ 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)
|
const absolute = resolve(opts?.cwd ?? '/', path)
|
||||||
return { targetKey: FsTargetKey(absolute), displayPath: absolute }
|
return { targetKey: FsTargetKey(absolute), displayPath: absolute }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,8 +277,8 @@ describe('workspace context instruction discovery', () => {
|
|||||||
expect(files.map(file => file.displayPath)).toEqual([
|
expect(files.map(file => file.displayPath)).toEqual([
|
||||||
'$DSH_HOME/AGENTS.md',
|
'$DSH_HOME/AGENTS.md',
|
||||||
'AGENTS.md',
|
'AGENTS.md',
|
||||||
'packages/CLAUDE.md',
|
join('packages', 'CLAUDE.md'),
|
||||||
'packages/app/AGENTS.md',
|
join('packages', 'app', 'AGENTS.md'),
|
||||||
])
|
])
|
||||||
expect(files.map(file => file.absolutePath)).not.toContain(join(root, 'CLAUDE.md'))
|
expect(files.map(file => file.absolutePath)).not.toContain(join(root, 'CLAUDE.md'))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -336,7 +336,7 @@ describe('workspace context instruction discovery', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('skips a file that becomes unreadable after discovery without failing the request', async () => {
|
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 {
|
||||||
@@ -932,7 +932,7 @@ describe('workspace context request injection', () => {
|
|||||||
await composeBaselinePrefix(ctx, agent)
|
await composeBaselinePrefix(ctx, agent)
|
||||||
|
|
||||||
expect(derivedText(agent)).toContain('omitted AGENTS.md')
|
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 {
|
} 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 })
|
||||||
@@ -1398,7 +1398,7 @@ describe('workspace context request injection', () => {
|
|||||||
await composeBaselinePrefix(ctx, agent)
|
await composeBaselinePrefix(ctx, agent)
|
||||||
|
|
||||||
expect(derivedText(agent)).toContain('Instructions from: AGENTS.md\n\nroot schema default rule')
|
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()
|
await ctx.fiber.dispose()
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
@@ -1700,7 +1700,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
changes: [{
|
changes: [{
|
||||||
action: 'set',
|
action: 'set',
|
||||||
scope: 'pkg',
|
scope: 'pkg',
|
||||||
path: 'pkg/AGENTS.md',
|
path: join('pkg', 'AGENTS.md'),
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
const meta = workspaceContextOf(result)?.meta
|
const meta = workspaceContextOf(result)?.meta
|
||||||
@@ -1714,7 +1714,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.',
|
||||||
'',
|
'',
|
||||||
@@ -1752,7 +1752,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const text = blocksText(workspaceContextOf(result)?.content)
|
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).toContain('local package rule')
|
||||||
expect(text).not.toContain('native package rule')
|
expect(text).not.toContain('native package rule')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1922,11 +1922,11 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
expect(workspaceContextOf(changed)?.meta).toMatchObject({
|
expect(workspaceContextOf(changed)?.meta).toMatchObject({
|
||||||
kind: 'workspace-instructions',
|
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([
|
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.',
|
||||||
'',
|
'',
|
||||||
@@ -1966,11 +1966,11 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
expect(workspaceContextOf(changed)?.meta).toMatchObject({
|
expect(workspaceContextOf(changed)?.meta).toMatchObject({
|
||||||
changes: [{
|
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(`Updated instructions from: ${join('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(`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(blocksText(workspaceContextOf(changed)?.content)).toContain('fallback package rule')
|
||||||
expect(unchanged.additionalContexts).toBeUndefined()
|
expect(unchanged.additionalContexts).toBeUndefined()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2002,11 +2002,11 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
expect(workspaceContextOf(removed)?.meta).toEqual({
|
expect(workspaceContextOf(removed)?.meta).toEqual({
|
||||||
kind: 'workspace-instructions',
|
kind: 'workspace-instructions',
|
||||||
version: 1,
|
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([
|
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>',
|
||||||
@@ -2044,9 +2044,9 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(restored)?.meta).toMatchObject({
|
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')
|
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('restored package rule')
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
@@ -2146,7 +2146,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const update = resumed.session.events.findLast(event => event.type === 'context/message')
|
const update = resumed.session.events.findLast(event => event.type === 'context/message')
|
||||||
expect(update?.type === 'context/message' && update.data.meta).toMatchObject({
|
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')
|
expect(update?.type === 'context/message' && blocksText(update.data.content)).toContain('new nested rule after resume')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2267,8 +2267,8 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const firstText = blocksText(workspaceContextOf(first)?.content)
|
const firstText = blocksText(workspaceContextOf(first)?.content)
|
||||||
expect(firstText).toContain('omitted pkg/AGENTS.md')
|
expect(firstText).toContain(`omitted ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(firstText).not.toContain('## pkg/AGENTS.md')
|
expect(firstText).not.toContain(`## ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(firstText).toContain('subtree rule')
|
expect(firstText).toContain('subtree rule')
|
||||||
expect(blocksText(workspaceContextOf(second)?.content)).toContain('parent rule')
|
expect(blocksText(workspaceContextOf(second)?.content)).toContain('parent rule')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2462,7 +2462,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
expect(workspaceContextOf(result)?.envelope).toBe('raw')
|
expect(workspaceContextOf(result)?.envelope).toBe('raw')
|
||||||
expect(workspaceContextOf(result)?.meta).toMatchObject({
|
expect(workspaceContextOf(result)?.meta).toMatchObject({
|
||||||
kind: 'workspace-instructions',
|
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)).toContain('nested package rule')
|
||||||
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')
|
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
|
import { join } from 'node:path'
|
||||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||||
@@ -416,7 +417,7 @@ describe('glob results', () => {
|
|||||||
const { ctx, bash } = await setup()
|
const { ctx, bash } = await setup()
|
||||||
bash.handler = () => runResult('/sessions/s1/src/a.ts\n/elsewhere/b.ts\nrel/c.ts\n')
|
bash.handler = () => runResult('/sessions/s1/src/a.ts\n/elsewhere/b.ts\nrel/c.ts\n')
|
||||||
const result = await call(ctx, 'glob', { pattern: '*' }, { agent: agent('/sessions/s1') })
|
const result = await call(ctx, 'glob', { pattern: '*' }, { agent: agent('/sessions/s1') })
|
||||||
expect(text(result)).toBe('src/a.ts\n/elsewhere/b.ts\nrel/c.ts')
|
expect(text(result)).toBe(`${join('src', 'a.ts')}\n/elsewhere/b.ts\nrel/c.ts`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('validates arguments (blank pattern, blank path)', async () => {
|
it('validates arguments (blank pattern, blank path)', async () => {
|
||||||
@@ -498,7 +499,7 @@ describe('grep results', () => {
|
|||||||
const { ctx, bash } = await setup()
|
const { ctx, bash } = await setup()
|
||||||
bash.handler = () => runResult(`${matchLine('/sessions/s1/deep/a.ts', 2, 'hit')}\n`)
|
bash.handler = () => runResult(`${matchLine('/sessions/s1/deep/a.ts', 2, 'hit')}\n`)
|
||||||
const result = await call(ctx, 'grep', { pattern: 'hit', path: '/sessions/s1' }, { agent: agent('/sessions/s1') })
|
const result = await call(ctx, 'grep', { pattern: 'hit', path: '/sessions/s1' }, { agent: agent('/sessions/s1') })
|
||||||
expect(text(result)).toContain('deep/a.ts\nLine 2: hit')
|
expect(text(result)).toContain(`${join('deep', 'a.ts')}\nLine 2: hit`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('previews a long matched line at grepMaxLineBytes preserving UTF-8', async () => {
|
it('previews a long matched line at grepMaxLineBytes preserving UTF-8', async () => {
|
||||||
@@ -608,7 +609,7 @@ describe('presentation', () => {
|
|||||||
|
|
||||||
describe('helpers', () => {
|
describe('helpers', () => {
|
||||||
it('toWorkdirRelative maps inside-workdir absolutes and passes everything else through', () => {
|
it('toWorkdirRelative maps inside-workdir absolutes and passes everything else through', () => {
|
||||||
expect(toWorkdirRelative('/w/a/b.ts', '/w')).toBe('a/b.ts')
|
expect(toWorkdirRelative('/w/a/b.ts', '/w')).toBe(join('a', 'b.ts'))
|
||||||
expect(toWorkdirRelative('/w', '/w')).toBe('.')
|
expect(toWorkdirRelative('/w', '/w')).toBe('.')
|
||||||
expect(toWorkdirRelative('/other/b.ts', '/w')).toBe('/other/b.ts')
|
expect(toWorkdirRelative('/other/b.ts', '/w')).toBe('/other/b.ts')
|
||||||
expect(toWorkdirRelative('/w-sibling/b.ts', '/w')).toBe('/w-sibling/b.ts')
|
expect(toWorkdirRelative('/w-sibling/b.ts', '/w')).toBe('/w-sibling/b.ts')
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest'
|
|||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
|
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { dirname, isAbsolute, join } from 'node:path'
|
import { basename, dirname, isAbsolute, join, normalize } from 'node:path'
|
||||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||||
import type { SaveTextSpill } from '@deepseek-ai/dsh-spill'
|
import type { SaveTextSpill } from '@deepseek-ai/dsh-spill'
|
||||||
@@ -63,7 +63,8 @@ describe('sessionDir', () => {
|
|||||||
it('is a stable per-session hash under the root', () => {
|
it('is a stable per-session hash under the root', () => {
|
||||||
const dir = sessionDir('/spill', 'sess-1')
|
const dir = sessionDir('/spill', 'sess-1')
|
||||||
expect(dir).toBe(sessionDir('/spill', 'sess-1'))
|
expect(dir).toBe(sessionDir('/spill', 'sess-1'))
|
||||||
expect(dir).toMatch(/\/spill\/session-[0-9a-f]{12}$/)
|
expect(dirname(dir)).toBe(normalize('/spill'))
|
||||||
|
expect(basename(dir)).toMatch(/^session-[0-9a-f]{12}$/)
|
||||||
expect(sessionDir('/spill', 'sess-2')).not.toBe(dir)
|
expect(sessionDir('/spill', 'sess-2')).not.toBe(dir)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -74,7 +75,7 @@ describe('saveTextFile', () => {
|
|||||||
expect(readFileSync(saved.path, 'utf8')).toBe('héllo')
|
expect(readFileSync(saved.path, 'utf8')).toBe('héllo')
|
||||||
expect(saved.bytes).toBe(Buffer.byteLength('héllo', 'utf8'))
|
expect(saved.bytes).toBe(Buffer.byteLength('héllo', 'utf8'))
|
||||||
expect(dirname(saved.path)).toBe(sessionDir(root, 'sess-1'))
|
expect(dirname(saved.path)).toBe(sessionDir(root, 'sess-1'))
|
||||||
expect(saved.path).toMatch(/\/[0-9a-f]{12}-r\.txt$/)
|
expect(basename(saved.path)).toMatch(/^[0-9a-f]{12}-r\.txt$/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sanitizes a traversal-shaped suggested name into one segment', async () => {
|
it('sanitizes a traversal-shaped suggested name into one segment', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { homedir } from 'node:os'
|
import { homedir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join, resolve } from 'node:path'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
DEFAULT_DSH_HOME_DISPLAY,
|
DEFAULT_DSH_HOME_DISPLAY,
|
||||||
@@ -28,7 +28,7 @@ describe('dsh path helpers', () => {
|
|||||||
const envHome = join(homedir(), 'env-dsh')
|
const envHome = join(homedir(), 'env-dsh')
|
||||||
|
|
||||||
expect(resolveDshHome(undefined, { DSH_HOME: '~/env-dsh' })).toBe(envHome)
|
expect(resolveDshHome(undefined, { DSH_HOME: '~/env-dsh' })).toBe(envHome)
|
||||||
expect(resolveDshHome('/tmp/explicit-dsh', { DSH_HOME: '~/env-dsh' })).toBe('/tmp/explicit-dsh')
|
expect(resolveDshHome('/tmp/explicit-dsh', { DSH_HOME: '~/env-dsh' })).toBe(resolve('/tmp/explicit-dsh'))
|
||||||
expect(resolveDshHome(undefined, {})).toBe(defaultDshHome())
|
expect(resolveDshHome(undefined, {})).toBe(defaultDshHome())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user