fix(agent-presets): repair Windows preset CI

This commit is contained in:
Yichen Jiang
2026-08-10 14:22:55 +08:00
parent 6648ff984a
commit 75100ad2e5
7 changed files with 70 additions and 17 deletions
@@ -65,20 +65,23 @@ describe('copying a preset', () => {
expect(listed.find(preset => preset.id === 'mine')?.trust).toBe('user')
})
it('copies the whole directory, execute bits kept and group/other stripped', async () => {
it('copies the whole directory and tightens POSIX modes', async () => {
await seedPreset(userRoot, 'source', {
extras: { 'skills/demo/SKILL.md': '# demo\n', 'skills/demo/run.sh': '#!/bin/sh\n' },
})
await chmod(join(userRoot, 'source', 'skills', 'demo', 'run.sh'), 0o755)
if (process.platform !== 'win32') {
await chmod(join(userRoot, 'source', 'skills', 'demo', 'run.sh'), 0o755)
}
await ctx.agentPresets.copy('source', 'mine')
expect(await readFile(join(userRoot, 'mine', 'skills', 'demo', 'SKILL.md'), 'utf8')).toBe('# demo\n')
// A preset may ship runnable helpers; the copy keeps them runnable for the
// owner while withdrawing the world-readability of the install.
expect((await stat(join(userRoot, 'mine', 'skills', 'demo', 'run.sh'))).mode & 0o777).toBe(0o700)
expect((await stat(join(userRoot, 'mine', 'skills', 'demo', 'SKILL.md'))).mode & 0o777).toBe(0o600)
expect((await stat(join(userRoot, 'mine'))).mode & 0o777).toBe(0o700)
// Windows mode bits are synthetic and cannot represent the inherited DACL.
if (process.platform !== 'win32') {
expect((await stat(join(userRoot, 'mine', 'skills', 'demo', 'run.sh'))).mode & 0o777).toBe(0o700)
expect((await stat(join(userRoot, 'mine', 'skills', 'demo', 'SKILL.md'))).mode & 0o777).toBe(0o600)
expect((await stat(join(userRoot, 'mine'))).mode & 0o777).toBe(0o700)
}
})
it('keeps the source description but never its name or order', async () => {
@@ -1,14 +1,37 @@
import { chmod, mkdtemp, mkdir, writeFile } from 'node:fs/promises'
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { COMPOSITION_FILE, discoverPresets, scanRoot } from '@deepseek-ai/dsh-agent-presets'
const fsHarness = vi.hoisted(() => ({
nextReadError: undefined as NodeJS.ErrnoException | undefined,
}))
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
readFile: (async (path: unknown, ...rest: never[]) => {
const error = fsHarness.nextReadError
if (error !== undefined) {
fsHarness.nextReadError = undefined
throw error
}
return (actual.readFile as (path: unknown, ...args: never[]) => Promise<unknown>)(path, ...rest)
}) as typeof actual.readFile,
}
})
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
const SYSTEM = { path: join(FIXTURES, 'system'), trust: 'system' as const }
const USER = { path: join(FIXTURES, 'user'), trust: 'user' as const }
beforeEach(() => {
fsHarness.nextReadError = undefined
})
describe('display order', () => {
it('puts declared order first, then everything else by id', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-order-'))
@@ -177,10 +200,11 @@ describe('composition health', () => {
await mkdir(join(root, 'sealed'))
const path = join(root, 'sealed', COMPOSITION_FILE)
await writeFile(path, '[]\n')
await chmod(path, 0o000)
fsHarness.nextReadError = Object.assign(new Error('EACCES: injected read failure'), { code: 'EACCES' })
const [preset] = await scanRoot({ path: root, trust: 'user' })
expect(fsHarness.nextReadError).toBeUndefined()
expect(preset?.broken).toMatch(/cannot be read/)
})
@@ -11,7 +11,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { beforeEach, describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AgentPresets, {
COMPOSITION_FILE, leakedServices, livePresetMounts, mountPreset, PresetMountError, serviceForAgent,
} from '@deepseek-ai/dsh-agent-presets'
@@ -84,6 +84,23 @@ beforeEach(async () => {
})
describe('composing an agent from a preset', () => {
it('hands an absolute plugin path to Node as a file URL', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-absolute-plugin-'))
const presetDir = join(root, 'absolute')
const plugin = join(FIXTURES, 'plugins', 'contribute.js')
await mkdir(presetDir)
await writeFile(
join(presetDir, COMPOSITION_FILE),
`- id: only\n name: ${plugin}\n config:\n tool: absolute\n`,
)
const scoped = await harness({ default: 'absolute', roots: [{ path: root, trust: 'user' }] })
const imported = vi.spyOn(scoped.loader.internal!, 'import')
await agentOn(scoped, 'sess-absolute-plugin')
expect(imported).toHaveBeenCalledWith(pathToFileURL(plugin).href, expect.any(String), {})
})
it('gives each session only its own preset\'s tools', async () => {
const alpha = await agentOn(ctx, 'sess-alpha', 'standard')
const beta = await agentOn(ctx, 'sess-beta', 'minimal')