瀏覽代碼

test(windows): use host path semantics

imccyu 2 月之前
父節點
當前提交
7e620db8ba

+ 23 - 23
packages/context/workspace-context/tests/workspace-context.spec.ts

@@ -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',
-        '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'))
       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('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(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).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(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')

+ 4 - 3
packages/fs/tool-fs-search/tests/tools.spec.ts

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

+ 4 - 3
packages/spill/spill-local/tests/spill-local.spec.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 () => {

+ 2 - 2
packages/util/paths/tests/paths.spec.ts

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