netxops/test/kb-local.test.mjs
oliver 3a731389f0 Add KB local write tools and align Netx Ops with standard preset.
Host-side netx__kb* tools jail create/update/delete under paths.local memories/drafts/suggestions; Netx Ops agent.cordis.yml now includes full standard capability rows plus ops tools.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 21:43:41 +08:00

165 lines
4.4 KiB
JavaScript

/**
* KB local path jail + create/update/delete ops.
*/
import assert from 'node:assert/strict'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import {
createDraft,
createMemory,
createSuggestion,
deleteLocalFile,
listLocalFiles,
updateLocalFile,
} from '../src/netx/kb-local-ops.ts'
import {
draftFileName,
resolveKbLocalRoot,
resolveWritableLocalPath,
sanitizeSlug,
} from '../src/netx/kb-local-path.ts'
import { applyKbEnv, resetKbContext } from '../src/netx/kb-runtime.ts'
function snap(root, localRel = '_local') {
return {
status: 'configured',
realRoot: root,
operatorName: 'IOH',
country: 'ID',
version: '1',
content: {
hasRegions: true,
hasTheory: false,
hasPacket: false,
hasCommon: false,
hasSkills: false,
},
paths: { local: localRel },
errorMessage: '',
}
}
function withTemp(run) {
const root = mkdtempSync(join(tmpdir(), 'netxops-kb-local-'))
try {
run(root)
} finally {
rmSync(root, { recursive: true, force: true })
}
}
test('sanitizeSlug strips separators', () => {
assert.equal(sanitizeSlug('../evil/x'), 'evil-x')
assert.equal(sanitizeSlug('你好 world'), '你好-world')
})
test('draftFileName forces DRAFT- prefix', () => {
const name = draftFileName({ date: '2026-09-15', slug: 'jakarta-down' })
assert.match(name, /^DRAFT-20260915-jakarta-down\.md$/)
})
test('resolveWritableLocalPath rejects identity', () => {
withTemp((root) => {
const local = join(root, '_local')
mkdirSync(local, { recursive: true })
assert.throws(
() => resolveWritableLocalPath(local, 'identity', '话术.md'),
/identity|allowed under/,
)
})
})
test('create / update / delete memory', () => {
withTemp((root) => {
const s = snap(root)
mkdirSync(join(root, '_local'), { recursive: true })
const created = createMemory(s, {
bucket: 'note',
slug: 'smoke',
body: '# hello\n',
date: '2026-09-15',
})
assert.equal(created.action, 'created')
assert.ok(created.relativePath.includes('日常笔记'))
assert.equal(readFileSync(created.absolutePath, 'utf8'), '# hello\n')
assert.throws(
() => createMemory(s, {
bucket: 'note',
slug: 'smoke',
body: 'x',
date: '2026-09-15',
}),
/already exists/,
)
const updated = updateLocalFile(s, {
path: created.relativePath,
body: '# updated\n',
})
assert.equal(updated.action, 'updated')
assert.equal(readFileSync(created.absolutePath, 'utf8'), '# updated\n')
const listed = listLocalFiles(s, { root: 'memories' })
assert.equal(listed.entries.length, 1)
const deleted = deleteLocalFile(s, { path: created.absolutePath })
assert.equal(deleted.action, 'deleted')
assert.equal(listLocalFiles(s).entries.length, 0)
})
})
test('draft injects status: draft frontmatter', () => {
withTemp((root) => {
const s = snap(root)
mkdirSync(join(root, '_local'), { recursive: true })
const created = createDraft(s, {
slug: 'case-a',
body: '# DRAFT\n\nbody\n',
domain: '02_路由',
date: '20260915',
})
const text = readFileSync(created.absolutePath, 'utf8')
assert.match(text, /^---\nstatus: draft\n---/)
assert.match(created.relativePath, /drafts\//)
})
})
test('suggestion + refuse identity path update', () => {
withTemp((root) => {
const s = snap(root)
const idDir = join(root, '_local', 'identity')
mkdirSync(idDir, { recursive: true })
writeFileSync(join(idDir, '话术与边界.md'), 'secret\n', 'utf8')
createSuggestion(s, {
kind: 'improvement',
slug: 'tool-x',
body: '# sug\n',
date: '2026-09-15',
})
assert.throws(
() => updateLocalFile(s, {
path: 'identity/话术与边界.md',
body: 'hacked\n',
}),
/identity|allowed under/,
)
assert.equal(readFileSync(join(idDir, '话术与边界.md'), 'utf8'), 'secret\n')
})
})
test('applyKbEnv sets KB_LOCAL', () => {
withTemp((root) => {
mkdirSync(join(root, '_local'), { recursive: true })
const s = snap(root)
assert.equal(resolveKbLocalRoot(s), join(root, '_local'))
applyKbEnv(s)
assert.equal(process.env.KB_LOCAL, join(root, '_local'))
resetKbContext()
assert.equal(process.env.KB_LOCAL, undefined)
})
})