test: remove dsh-* temp dirs created by unit tests at teardown
Spec files that create /tmp/dsh-* directories via mkdtemp now track and delete them in afterEach/afterAll; module-scope fixture dirs (executor spill dirs) are removed in afterAll. The file list came from the observed-residue inventory on the self-hosted CI host: only specs whose dirs actually accumulated were leak sources (issue #3134), superseding the kept-but-unmerged CI sweep branch per the #3233 review decision. Product per-process spill roots (dsh-subprocess-local spawn, dsh-spill-local store) register a process-exit handler that removes the memoized dir, so processes that used the spawn/spill path clean up on normal exit. A SIGKILLed process cannot run in-process teardown; the machine-side timer remains the backstop for that path. Agent Note: .agents/notes/implemented/process/2026-08-28-test-temp-dir-self-cleanup.md
This commit is contained in:
1 parent
9fe2112a63
commit
0364343a7e
39 files changed
+461
-73
No files matched your search
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-28-test-temp-dir-self-cleanup.md
|
||||
2026-08-28-test-temp-dir-self-cleanup.md: e8e0c5e2db716ff9a63ef55ae1af07a66eaf28a0
|
||||
2026-08-28-test-temp-dir-self-cleanup.zh.md: cdc0dfa2b6a00fc11703743c51326238bf644fb1
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: Unit tests remove the dsh-* temp dirs they create
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-28-test-temp-dir-self-cleanup.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Test processes create `/tmp/dsh-*` directories with `mkdtemp(join(tmpdir(), 'dsh-*'))` and leave them behind. On the self-hosted Linux CI host (32 runner instances sharing one `/tmp`) the residue exhausted the root partition's inode capacity twice (issue #3134, 2026-08-13 and 2026-08-26). The machine-side `dsh-tmp-sweep` timer and the CI lane sweep (kept, unmerged, on branch `fix/ci-tmp-residue-cleanup`) remove residue after the fact but leave the producing defect in place. Human review of #3233 (2026-08-28) rejected the sweep: unit tests must clean up the directories they create instead.
|
||||
|
||||
## Decision
|
||||
|
||||
Retrofit removal of every `dsh-*` temp dir a spec file creates, at the owning test's teardown:
|
||||
|
||||
- Spec files that created dirs without removing any now track each created root in a module-level list and delete the list in `afterEach`/`afterAll` (`rm`/`rmSync` with `recursive: true, force: true`), the convention already used across the session packages. Root-creating helpers (`tmp()`, `tempDir()`, `fakeLauncher()`, harness functions) register the root at creation, so every caller is covered at one point.
|
||||
- Module-scope fixture dirs shared by a whole file (executor spill dirs) are removed in `afterAll` after the last test.
|
||||
- The file list came from the observed-residue inventory on the CI host (a template histogram of current `/tmp/dsh-*` dirs): only spec files whose dirs actually appeared were leak sources. Files that already remove their dirs (agent-team, tool-subagent, list-children, hooks coverage cases) were confirmed clean on the normal-exit path and left unchanged.
|
||||
- Product per-process spill roots (`privateSpillDir` in `dsh-subprocess-local/spawn`, `privateRoot` in `dsh-spill-local/store`) register a `process.once('exit')` handler that removes the memoized directory, so every process that used the spawn/spill path cleans up on normal exit.
|
||||
|
||||
## Verification
|
||||
|
||||
- Targeted local run of every changed spec (32 files, 700 tests) passed, including the suites that exercise the changed product sources.
|
||||
- CI runs the changed specs on the Linux and Windows coverage lanes; after a full green run, the fixed files' residue templates (observed at up to ~5,000 dirs per two hours each, e.g. `dsh-profile-`, `dsh-app-boot-`, `dsh-presets-*`, `dsh-upload-index-`) should no longer appear in fresh `/tmp` residue on the CI host.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Keep the sweep-only approach (rejected in review)
|
||||
|
||||
Sweep steps and timers delete residue after it exists; they do not stop local runs from accumulating, and a machine sweep cannot distinguish a dead run's residue from a live one's. The reviewer decision was per-test cleanup, implemented here for the normal-exit path.
|
||||
|
||||
### Introduce a shared temp-dir helper package
|
||||
|
||||
Not chosen: the files that leak each create roots through their own small helpers, and tracking them at those helpers is a per-file one-point change. A new test-support package would add a dependency without reducing the per-file audit.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Bought: on normal completion — including failed tests — a spec's `dsh-*` dirs are removed at teardown, and per-process spill roots are removed when their process exits normally.
|
||||
- Cost: a process killed with SIGKILL (a cancelled run, a timeout kill) cannot run any in-process teardown; its in-flight residue remains. The machine-side timer stays as the backstop for that path.
|
||||
- Cost: dirs created by a spawned child are covered only when the test knows their paths; product-owned per-process roots are covered by the exit handler in the process that created them.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note:单测删除自己创建的 dsh-* 临时目录
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-28-test-temp-dir-self-cleanup.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
测试进程用 `mkdtemp(join(tmpdir(), 'dsh-*'))` 创建 `/tmp/dsh-*` 目录后不清理。在自托管 Linux CI 主机上(32 个 runner 实例共享一个 `/tmp`),残留两次耗尽根分区 inode(issue #3134,2026-08-13 与 2026-08-26)。机器侧 `dsh-tmp-sweep` timer 与 CI lane sweep(保留未合并,在分支 `fix/ci-tmp-residue-cleanup` 上)都是事后删除残留,未修掉产生残留的缺陷本体。人类 review #3233(2026-08-28)否决了 sweep:单测应改为自己清理创建的目录。
|
||||
|
||||
## Decision
|
||||
|
||||
为 spec 文件创建的每个 `dsh-*` 临时目录补上删除路径,挂在所属测试的 teardown 上:
|
||||
|
||||
- 创建目录但从不删除的 spec 文件,现在把每个创建的 root 记入模块级列表,并在 `afterEach`/`afterAll` 里删除(`rm`/`rmSync` 带 `recursive: true, force: true`)——与 session 包既有的 `roots.splice(0)` 约定一致。创建 root 的 helper(`tmp()`、`tempDir()`、`fakeLauncher()`、harness 函数)在创建处登记,一个点覆盖全部调用方。
|
||||
- 整文件共享的模块级 fixture 目录(executor spill 目录)在最后一个测试之后的 `afterAll` 里删除。
|
||||
- 目标文件清单来自 CI 主机上的残留实测清单(当前 `/tmp/dsh-*` 目录的模板直方图):只有目录确实出现在残留里的 spec 文件才是泄漏源。已有删除逻辑的文件(agent-team、tool-subagent、list-children、hooks coverage cases)确认在正常结束路径上本来干净,不改。
|
||||
- 产品侧每进程 spill root(`dsh-subprocess-local/spawn` 的 `privateSpillDir`、`dsh-spill-local/store` 的 `privateRoot`)注册 `process.once('exit')` handler,在进程正常退出时删除记忆化的目录——凡走过 spawn/spill 路径的进程都会在正常结束时清理。
|
||||
|
||||
## Verification
|
||||
|
||||
- 本地定向跑过全部改动 spec(32 个文件、700 个测试)通过,含直接使用改动后产品源码的套件。
|
||||
- CI 在 Linux 与 Windows coverage lane 跑改动 spec;一次全绿后,被修文件的残留模板(实测每两小时最多各约 5,000 个目录,如 `dsh-profile-`、`dsh-app-boot-`、`dsh-presets-*`、`dsh-upload-index-`)应不再出现在 CI 主机的新鲜 `/tmp` 残留里。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### 保留纯 sweep 方案(review 否决)
|
||||
|
||||
Sweep 步骤与 timer 只删已存在的残留;本地运行仍会累积,机器 sweep 也区分不了已死 run 的残留与存活 run 的目录。review 的决定是逐测试清理,本实现覆盖正常结束路径。
|
||||
|
||||
### 引入共享临时目录 helper 包
|
||||
|
||||
未选:泄漏文件各自通过自己的小 helper 创建 root,在那些 helper 处登记是每个文件单点改动;新增 test-support 包只会增加依赖,不减少逐文件审计量。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 收益:正常结束(含测试失败)时,spec 的 `dsh-*` 目录在 teardown 删除;每进程 spill root 在其进程正常退出时删除。
|
||||
- 代价:被 SIGKILL 的进程(run 被取消、超时被杀)无法运行任何进程内 teardown,飞行中的残留仍在——机器侧 timer 继续兜底该路径。
|
||||
- 代价:子进程创建的目录只有在测试知道其路径时才被覆盖;产品自有每进程 root 由创建它的进程的 exit handler 覆盖。
|
||||
@@ -76,6 +76,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
await rm(userRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('offers the roster with copy as the only way to create', async () => {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//
|
||||
// Zero model calls: no replay fixture mounts, so a stray stream fails loud.
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
@@ -232,6 +232,7 @@ describe('web e2e: agent-preset selection', () => {
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
await rm(presetRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('offers the chip on the new-session screen, beside the workspace picker', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
@@ -22,8 +22,12 @@ import { installSessionReadTestServices, testSessionPersistence } from './test-r
|
||||
|
||||
const roots: Context[] = []
|
||||
|
||||
/** Session cwd roots created per test, removed after their context settles. */
|
||||
const tempDirs: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function harness(): Promise<{ ctx: Context; agents: ApiSessionAgentController }> {
|
||||
@@ -297,6 +301,7 @@ describe('ApiSession create or adoption', () => {
|
||||
it('shares one in-flight creation between concurrent callers', async () => {
|
||||
const { ctx, agents } = await harness()
|
||||
const cwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-concurrent-'))
|
||||
tempDirs.push(cwd)
|
||||
const meta = header('concurrent-create', cwd)
|
||||
const created = unpublishedAgent(ctx, meta)
|
||||
let release!: () => void
|
||||
@@ -317,6 +322,7 @@ describe('ApiSession create or adoption', () => {
|
||||
it('accepts a raced ordinary creation and rejects a raced attached child', async () => {
|
||||
const ordinary = await harness()
|
||||
const cwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-create-'))
|
||||
tempDirs.push(cwd)
|
||||
const ordinaryMeta = header('create-race', cwd)
|
||||
const winner = agent(ordinary.ctx, ordinaryMeta)
|
||||
vi.spyOn(ordinary.ctx.agents, 'create').mockImplementation(async () => {
|
||||
@@ -328,6 +334,7 @@ describe('ApiSession create or adoption', () => {
|
||||
|
||||
const child = await harness()
|
||||
const childCwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-child-'))
|
||||
tempDirs.push(childCwd)
|
||||
const childId = SessionId('create-child-race')
|
||||
vi.spyOn(child.ctx.agents, 'create').mockImplementation(async () => {
|
||||
child.ctx.sessions.create(childId, {
|
||||
@@ -342,6 +349,7 @@ describe('ApiSession create or adoption', () => {
|
||||
it('validates ownership and cwd on the Agent returned by creation', async () => {
|
||||
const child = await harness()
|
||||
const childCwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-returned-child-'))
|
||||
tempDirs.push(childCwd)
|
||||
const childMeta = {
|
||||
...header('returned-child', childCwd),
|
||||
parentSession: SessionId('parent'),
|
||||
@@ -357,6 +365,7 @@ describe('ApiSession create or adoption', () => {
|
||||
|
||||
const wrong = await harness()
|
||||
const requestedCwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-wrong-cwd-'))
|
||||
tempDirs.push(requestedCwd)
|
||||
const wrongAgent = unpublishedAgent(wrong.ctx, header('wrong-returned-cwd', '/other'))
|
||||
vi.spyOn(wrong.ctx.agents, 'create').mockResolvedValue({
|
||||
agent: wrongAgent,
|
||||
@@ -437,6 +446,7 @@ describe('ApiSession create or adoption', () => {
|
||||
it('surfaces directory creation failure and rejects setup without a scoped Agent', async () => {
|
||||
const { agents } = await harness()
|
||||
const parent = mkdtempSync(join(tmpdir(), 'dsh-session-controller-file-'))
|
||||
tempDirs.push(parent)
|
||||
const file = join(parent, 'file')
|
||||
writeFileSync(file, 'not a directory')
|
||||
await expect(agents.ensureSession(SessionId('mkdir-failure'), join(file, 'child'), false))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Session creation and adoption rules for Agent preset identity. */
|
||||
|
||||
import { mkdtempSync, realpathSync } from 'node:fs'
|
||||
import { mkdtempSync, realpathSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
@@ -10,9 +10,17 @@ import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-presets'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createSessionTestRemote } from './test-remote.ts'
|
||||
|
||||
/** Booted contexts and their temp roots, torn down after each test. */
|
||||
const contexts: Context[] = []
|
||||
const tempDirs: string[] = []
|
||||
afterEach(async () => {
|
||||
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function stubAgent(session: Session): Agent {
|
||||
return { id: session.id, session, status: 'idle' } as unknown as Agent
|
||||
}
|
||||
@@ -42,7 +50,9 @@ function roster(ids: readonly string[]): unknown {
|
||||
|
||||
async function harness(presets?: readonly string[]) {
|
||||
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-')))
|
||||
tempDirs.push(cwd)
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
if (presets !== undefined) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdirSync, mkdtempSync, realpathSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -22,8 +22,12 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
|
||||
const roots: Context[] = []
|
||||
|
||||
/** Workspace roots created per test, removed after their context settles. */
|
||||
const tempDirs: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
interface Deferred<T> {
|
||||
@@ -39,6 +43,7 @@ function deferred<T>(): Deferred<T> {
|
||||
|
||||
async function harness() {
|
||||
const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-workspace-controller-')))
|
||||
tempDirs.push(root)
|
||||
const ctx = new Context()
|
||||
roots.push(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve, sep } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import {
|
||||
@@ -13,7 +13,16 @@ import {
|
||||
|
||||
const NAME = 'dsh-test-bin'
|
||||
|
||||
const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-app-boot-'))
|
||||
const tempRoots: string[] = []
|
||||
afterAll(() => {
|
||||
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const tmp = (): string => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-app-boot-'))
|
||||
tempRoots.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
describe('resolveConfigPath', () => {
|
||||
it('resolves relative to the given cwd outside replay mode', () => {
|
||||
|
||||
@@ -7,18 +7,27 @@
|
||||
* shared overlay whose row exists only on another surface.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { loadOverlayPatches, renderConfigDump } from '../src/index.ts'
|
||||
|
||||
const NAME = 'dsh-test-bin'
|
||||
|
||||
const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-config-dump-'))
|
||||
const tempRoots: string[] = []
|
||||
afterAll(() => {
|
||||
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const tmp = (): string => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-config-dump-'))
|
||||
tempRoots.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
function writeBase(dir: string): string {
|
||||
const base = join(dir, 'base.yml')
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
* previous generation has been retained or restored.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { Include } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { boot } from '../src/index.ts'
|
||||
@@ -16,6 +16,11 @@ const NAME = 'dsh-test-bin'
|
||||
|
||||
const NOOP_PLUGIN = 'export const name = "noop"\nexport function apply() {}\n'
|
||||
|
||||
const tempRoots: string[] = []
|
||||
afterAll(() => {
|
||||
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
interface TreeFixture {
|
||||
ctx: Context
|
||||
dir: string
|
||||
@@ -24,6 +29,7 @@ interface TreeFixture {
|
||||
|
||||
async function bootTree(configBody: string, files: Record<string, string> = {}): Promise<TreeFixture> {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-config-reload-'))
|
||||
tempRoots.push(dir)
|
||||
writeFileSync(join(dir, 'noop.mjs'), NOOP_PLUGIN)
|
||||
for (const [name, content] of Object.entries(files)) writeFileSync(join(dir, name), content)
|
||||
writeFileSync(join(dir, 'cordis.yml'), configBody)
|
||||
@@ -282,6 +288,7 @@ describe('loader tree replacement', () => {
|
||||
describe('include refresh with overlay patches', () => {
|
||||
it('re-applies entry patches and inserted entries on every re-read (parity with initial load)', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-config-reload-overlay-'))
|
||||
tempRoots.push(dir)
|
||||
writeFileSync(join(dir, 'noop.mjs'), NOOP_PLUGIN)
|
||||
writeFileSync(join(dir, 'base.yml'), '- id: noop\n name: ./noop.mjs\n config:\n value: base\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), [
|
||||
@@ -347,6 +354,7 @@ describe('include patches layered over one base', () => {
|
||||
// must therefore be able to reach a row an earlier layer inserted, or
|
||||
// bundle-only rows would be invisible to the user's patch layer.
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-config-layered-'))
|
||||
tempRoots.push(dir)
|
||||
writeFileSync(join(dir, 'noop.mjs'), NOOP_PLUGIN)
|
||||
writeFileSync(join(dir, 'base.yml'), '- id: shared\n name: ./noop.mjs\n config:\n value: base\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), [
|
||||
|
||||
@@ -7,7 +7,10 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import Hmr from '@deepseek-ai/cordis-plugin-hmr'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import Timer from '@deepseek-ai/cordis-plugin-timer'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/** Every per-test tree root, removed once the booted watcher has been disposed. */
|
||||
const hmrRoots: string[] = []
|
||||
|
||||
async function bootHmr(dir: string, root: string[] = [], usePolling?: boolean): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
@@ -32,6 +35,10 @@ async function eventually(test: () => boolean, message: string): Promise<void> {
|
||||
}
|
||||
|
||||
describe('HMR exact config paths', () => {
|
||||
afterEach(() => {
|
||||
for (const root of hmrRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('observes module changes when its watch base is a filesystem alias', { timeout: 30_000 }, async () => {
|
||||
const target = mkdtempSync(join(tmpdir(), 'dsh-hmr-module-canonical-'))
|
||||
const alias = `${target}-alias`
|
||||
@@ -85,6 +92,7 @@ describe('HMR exact config paths', () => {
|
||||
|
||||
it('observes add, change, and unlink outside its module roots', { timeout: 20_000 }, async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-hmr-config-'))
|
||||
hmrRoots.push(dir)
|
||||
const filename = join(dir, 'plugins.yml')
|
||||
const ctx = await bootHmr(dir)
|
||||
const observed: string[] = []
|
||||
@@ -111,6 +119,7 @@ describe('HMR exact config paths', () => {
|
||||
|
||||
it('observes creation when the config parent did not exist at registration', { timeout: 20_000 }, async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-hmr-config-'))
|
||||
hmrRoots.push(root)
|
||||
const dir = join(root, 'later')
|
||||
const filename = join(dir, 'plugins.yml')
|
||||
const ctx = await bootHmr(root)
|
||||
@@ -129,6 +138,7 @@ describe('HMR exact config paths', () => {
|
||||
|
||||
it('serializes refreshes and waits for them during disposal', { timeout: 20_000 }, async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-hmr-config-'))
|
||||
hmrRoots.push(dir)
|
||||
const filename = join(dir, 'plugins.yml')
|
||||
writeFileSync(filename, 'one')
|
||||
const ctx = await bootHmr(dir)
|
||||
@@ -170,6 +180,7 @@ describe('HMR exact config paths', () => {
|
||||
|
||||
it('normalizes refresh failures and broadcasts them without escaping the watcher', { timeout: 20_000 }, async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-hmr-config-'))
|
||||
hmrRoots.push(dir)
|
||||
const filename = join(dir, 'plugins.yml')
|
||||
const ctx = await bootHmr(dir)
|
||||
const failure = Promise.withResolvers<{ filename: string; error: Error }>()
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
composeEntries,
|
||||
healProfilesModuleFallback,
|
||||
@@ -26,7 +26,16 @@ import {
|
||||
type Profile,
|
||||
} from '../src/index.ts'
|
||||
|
||||
const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-profile-'))
|
||||
const tempRoots: string[] = []
|
||||
afterAll(() => {
|
||||
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const tmp = (): string => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-profile-'))
|
||||
tempRoots.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
/** Stage a fake installed app: package.json with deps and a node_modules holding bundles. */
|
||||
function stageInstallation(
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* a real Loader tree, kept live through transactional HMR.
|
||||
*/
|
||||
|
||||
import { mkdirSync, mkdtempSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterAll, afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Hmr from '@deepseek-ai/cordis-plugin-hmr'
|
||||
import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
@@ -23,7 +23,16 @@ import {
|
||||
|
||||
const NAME = 'dsh-test-bin'
|
||||
|
||||
const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-user-patches-'))
|
||||
const tempRoots: string[] = []
|
||||
afterAll(() => {
|
||||
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const tmp = (): string => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-user-patches-'))
|
||||
tempRoots.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
async function eventually(test: () => boolean, message: string): Promise<void> {
|
||||
const deadline = Date.now() + 10_000
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* active, then resolves that row's config against its injection-ready context.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -34,6 +34,9 @@ interface Fixture {
|
||||
|
||||
const disposers: (() => Promise<void>)[] = []
|
||||
|
||||
/** Fixture tree roots, removed after their booted tree has been disposed. */
|
||||
const tempDirs: string[] = []
|
||||
|
||||
const readyApp: AppReady = {
|
||||
onReady(listener) {
|
||||
listener()
|
||||
@@ -59,6 +62,7 @@ function controlledAppReady(): { service: AppReady; commit(): void } {
|
||||
|
||||
afterEach(async () => {
|
||||
for (const dispose of disposers.splice(0)) await dispose()
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
internals.stdin = process.stdin
|
||||
internals.stdout = process.stdout
|
||||
internals.stderr = process.stderr
|
||||
@@ -103,6 +107,7 @@ async function bootFixture(
|
||||
options: { objectInject?: boolean; withoutProvider?: boolean } = {},
|
||||
): Promise<Fixture> {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-cmdline-'))
|
||||
tempDirs.push(dir)
|
||||
const observed: Observed = { exits: [], out: '' }
|
||||
writeFileSync(join(dir, 'reader.mjs'), `
|
||||
export const name = 'reader'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* the consumer pending.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
@@ -24,8 +24,12 @@ interface Observed {
|
||||
|
||||
const disposers: (() => Promise<void>)[] = []
|
||||
|
||||
/** Fixture tree roots, removed after their booted tree has been disposed. */
|
||||
const tempDirs: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
for (const dispose of disposers.splice(0)) await dispose()
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
internals.stdout = process.stdout
|
||||
internals.stderr = process.stderr
|
||||
})
|
||||
@@ -37,6 +41,7 @@ afterEach(async () => {
|
||||
*/
|
||||
async function bootStartup(args: string[]): Promise<{ task: HeadlessStartupValues | undefined; observed: Observed }> {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-headless-startup-'))
|
||||
tempDirs.push(dir)
|
||||
const observed: Observed = { exits: [], out: '' }
|
||||
writeFileSync(join(dir, 'row.mjs'), 'export function apply(_ctx, config) { globalThis.__headlessStartupObserved.runnerConfig = config }\n')
|
||||
// Loader imports through Node's resolver, so this fixture delegates to the
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* releases a consumer whose config reads `ctx.webStartup` directly.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
@@ -23,8 +23,12 @@ interface Observed {
|
||||
|
||||
const disposers: (() => Promise<void>)[] = []
|
||||
|
||||
/** Fixture tree roots, removed after their booted tree has been disposed. */
|
||||
const tempDirs: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
for (const dispose of disposers.splice(0)) await dispose()
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
internals.stdout = process.stdout
|
||||
internals.stderr = process.stderr
|
||||
})
|
||||
@@ -39,6 +43,7 @@ async function bootProvider(args: string[]): Promise<{
|
||||
observed: Observed
|
||||
}> {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-web-startup-'))
|
||||
tempDirs.push(dir)
|
||||
const observed: Observed = { exits: [], out: '' }
|
||||
writeFileSync(join(dir, 'reader.mjs'), `
|
||||
export function apply(_ctx, config) { globalThis.__webStartupObserved.readerConfig = config }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdtemp } from 'node:fs/promises'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment'
|
||||
import { DeepSeekFileStore, MAX_CHAT_IMAGE_BYTES } from '../src/file-store.ts'
|
||||
@@ -31,6 +31,12 @@ const CONNECTION = { baseURL: 'https://api.deepseek.com', apiKey: 'key' }
|
||||
const POLICY = { expiresAfterSeconds: 604_800, refreshMarginSeconds: 3_600, quotaCleanupBatch: 100 }
|
||||
const NOW = 1_700_000_000_000
|
||||
|
||||
/** Every temp store root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function requestUrl(input: string | URL | Request): string {
|
||||
if (typeof input === 'string') return input
|
||||
return input instanceof URL ? input.href : input.url
|
||||
@@ -64,6 +70,7 @@ function uploadFetch(now: () => number = () => NOW) {
|
||||
describe('DeepSeekFileStore', () => {
|
||||
it('singleflights the first upload and reuses the durable mapping across store instances', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
const remote = uploadFetch()
|
||||
const first = new DeepSeekFileStore({ index, now: () => NOW, fetch: remote.fetchImpl })
|
||||
@@ -84,6 +91,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('keeps a shared upload alive while another waiter remains', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
let complete: ((response: Response) => void) | undefined
|
||||
let uploadSignal: AbortSignal | undefined
|
||||
@@ -123,6 +131,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('aborts the shared upload after its only waiter cancels', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
let uploadSignal: AbortSignal | undefined
|
||||
const fetchImpl = vi.fn((_url: string | URL | Request, init?: RequestInit) => {
|
||||
@@ -149,6 +158,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('normalizes a non-Error cancellation reason', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const fetchImpl = vi.fn((_url: string | URL | Request, init?: RequestInit) => (
|
||||
new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () => {
|
||||
@@ -176,6 +186,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('starts a fresh upload while the cancelled transport is settling', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
let requests = 0
|
||||
const fetchImpl = vi.fn((_url: string | URL | Request, init?: RequestInit) => {
|
||||
requests += 1
|
||||
@@ -222,6 +233,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('does not persist an upload whose response is missing and retries on the next request', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
const good = uploadFetch()
|
||||
let first = true
|
||||
@@ -242,6 +254,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('rejects an upload response whose byte count differs from the request version', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const fetchImpl = vi.fn(() => Promise.resolve(new Response(JSON.stringify({
|
||||
id: 'file-api-wrong-size', object: 'file', bytes: 2, created_at: NOW / 1_000,
|
||||
filename: 'dsh-wrong.png', purpose: 'user_data',
|
||||
@@ -262,6 +275,7 @@ describe('DeepSeekFileStore', () => {
|
||||
['image/gif', 'gif'],
|
||||
] as const)('uses the %s filename extension for uploads', async (mediaType, extension) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const remote = uploadFetch()
|
||||
const store = new DeepSeekFileStore({
|
||||
index: new DeepSeekUploadIndex(join(dir, `${extension}.json`)),
|
||||
@@ -279,6 +293,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('normalizes a non-Error failure from the durable upload index', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
vi.spyOn(index, 'get').mockRejectedValue('index unavailable')
|
||||
const store = new DeepSeekFileStore({ index, now: () => NOW, fetch: vi.fn() as typeof fetch })
|
||||
@@ -291,6 +306,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('reuses local expires_at above the refresh margin and uploads again at the margin', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
let now = NOW
|
||||
const remote = uploadFetch(() => now)
|
||||
@@ -311,6 +327,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('releases an indexed file through DELETE and removes only that mapping', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
const remote = uploadFetch()
|
||||
const store = new DeepSeekFileStore({ index, now: () => NOW, fetch: remote.fetchImpl })
|
||||
@@ -323,6 +340,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('removes a losing upload and keeps the winning durable mapping when duplicate cleanup fails', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
vi.spyOn(index, 'commit').mockResolvedValue({
|
||||
accepted: false,
|
||||
@@ -352,6 +370,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('reclaims one owned file after quota rejection and retries the upload once', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
let uploads = 0
|
||||
const fetchImpl = vi.fn((input: string | URL | Request, init?: RequestInit) => {
|
||||
if (init?.method === 'POST') {
|
||||
@@ -394,6 +413,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('preserves a quota error when no harness-owned file can be reclaimed', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
|
||||
if (init?.method === 'POST') return Promise.resolve(new Response(JSON.stringify({
|
||||
error: { message: 'file count quota exceeded', code: 'file_quota' },
|
||||
@@ -418,6 +438,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('finishes pagination before deleting cursor files during quota recovery', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const deleted = new Set<string>()
|
||||
const fetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const target = new URL(requestUrl(input))
|
||||
@@ -456,6 +477,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('stops pagination when a page omits or repeats its cursor', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
for (const mode of ['missing', 'repeated'] as const) {
|
||||
let page = 0
|
||||
const fetchImpl = vi.fn((input: string | URL | Request, init?: RequestInit) => {
|
||||
@@ -482,6 +504,7 @@ describe('DeepSeekFileStore', () => {
|
||||
|
||||
it('releases every batch and clears the scoped upload index', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
const store = new DeepSeekFileStore({ index, now: () => NOW, fetch: vi.fn() as typeof fetch })
|
||||
const reclaim = vi.spyOn(store, 'reclaimOldestOwned')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment'
|
||||
import { DeepSeekFileId } from '../src/file-id.ts'
|
||||
import { deepSeekFileScope, DeepSeekUploadIndex } from '../src/upload-index.ts'
|
||||
@@ -9,6 +9,12 @@ import { deepSeekFileScope, DeepSeekUploadIndex } from '../src/upload-index.ts'
|
||||
const ATTACHMENT = AttachmentId(`sha256:${'a'.repeat(64)}`)
|
||||
const VARIANT = ImageVariantId(`sha256:${'b'.repeat(64)}`)
|
||||
|
||||
/** Every temp index root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('DeepSeekUploadIndex', () => {
|
||||
it('normalizes trailing endpoint slashes in the credential scope', () => {
|
||||
expect(deepSeekFileScope('https://api.deepseek.com///', 'key'))
|
||||
@@ -17,6 +23,7 @@ describe('DeepSeekUploadIndex', () => {
|
||||
|
||||
it('isolates API-key namespaces and reuses only records above the refresh margin', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
const first = deepSeekFileScope('https://api.deepseek.com', 'first-key')
|
||||
const second = deepSeekFileScope('https://api.deepseek.com', 'second-key')
|
||||
@@ -38,6 +45,7 @@ describe('DeepSeekUploadIndex', () => {
|
||||
|
||||
it('keeps a reusable cross-process winner and removes only an exact generation', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
const scope = deepSeekFileScope('https://api.deepseek.com', 'key')
|
||||
const first = {
|
||||
@@ -56,6 +64,7 @@ describe('DeepSeekUploadIndex', () => {
|
||||
|
||||
it('treats a corrupt upload cache as empty and repairs it on the next commit', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-'))
|
||||
roots.push(dir)
|
||||
const path = join(dir, 'index.json')
|
||||
await writeFile(path, '{bad', 'utf8')
|
||||
const index = new DeepSeekUploadIndex(path)
|
||||
@@ -128,6 +137,7 @@ describe('DeepSeekUploadIndex', () => {
|
||||
})}]}`,
|
||||
])('treats an invalid persisted index as empty %#', async (text) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-'))
|
||||
roots.push(dir)
|
||||
const path = join(dir, 'index.json')
|
||||
await writeFile(path, text, 'utf8')
|
||||
const index = new DeepSeekUploadIndex(path)
|
||||
@@ -138,6 +148,7 @@ describe('DeepSeekUploadIndex', () => {
|
||||
|
||||
it('rejects duplicate persisted mappings as a corrupt cache', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-'))
|
||||
roots.push(dir)
|
||||
const path = join(dir, 'index.json')
|
||||
const scope = deepSeekFileScope('https://api.deepseek.com', 'key')
|
||||
const record = {
|
||||
@@ -151,6 +162,7 @@ describe('DeepSeekUploadIndex', () => {
|
||||
|
||||
it('drops expired records on commit and clears only the selected namespace', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-'))
|
||||
roots.push(dir)
|
||||
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
||||
const first = deepSeekFileScope('https://api.deepseek.com', 'first')
|
||||
const second = deepSeekFileScope('https://api.deepseek.com', 'second')
|
||||
@@ -171,6 +183,7 @@ describe('DeepSeekUploadIndex', () => {
|
||||
|
||||
it('propagates non-cache filesystem read failures', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-'))
|
||||
roots.push(dir)
|
||||
const path = join(dir, 'directory')
|
||||
await mkdir(path)
|
||||
const index = new DeepSeekUploadIndex(path)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* stays read-only.
|
||||
*/
|
||||
|
||||
import { chmod, mkdtemp, mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { chmod, mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
@@ -15,7 +15,7 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import Include from '@deepseek-ai/cordis-plugin-include'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import AgentPresets, {
|
||||
COMPOSITION_FILE, copyComposition, METADATA_FILE, type Config,
|
||||
} from '@deepseek-ai/dsh-agent-presets'
|
||||
@@ -23,6 +23,12 @@ import AgentPresets, {
|
||||
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
|
||||
const VALID = '- id: tool-alpha\n name: ../../plugins/contribute.js\n config:\n tool: alpha\n'
|
||||
|
||||
/** Every temp root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
let ctx: Context
|
||||
let userRoot: string
|
||||
|
||||
@@ -49,6 +55,7 @@ async function seedPreset(
|
||||
|
||||
beforeEach(async () => {
|
||||
userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-authoring-'))
|
||||
roots.push(userRoot)
|
||||
ctx = new Context()
|
||||
ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
|
||||
await ctx.plugin(Loader)
|
||||
@@ -201,6 +208,7 @@ describe('deleting a preset', () => {
|
||||
describe('a deployment with more than one user root', () => {
|
||||
it('refuses to delete a preset the writable root does not own', async () => {
|
||||
const second = await mkdtemp(join(tmpdir(), 'dsh-preset-second-'))
|
||||
roots.push(second)
|
||||
await seedPreset(second, 'elsewhere')
|
||||
const layered = new Context()
|
||||
layered.baseUrl = pathToFileURL(FIXTURES).href + '/'
|
||||
@@ -246,7 +254,9 @@ describe('a deployment with no writable root', () => {
|
||||
|
||||
describe('a user root that does not exist yet', () => {
|
||||
it('is created by the first copy', async () => {
|
||||
const absent = join(await mkdtemp(join(tmpdir(), 'dsh-preset-absent-')), 'nested', 'preset')
|
||||
const absentRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-absent-'))
|
||||
roots.push(absentRoot)
|
||||
const absent = join(absentRoot, 'nested', 'preset')
|
||||
const fresh = new Context()
|
||||
fresh.baseUrl = pathToFileURL(FIXTURES).href + '/'
|
||||
await fresh.plugin(Loader)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* read reported broken by reason instead of dropped.
|
||||
*/
|
||||
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
@@ -33,6 +33,9 @@ const VALID = '- id: prompt\n name: \'@deepseek-ai/dsh-system-prompt\'\n'
|
||||
|
||||
const contexts: Context[] = []
|
||||
|
||||
/** Every temp root created by this file, removed after its contexts settle. */
|
||||
const roots: string[] = []
|
||||
|
||||
/** A Loader-context evaluator over an empty scope, enough for literal gates. */
|
||||
const evaluateExpression = (expression: string): unknown => evaluate({}, expression)
|
||||
/** An evaluator that refuses every expression, leaving rows conditional. */
|
||||
@@ -41,6 +44,7 @@ const refuseExpression = (): never => { throw new Error('no loader context') }
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks()
|
||||
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function harness(roster: Config): Promise<Context> {
|
||||
@@ -63,6 +67,7 @@ async function harness(roster: Config): Promise<Context> {
|
||||
describe('fileComposition', () => {
|
||||
it('flattens groups and keeps refused expressions conditional', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-composition-'))
|
||||
roots.push(dir)
|
||||
const path = join(dir, COMPOSITION_FILE)
|
||||
await writeFile(path, [
|
||||
'- id: alpha',
|
||||
@@ -124,6 +129,7 @@ describe('fileComposition', () => {
|
||||
|
||||
it('evaluates decidable gates the way a mount would', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-composition-'))
|
||||
roots.push(dir)
|
||||
const path = join(dir, COMPOSITION_FILE)
|
||||
await writeFile(path, [
|
||||
'- id: off',
|
||||
@@ -144,6 +150,7 @@ describe('fileComposition', () => {
|
||||
|
||||
it('answers broken for a file that stopped reading as a composition', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-composition-'))
|
||||
roots.push(dir)
|
||||
|
||||
const missing = await fileComposition(join(dir, COMPOSITION_FILE), refuseExpression)
|
||||
expect(missing).toHaveProperty('broken')
|
||||
@@ -201,6 +208,7 @@ describe('mountedCompositionRows', () => {
|
||||
describe('AgentPresets.compositionInventory', () => {
|
||||
it('reads unmounted presets from their files, marking the default and metadata', async () => {
|
||||
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-composition-roster-'))
|
||||
roots.push(userRoot)
|
||||
await mkdir(join(userRoot, 'documented'))
|
||||
await writeFile(join(userRoot, 'documented', COMPOSITION_FILE), [
|
||||
VALID.trimEnd(),
|
||||
@@ -287,6 +295,7 @@ describe('AgentPresets.compositionInventory', () => {
|
||||
|
||||
it('prefers the standing mount over a file that broke after mounting', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-composition-volatile-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'volatile'))
|
||||
const plugin = join(FIXTURES, 'plugins', 'contribute.js')
|
||||
await writeFile(
|
||||
@@ -344,6 +353,7 @@ describe('AgentPresets.compositionInventory', () => {
|
||||
|
||||
it('keeps a broken preset on the inventory with its discovery reason', async () => {
|
||||
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-composition-roster-'))
|
||||
roots.push(userRoot)
|
||||
await mkdir(join(userRoot, 'damaged'))
|
||||
const ctx = await harness({
|
||||
default: 'minimal',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mkdtemp, mkdir, symlink, writeFile } from 'node:fs/promises'
|
||||
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { COMPOSITION_FILE, discoverPresets, scanRoot } from '@deepseek-ai/dsh-agent-presets'
|
||||
|
||||
const fsHarness = vi.hoisted(() => ({
|
||||
@@ -31,6 +31,12 @@ const HARNESS = new URL('.', import.meta.url).href
|
||||
const SYSTEM = { path: join(FIXTURES, 'system'), trust: 'system' as const }
|
||||
const USER = { path: join(FIXTURES, 'user'), trust: 'user' as const }
|
||||
|
||||
/** Every temp root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
fsHarness.nextReadError = undefined
|
||||
})
|
||||
@@ -38,6 +44,7 @@ beforeEach(() => {
|
||||
describe('display order', () => {
|
||||
it('puts declared order first, then everything else by id', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-order-'))
|
||||
roots.push(root)
|
||||
for (const [id, order] of [['zulu', 1], ['alpha', 2]] as const) {
|
||||
await mkdir(join(root, id), { recursive: true })
|
||||
await writeFile(join(root, id, COMPOSITION_FILE), '[]\n')
|
||||
@@ -57,6 +64,7 @@ describe('display order', () => {
|
||||
|
||||
it('breaks a tie between equal declared orders by id', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-order-tie-'))
|
||||
roots.push(root)
|
||||
for (const id of ['yankee', 'alpha']) {
|
||||
await mkdir(join(root, id), { recursive: true })
|
||||
await writeFile(join(root, id, COMPOSITION_FILE), '[]\n')
|
||||
@@ -94,6 +102,7 @@ describe('preset discovery', () => {
|
||||
|
||||
it('skips a directory whose name no preset id could ever claim', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-presets-oddname-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, '.hidden'))
|
||||
await mkdir(join(root, 'Has_Caps'))
|
||||
await mkdir(join(root, 'usable'))
|
||||
@@ -129,6 +138,7 @@ describe('preset discovery', () => {
|
||||
|
||||
it('ignores a plain file sitting beside the preset directories', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-presets-'))
|
||||
roots.push(root)
|
||||
await writeFile(join(root, 'stray.yml'), '- id: x\n')
|
||||
await mkdir(join(root, 'real'))
|
||||
await writeFile(join(root, 'real', COMPOSITION_FILE), '[]\n')
|
||||
@@ -140,6 +150,7 @@ describe('preset discovery', () => {
|
||||
|
||||
it('reports a root it cannot read rather than treating it as empty', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-presets-'))
|
||||
roots.push(root)
|
||||
const notADirectory = join(root, 'file-as-root')
|
||||
await writeFile(notADirectory, 'not a directory\n')
|
||||
|
||||
@@ -168,6 +179,7 @@ describe('composition health', () => {
|
||||
*/
|
||||
async function scanned(composition: string): Promise<string | undefined> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-presets-health-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'probe'))
|
||||
await writeFile(join(root, 'probe', COMPOSITION_FILE), composition)
|
||||
const [preset] = await scanRoot({ path: root, trust: 'user' }, HARNESS)
|
||||
@@ -208,6 +220,7 @@ describe('composition health', () => {
|
||||
|
||||
it('reports a composition that stats but cannot be read', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-presets-unreadable-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'sealed'))
|
||||
const path = join(root, 'sealed', COMPOSITION_FILE)
|
||||
await writeFile(path, '[]\n')
|
||||
@@ -235,6 +248,7 @@ describe('rows naming a plugin that cannot be resolved', () => {
|
||||
/** One directory under a fresh root holding `composition`, scanned. */
|
||||
async function scanned(composition: string): Promise<string | undefined> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-presets-resolve-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'probe'))
|
||||
await writeFile(join(root, 'probe', COMPOSITION_FILE), composition)
|
||||
const [preset] = await scanRoot({ path: root, trust: 'user' }, HARNESS)
|
||||
@@ -272,6 +286,7 @@ describe('rows naming a plugin that cannot be resolved', () => {
|
||||
|
||||
it('resolves a preset-relative row against the preset\'s own directory', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-presets-relative-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'probe'))
|
||||
await writeFile(join(root, 'probe', 'own-plugin.mjs'), 'export function apply() {}\n')
|
||||
await writeFile(join(root, 'probe', COMPOSITION_FILE), '- id: own\n name: ./own-plugin.mjs\n- id: gone\n name: ./deleted.mjs\n')
|
||||
@@ -311,6 +326,7 @@ describe('rows naming a plugin that cannot be resolved', () => {
|
||||
// The fast path, and the one that has to answer alone: this package has a
|
||||
// directory and nothing to import, so a resolver would reject it.
|
||||
const home = await mkdtemp(join(tmpdir(), 'dsh-presets-installed-'))
|
||||
roots.push(home)
|
||||
await mkdir(join(home, 'node_modules', '@scope', 'pkg'), { recursive: true })
|
||||
await writeFile(join(home, 'node_modules', '@scope', 'pkg', 'package.json'), '{"name":"@scope/pkg"}\n')
|
||||
await mkdir(join(home, 'presets', 'probe'), { recursive: true })
|
||||
@@ -326,6 +342,7 @@ describe('rows naming a plugin that cannot be resolved', () => {
|
||||
// What a stale profile install leaves behind: the name is still in
|
||||
// `node_modules`, pointing at a checkout that is gone.
|
||||
const home = await mkdtemp(join(tmpdir(), 'dsh-presets-dangling-'))
|
||||
roots.push(home)
|
||||
await mkdir(join(home, 'node_modules', '@scope'), { recursive: true })
|
||||
await symlink(join(home, 'deleted-checkout'), join(home, 'node_modules', '@scope', 'pkg'))
|
||||
await mkdir(join(home, 'presets', 'probe'), { recursive: true })
|
||||
|
||||
@@ -6,15 +6,22 @@
|
||||
* from the file a user can write.
|
||||
*/
|
||||
|
||||
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { METADATA_FILE, readPresetMetadata, renderPresetMetadata } from '../src/metadata.ts'
|
||||
|
||||
/** Every temp preset directory created by this file, removed after each test. */
|
||||
const tempDirs: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const dir of tempDirs.splice(0)) await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/** A preset directory holding exactly the given metadata text. */
|
||||
async function presetDir(content?: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-preset-meta-'))
|
||||
tempDirs.push(dir)
|
||||
await mkdir(dir, { recursive: true })
|
||||
if (content !== undefined) await writeFile(join(dir, METADATA_FILE), content)
|
||||
return dir
|
||||
|
||||
@@ -13,7 +13,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import AgentRegistry, { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AgentPresets, {
|
||||
COMPOSITION_FILE, leakedServices, livePresetMounts, mountPreset, serviceForAgent,
|
||||
} from '@deepseek-ai/dsh-agent-presets'
|
||||
@@ -86,6 +86,13 @@ function rootResolves(ctx: Context, name: string): boolean {
|
||||
}
|
||||
|
||||
let ctx: Context
|
||||
|
||||
/** Every temp preset root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx = await harness()
|
||||
})
|
||||
@@ -93,6 +100,7 @@ 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-'))
|
||||
roots.push(root)
|
||||
const presetDir = join(root, 'absolute')
|
||||
const plugin = join(FIXTURES, 'plugins', 'contribute.js')
|
||||
await mkdir(presetDir)
|
||||
@@ -360,6 +368,7 @@ describe('composing from a broken preset', () => {
|
||||
/** A roster whose only user preset carries `composition`. */
|
||||
async function rosterWith(composition: string): Promise<Context> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-broken-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'damaged'))
|
||||
await writeFile(join(root, 'damaged', COMPOSITION_FILE), composition)
|
||||
return await harness({ default: 'damaged', roots: [{ path: root, trust: 'user' as const }], includeShippedRoot: false, includeUserRoot: false })
|
||||
@@ -428,6 +437,7 @@ describe('the preset file is an input, never a persistence target', () => {
|
||||
// committed fixture would be mutated by the very run that proves the bug
|
||||
// and every later run would compare against the damaged file and pass.
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-write-'))
|
||||
roots.push(root)
|
||||
const dir = join(root, 'self-disposing')
|
||||
await mkdir(dir)
|
||||
const path = join(dir, COMPOSITION_FILE)
|
||||
@@ -625,6 +635,7 @@ describe('replacing a composition', () => {
|
||||
// A preset root this test owns, so removing the composition mid-flight
|
||||
// cannot disturb the shipped fixtures.
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-restore-'))
|
||||
roots.push(root)
|
||||
const seeded: [string, string][] = [['first', `- id: only\n name: ${join(FIXTURES, 'plugins', 'contribute.js')}\n config:\n tool: only\n`], ['broken', `- id: nope\n name: ${join(FIXTURES, 'plugins', 'throws.js')}\n config:\n message: refuses\n`]]
|
||||
for (const [id, body] of seeded) {
|
||||
await mkdir(join(root, id))
|
||||
@@ -679,6 +690,7 @@ describe('editing a composition file', () => {
|
||||
*/
|
||||
async function editable(id: string): Promise<{ scoped: Context; path: string }> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-edit-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, id))
|
||||
const path = join(root, id, COMPOSITION_FILE)
|
||||
await writeFile(path, rowFor('before'))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* which is the only one of the three that mutates an agent.
|
||||
*/
|
||||
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
@@ -34,7 +34,13 @@ const ROOTS = [
|
||||
// temp preset directory these tests seed would report the composition broken.
|
||||
const VALID = '- id: prompt\n name: \'@deepseek-ai/dsh-system-prompt\'\n'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
/** Every temp preset root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks()
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function remoteFailure(operation: Promise<unknown>): Promise<RemoteFailure> {
|
||||
try {
|
||||
@@ -97,6 +103,7 @@ const recordedPreset = (agent: Agent): unknown =>
|
||||
describe('the roster a client reads', () => {
|
||||
it('projects path-free rows, marking the default and carrying published metadata', async () => {
|
||||
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
|
||||
roots.push(userRoot)
|
||||
await mkdir(join(userRoot, 'documented'), { recursive: true })
|
||||
await writeFile(join(userRoot, 'documented', COMPOSITION_FILE), VALID)
|
||||
await writeFile(join(userRoot, 'documented', METADATA_FILE), 'name: 我的模式\ndescription: 只做检索。\n')
|
||||
@@ -122,6 +129,7 @@ describe('the roster a client reads', () => {
|
||||
|
||||
it('keeps a broken preset on the roster with its reason', async () => {
|
||||
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
|
||||
roots.push(userRoot)
|
||||
await mkdir(join(userRoot, 'damaged'), { recursive: true })
|
||||
const ctx = await harness({
|
||||
default: 'standard',
|
||||
@@ -174,6 +182,7 @@ describe('reading one composition', () => {
|
||||
|
||||
it('carries the display metadata a preset published', async () => {
|
||||
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
|
||||
roots.push(userRoot)
|
||||
await mkdir(join(userRoot, 'documented'), { recursive: true })
|
||||
await writeFile(join(userRoot, 'documented', COMPOSITION_FILE), VALID)
|
||||
await writeFile(join(userRoot, 'documented', METADATA_FILE), 'name: 我的模式\ndescription: 只做检索。\n')
|
||||
@@ -241,6 +250,7 @@ describe('authoring over Remote', () => {
|
||||
|
||||
it('copies and deletes through the Remote adapters', async () => {
|
||||
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
|
||||
roots.push(userRoot)
|
||||
const ctx = await harness({
|
||||
default: 'standard',
|
||||
roots: [{ path: join(FIXTURES, 'system'), trust: 'system' }, { path: userRoot, trust: 'user' }],
|
||||
@@ -432,6 +442,7 @@ describe('switching one session\'s composition', () => {
|
||||
|
||||
it('reports an unusable composition with its discovery reason', async () => {
|
||||
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
|
||||
roots.push(userRoot)
|
||||
await mkdir(join(userRoot, 'damaged'), { recursive: true })
|
||||
const ctx = await harness({
|
||||
default: 'standard',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* so a person can change which preset new sessions get without a restart.
|
||||
*/
|
||||
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
@@ -19,13 +19,19 @@ import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import AgentPresets, { COMPOSITION_FILE, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
|
||||
|
||||
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
|
||||
const ROOTS = [{ path: join(FIXTURES, 'system'), trust: 'system' as const }]
|
||||
const NS = SETTINGS_NAMESPACE
|
||||
|
||||
/** Every temp root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/**
|
||||
* A composition with a real file-backed settings provider. `settingsFiber` is
|
||||
* the provider's own handle, so a test can take it away the way a reload does.
|
||||
@@ -34,6 +40,7 @@ async function harness(
|
||||
extraRoots: readonly { path: string; trust: 'system' | 'user' }[] = [],
|
||||
): Promise<{ ctx: Context; settingsFile: string; settingsFiber: { dispose: () => unknown } }> {
|
||||
const home = await mkdtemp(join(tmpdir(), 'dsh-preset-settings-'))
|
||||
roots.push(home)
|
||||
const settingsFile = join(home, 'settings.yaml')
|
||||
await writeFile(settingsFile, '{}\n')
|
||||
|
||||
@@ -119,6 +126,7 @@ describe('the default preset as a user setting', () => {
|
||||
|
||||
it('clears a user default it has just deleted', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-authored-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'mine'))
|
||||
await writeFile(
|
||||
join(root, 'mine', COMPOSITION_FILE),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* suite: the derived writable root is resolved in the constructor.
|
||||
*/
|
||||
|
||||
import { mkdtemp, readFile } from 'node:fs/promises'
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
@@ -24,16 +24,19 @@ import AgentPresets, { SHIPPED_PRESET_ROOT, type Config } from '@deepseek-ai/dsh
|
||||
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
|
||||
const SYSTEM_ROOT = join(FIXTURES, 'system')
|
||||
|
||||
let home: string
|
||||
let previousHome: string | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
previousHome = process.env.DSH_HOME
|
||||
process.env.DSH_HOME = await mkdtemp(join(tmpdir(), 'dsh-shipped-root-'))
|
||||
home = await mkdtemp(join(tmpdir(), 'dsh-shipped-root-'))
|
||||
process.env.DSH_HOME = home
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
if (previousHome === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previousHome
|
||||
await rm(home, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/** Boot a roster with the shipped root left to the plugin's default. */
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* temporary home, or it would reach the developer's real one.
|
||||
*/
|
||||
|
||||
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
@@ -31,15 +31,20 @@ const VALID = '- id: tool-alpha\n name: ../../plugins/contribute.js\n config:\
|
||||
let home: string
|
||||
let previousHome: string | undefined
|
||||
|
||||
/** Extra per-test roots, removed with the home directory. */
|
||||
const explicitRoots: string[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
home = await mkdtemp(join(tmpdir(), 'dsh-preset-home-'))
|
||||
previousHome = process.env.DSH_HOME
|
||||
process.env.DSH_HOME = home
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
if (previousHome === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previousHome
|
||||
await rm(home, { recursive: true, force: true })
|
||||
for (const root of explicitRoots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/** Boot a roster over the fixture system root, with the derived root left to the plugin. */
|
||||
@@ -120,6 +125,7 @@ describe('the harness-home preset root', () => {
|
||||
|
||||
it('yields to a configured user root for authoring, which writableRoot takes first', async () => {
|
||||
const explicit = await mkdtemp(join(tmpdir(), 'dsh-preset-explicit-'))
|
||||
explicitRoots.push(explicit)
|
||||
const ctx = await roster({
|
||||
roots: [
|
||||
{ path: SYSTEM_ROOT, trust: 'system' as const },
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
* are all exercised through the real `confine()` path.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { LAUNCHER_FAILURE_EXIT } from '@deepseek-ai/node-addon-landlock-run'
|
||||
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||
@@ -24,6 +24,12 @@ import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from '../s
|
||||
const RO: SandboxPolicy = { mode: 'read-only', workspaceRoot: '/ws' }
|
||||
const WW: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }
|
||||
|
||||
/** Every temp dir created by this file (fake launchers and runner entries), removed after each test. */
|
||||
const tempDirs: string[] = []
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function setup(config: Config = {}, internals: LocalSandboxProvider['internals'] = {}) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalSandboxProvider, config)
|
||||
@@ -39,12 +45,15 @@ async function setup(config: Config = {}, internals: LocalSandboxProvider['inter
|
||||
* `sandbox-windows-acl/lib/runner.js`.
|
||||
*/
|
||||
function absentRunnerEntry(): string {
|
||||
return join(mkdtempSync(join(tmpdir(), 'dsh-absent-acl-entry-')), 'runner.js')
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-absent-acl-entry-'))
|
||||
tempDirs.push(dir)
|
||||
return join(dir, 'runner.js')
|
||||
}
|
||||
|
||||
/** Write an executable fake `landlock-run` that answers `--probe` with `report`. */
|
||||
function fakeLauncher(report = 'landlock: fully enforced'): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
|
||||
tempDirs.push(dir)
|
||||
const launcher = join(dir, 'landlock-run')
|
||||
writeFileSync(launcher, `#!/bin/sh\nif [ "$1" = "--probe" ]; then echo "${report}"; exit 0; fi\nexit ${LAUNCHER_FAILURE_EXIT}\n`, { mode: 0o755 })
|
||||
return launcher
|
||||
@@ -53,6 +62,7 @@ function fakeLauncher(report = 'landlock: fully enforced'): string {
|
||||
/** Write an executable fake `sandbox-exec` that exits `status` for any invocation. */
|
||||
function fakeSeatbeltExec(status: number): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-seatbelt-'))
|
||||
tempDirs.push(dir)
|
||||
const exec = join(dir, 'sandbox-exec')
|
||||
writeFileSync(exec, `#!/bin/sh\nexit ${status}\n`, { mode: 0o755 })
|
||||
return exec
|
||||
@@ -316,6 +326,7 @@ describe('the default landlock probe (launcher CLI contract)', () => {
|
||||
|
||||
it('reads a failing launcher as unusable: the chain ends and fails closed', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
|
||||
tempDirs.push(dir)
|
||||
const launcher = join(dir, 'landlock-run')
|
||||
writeFileSync(launcher, `#!/bin/sh\nexit ${LAUNCHER_FAILURE_EXIT}\n`, { mode: 0o755 })
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
|
||||
@@ -337,6 +348,7 @@ describe('probeTimeoutMs config', () => {
|
||||
// spawnSync blocks the worker and fork/exec latency inflates wall-clock)
|
||||
// cannot flip either verdict; the vitest timeout clears the patient budget.
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-slow-landlock-'))
|
||||
tempDirs.push(dir)
|
||||
const launcher = join(dir, 'landlock-run')
|
||||
writeFileSync(launcher, '#!/bin/sh\nsleep 1\necho "landlock: fully enforced"\nexit 0\n', { mode: 0o755 })
|
||||
|
||||
@@ -440,6 +452,7 @@ describe('the windows-acl probe (runner invocation contract)', () => {
|
||||
|
||||
it('prefers the built lib/runner.js entry when the resolved file exists', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-acl-entry-'))
|
||||
tempDirs.push(dir)
|
||||
const builtEntry = join(dir, 'runner.js')
|
||||
writeFileSync(builtEntry, '')
|
||||
const { sandbox } = await setup({}, {
|
||||
|
||||
@@ -4,16 +4,22 @@
|
||||
* deriving from `writableRoots` — cannot drift.
|
||||
*/
|
||||
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { mkdtempSync, realpathSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
||||
|
||||
/** Every temp root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('canonicalPath', () => {
|
||||
it('resolves symlinks (an existing path realpaths)', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-roots-'))
|
||||
roots.push(dir)
|
||||
expect(canonicalPath(dir)).toBe(realpathSync.native(dir))
|
||||
})
|
||||
|
||||
@@ -29,11 +35,12 @@ describe('writableRoots', () => {
|
||||
|
||||
it('workspace-write grants the workspace root plus the platform temp areas, canonical and deduplicated', () => {
|
||||
const ws = mkdtempSync(join(tmpdir(), 'dsh-ws-'))
|
||||
const roots = writableRoots({ mode: 'workspace-write', workspaceRoot: ws })
|
||||
expect(roots).toContain(realpathSync.native(ws))
|
||||
expect(roots).toContain(canonicalPath('/tmp'))
|
||||
expect(roots).toContain(realpathSync.native(tmpdir()))
|
||||
roots.push(ws)
|
||||
const writable = writableRoots({ mode: 'workspace-write', workspaceRoot: ws })
|
||||
expect(writable).toContain(realpathSync.native(ws))
|
||||
expect(writable).toContain(canonicalPath('/tmp'))
|
||||
expect(writable).toContain(realpathSync.native(tmpdir()))
|
||||
// Deduplicated after canonicalization (/tmp and os.tmpdir() may coincide).
|
||||
expect(new Set(roots).size).toBe(roots.length)
|
||||
expect(new Set(writable).size).toBe(writable.length)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
@@ -10,6 +10,10 @@ import type { ShellProcess } from '@deepseek-ai/dsh-shell'
|
||||
|
||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-bash-exec-spec-'))
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(spillDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function setup(config: ConstructorParameters<typeof LocalBashExecutor>[1] = {}) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
* writes CRLF on Windows, so exact text assertions normalize line endings.
|
||||
*/
|
||||
|
||||
import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterAll, afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { PwshLocalExecutor, ENCODING_PREAMBLE, candidatePwshPaths, resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
@@ -24,6 +24,16 @@ import type { ShellProcess } from '@deepseek-ai/dsh-shell'
|
||||
|
||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-exec-spec-'))
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(spillDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/** Per-test temp dirs, removed after each test. */
|
||||
const tempDirs: string[] = []
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
// The probe follows the executor's own resolution (Program Files installs on
|
||||
// Windows are found even when bare `pwsh` is not on PATH).
|
||||
const hasPwsh = spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0
|
||||
@@ -114,6 +124,7 @@ describe('resolvePwshPath and candidatePwshPaths (pure, every platform)', () =>
|
||||
|
||||
it('returns the first EXISTING win32 candidate, else pwsh', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-resolve-'))
|
||||
tempDirs.push(dir)
|
||||
const store = join(dir, 'store')
|
||||
mkdirSync(store, { recursive: true })
|
||||
writeFileSync(join(store, 'pwsh.exe'), '')
|
||||
@@ -131,6 +142,7 @@ describe('resolvePwshPath and candidatePwshPaths (pure, every platform)', () =>
|
||||
// Store app execution aliases stat as EACCES but lstat as a link; a
|
||||
// dangling symlink reproduces that split on every platform.
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-resolve-link-'))
|
||||
tempDirs.push(dir)
|
||||
const store = join(dir, 'store')
|
||||
mkdirSync(store, { recursive: true })
|
||||
const link = join(store, 'pwsh.exe')
|
||||
@@ -141,6 +153,7 @@ describe('resolvePwshPath and candidatePwshPaths (pure, every platform)', () =>
|
||||
|
||||
it('skips a directory candidate and falls through to the PATH-resolution default', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-resolve-dir-'))
|
||||
tempDirs.push(dir)
|
||||
const store = join(dir, 'store')
|
||||
mkdirSync(join(store, 'pwsh.exe'), { recursive: true })
|
||||
expect(resolvePwshPath(undefined, {
|
||||
@@ -201,6 +214,7 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.run', () => {
|
||||
it('uses config cwd, overridable per call', async () => {
|
||||
const first = mkdtempSync(join(tmpdir(), 'dsh-pwsh-cwd-a-'))
|
||||
const second = mkdtempSync(join(tmpdir(), 'dsh-pwsh-cwd-b-'))
|
||||
tempDirs.push(first, second)
|
||||
const { bash } = await setup({ cwd: first })
|
||||
const fromConfig = await bash.run(bash.resolve({ command: '(Get-Location).Path' }))
|
||||
expect(samePath(fromConfig.stdout.text.trim(), first)).toBe(true)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Stats } from 'node:fs'
|
||||
import { mkdir, realpath, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import SkillRegistry from '@deepseek-ai/dsh-skill'
|
||||
|
||||
@@ -91,8 +91,16 @@ vi.mock('chokidar', () => ({
|
||||
|
||||
const SkillFileSystem = await import('../src/index.ts')
|
||||
|
||||
/** Every temp dir created by this file, removed after each test. */
|
||||
const tempDirs: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const dir of tempDirs.splice(0)) await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function tempDir(name: string): Promise<string> {
|
||||
return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
|
||||
const dir = await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
async function writeSkill(root: string, name: string): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { mkdir, readdir, readFile, rename, rm, stat, symlink, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
@@ -7,8 +7,16 @@ import SkillRegistry from '@deepseek-ai/dsh-skill'
|
||||
import { FileSystem, FsError, FsVersion, type FsDirEntry, type FsEditOutcome, type FsEditRequest, type FsInfo, type FsPathInfo, type FsTarget, type FsWriteOutcome } from '@deepseek-ai/dsh-fs'
|
||||
import * as SkillFileSystem from '../src/index.ts'
|
||||
|
||||
/** Every temp dir created by this file, removed after each test. */
|
||||
const tempDirs: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const dir of tempDirs.splice(0)) await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function tempDir(name: string): Promise<string> {
|
||||
return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
|
||||
const dir = await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
async function writeSkill(root: string, name: string, description: string, body = 'Use the skill.'): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
@@ -15,8 +15,16 @@ import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
/** Every temp dir created by this file, removed after each test. */
|
||||
const tempDirs: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const dir of tempDirs.splice(0)) await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function tempDir(name: string): Promise<string> {
|
||||
return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
|
||||
const dir = await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
async function writeSkill(root: string, name: string, description: string, body: string): Promise<void> {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from 'node:crypto'
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { mkdir, open } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
@@ -38,6 +38,13 @@ export function privateRoot(): string {
|
||||
return defaultRoot
|
||||
}
|
||||
|
||||
// The spill root is private to this process, so it is removed when the process
|
||||
// ends normally; a process killed with SIGKILL cannot run this, and its
|
||||
// residue is left to OS temp hygiene.
|
||||
process.once('exit', () => {
|
||||
if (defaultRoot !== undefined) rmSync(defaultRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
// Spill keeps its empty-name policy local so storage backends stay decoupled.
|
||||
/* jscpd:ignore-start */
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
|
||||
import type { Readable } from 'node:stream'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { closeSync, mkdtempSync, openSync, unlinkSync, writeSync } from 'node:fs'
|
||||
import { closeSync, mkdtempSync, openSync, rmSync, unlinkSync, writeSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { setTimeout as sleepMs } from 'node:timers/promises'
|
||||
@@ -91,6 +91,13 @@ function privateSpillDir(): string {
|
||||
return defaultSpillDir
|
||||
}
|
||||
|
||||
// The spill directory is private to this process, so it is removed when the
|
||||
// process ends normally; a process killed with SIGKILL cannot run this, and
|
||||
// its residue is left to OS temp hygiene.
|
||||
process.once('exit', () => {
|
||||
if (defaultSpillDir !== undefined) rmSync(defaultSpillDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/**
|
||||
* Collects one stream with a bounded in-memory tail. With a spill cap, on
|
||||
* first overflow a spill file is created and every chunk (including those
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdtempSync, readFileSync, statSync, unlinkSync } from 'node:fs'
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync, unlinkSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
childEnv,
|
||||
killGroup,
|
||||
@@ -82,6 +82,10 @@ vi.mock('node:fs', async (importOriginal) => {
|
||||
|
||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-subprocess-spec-'))
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(spillDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
type SpecOverrides = Partial<Parameters<typeof spawnSubprocess>[0]> & {
|
||||
stdoutMaxBytes?: number
|
||||
stderrMaxBytes?: number
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { access, mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'
|
||||
import { access, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
|
||||
const passed: CoverageCommandResult = { exitCode: 0, signalCode: null }
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
/** Every temporary root created by this file, removed after each test. */
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks()
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function writeBlob(command: CoverageCommand): Promise<void> {
|
||||
if (command.blobPath === undefined) return
|
||||
@@ -31,7 +36,9 @@ async function writeBlob(command: CoverageCommand): Promise<void> {
|
||||
}
|
||||
|
||||
async function temporaryRoot(): Promise<string> {
|
||||
return await mkdtemp(join(tmpdir(), 'dsh-coverage-partitions-'))
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-coverage-partitions-'))
|
||||
roots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
/** Write a Vitest results cache under a temporary root. */
|
||||
|
||||
Reference in new issue
Block a user