Files
omp-edit/test/harness.mjs
T

133 lines
6.2 KiB
JavaScript

// 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: `<path>${v.path}</path>\n<type>file</type>\n<content>\n${v.lines.map((l) => `${l.number}: ${l.text}`).join('\n')}\n\n(End of file - total ${v.totalLines} lines)\n</content>` }],
},
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 }
},
})
}