// Minimal DSH-like context for integration tests: real @deepseek-ai/dsh-fs-local backend, // the fs-observation-policy rules, a scoped tool registry, and fake agents. import os from 'node:os' import path from 'node:path' import fs from 'node:fs' const DSH = '/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai' export async function makeHarness() { const { default: LocalFileSystem } = await import(`${DSH}/dsh-fs-local/lib/index.js`) const { FsError } = await import(`${DSH}/dsh-fs/lib/index.js`) const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'omp-edit-')) const listeners = new Map() const on = (name, fn, opts) => { const list = listeners.get(name) ?? [] if (opts?.prepend) list.unshift(fn); else list.push(fn) listeners.set(name, list) return () => listeners.set(name, (listeners.get(name) ?? []).filter((f) => f !== fn)) } const fsCtx = { config: { cwd: dir }, effect() {}, on() {}, get() {}, provide() {}, emit() {} } // Construct without a Cordis context: borrow the prototype and the instance fields. const fsImpl = Object.create(LocalFileSystem.prototype) Object.assign(fsImpl, { config: { cwd: dir, diffBasisMaxBytes: 10 * 1024 * 1024 }, internals: {}, locks: new Map() }) void fsCtx // observation policy (copy of DSH's rules) const observed = new WeakMap() const own = (actor) => actor?.agent?.session on('fs/write-intent', async (target, actor) => { const p = own(actor) && observed.get(own(actor))?.get(target.targetKey) return p?.kind === 'present' ? { kind: 'replaceIfVersion', version: p.version } : { kind: 'createIfAbsent' } }) on('fs/edit-intent', async (target, actor) => { const o = own(actor); const p = o && observed.get(o)?.get(target.targetKey) if (!o || p === undefined) throw new FsError(`edit requires reading "${target.displayPath}" first`, 'FS_NOT_OBSERVED') if (p.kind === 'absent') throw new FsError(`cannot edit "${target.displayPath}": not found`, 'FS_NOT_FOUND') return { version: p.version } }) on('fs/observed', (target, obs, actor) => { const o = own(actor); if (!o) return if (!observed.has(o)) observed.set(o, new Map()) observed.get(o).set(target.targetKey, obs) }) const global = new Map() const scoped = new WeakMap() const makeTools = (scope) => ({ register(def) { const m = scope ? (scoped.get(scope) ?? scoped.set(scope, new Map()).get(scope)) : global if (m.has(def.name)) throw new Error(`duplicate tool ${def.name}`) m.set(def.name, def) return () => m.delete(def.name) }, get(name, s) { return (s && scoped.get(s)?.get(name)) ?? global.get(name) }, }) const agents = [] const sp = { section: () => () => {}, getSectionOrder: () => 1300 } const ctx = { fs: fsImpl, tools: makeTools(undefined), agents: { list: () => agents }, systemPrompt: sp, on, emit(name, ...args) { for (const f of listeners.get(name) ?? []) f(...args) }, async waterfall(name, ...args) { const inner = args.pop() const cbs = [...(listeners.get(name) ?? [])] const next = () => (cbs.shift() ?? inner)(...args, next) return next() }, effect(fn) { const d = fn(); return d }, get() { return undefined }, } function newAgent(model = 'anthropic/claude-sonnet') { const agent = { options: { model }, session: { header: { cwd: dir } } } agent.ctx = { tools: makeTools(agent), on, systemPrompt: sp } agents.push(agent) for (const f of listeners.get('agent/created') ?? []) f({ agent }) return agent } const call = async (agent, name, args) => { const t = ctx.tools.get(name, agent) if (!t) throw new Error(`no tool ${name}`) const exec = { agent, name, arguments: args, signal: new AbortController().signal } const value = await t.execute(args, exec) const content = t.output.render(args, value) const meta = t.output.presentationMeta?.(args, value) return { value, text: content.map((c) => c.text).join('\n'), meta } } const write = (rel, text) => fs.writeFileSync(path.join(dir, rel), text) const readF = (rel) => fs.readFileSync(path.join(dir, rel), 'utf8') return { ctx, dir, newAgent, call, write, read: readF, listeners, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) } } /** Fake built-in read/grep with DSH's output shapes. */ export function registerBuiltins(h) { h.ctx.tools.register({ name: 'read', description: 'Read a UTF-8 text file and return line-numbered content.', parameters: { type: 'object', properties: { file_path: { type: 'string' } }, required: ['file_path'] }, output: { schema: { type: 'object', additionalProperties: false, properties: { path: { type: 'string' }, offset: { type: 'integer' }, lines: { type: 'array' }, totalLines: { type: 'integer' } } }, render: (_a, v) => [{ type: 'text', text: `${v.path}\nfile\n\n${v.lines.map((l) => `${l.number}: ${l.text}`).join('\n')}\n\n(End of file - total ${v.totalLines} lines)\n` }], }, async execute(args, exec) { const t = await h.ctx.fs.resolve(args.file_path, { cwd: h.dir }) const info = await h.ctx.fs.stat(t) const text = await h.ctx.fs.readText(t) const lines = text.replace(/\n$/, '').split('\n').map((x, i) => ({ number: i + 1, text: x })) h.ctx.emit('fs/observed', t, { kind: 'present', version: info.version }, exec) return { path: t.displayPath, offset: 1, lines, totalLines: lines.length } }, }) h.ctx.tools.register({ name: 'grep', description: 'Search file contents.', parameters: { type: 'object', properties: { pattern: { type: 'string' } }, required: ['pattern'] }, output: { schema: { type: 'object', additionalProperties: false, properties: { matches: { type: 'array' } } }, render: (_a, v) => [{ type: 'text', text: v.matches.map((m) => `${m.path}:${m.lineNumber}: ${m.line}`).join('\n') || 'No matches' }], }, async execute(args) { const out = [] for (const f of fs.readdirSync(h.dir)) { const p = path.join(h.dir, f) if (!fs.statSync(p).isFile()) continue fs.readFileSync(p, 'utf8').split('\n').forEach((l, i) => { if (new RegExp(args.pattern).test(l)) out.push({ path: f, lineNumber: i + 1, line: l }) }) } return { matches: out } }, }) }