test(windows): use host path semantics

This commit is contained in:
imccyu
2026-07-17 21:08:59 +08:00
parent 228f6e3867
commit 7e620db8ba
4 changed files with 33 additions and 31 deletions

View File

@@ -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')

View File

@@ -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')

View File

@@ -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 () => {

View File

@@ -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())
}) })
}) })