mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-20 00:13:57 +08:00
Fix preset publication, volatile settings and client form binding
This commit is contained in:
parent
fe0fe70d33
commit
0b9a3a82bc
66 changed files with 8546 additions and 3690 deletions
248
tests/volatile-startup.test.mjs
Normal file
248
tests/volatile-startup.test.mjs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import test, { after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
import { mkdtempSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import * as host from '../lib/index.js'
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import { parsePresetIds, capabilityGroupsFromSettings, kbEnabledForPlane } from '../src/netx/capability-groups.ts'
|
||||
|
||||
// Exercise the current official Schema implementation, not merely the installed
|
||||
// lib: this local Harness has newer source alongside an older scalar-only lib.
|
||||
const peer = createRequire(import.meta.resolve('@deepseek-ai/schemastery'))
|
||||
const cosmokitSource = peer.resolve('@deepseek-ai/cosmokit/src/index.ts')
|
||||
const schemaSource = peer.resolve('@deepseek-ai/schemastery/src/index.ts')
|
||||
const built = await Bun.build({ entrypoints: [schemaSource], target: 'bun', format: 'esm', plugins: [{
|
||||
name: 'official-volatile-source',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^@deepseek-ai\/cosmokit$/ }, () => ({ path: cosmokitSource }))
|
||||
},
|
||||
}] })
|
||||
assert.equal(built.success, true, String(built.logs))
|
||||
const schemaUrl = `data:text/javascript;base64,${Buffer.from(await built.outputs[0].text()).toString('base64')}`
|
||||
// Load the shipped JS with the current official schema peer. Keep it beside
|
||||
// lib/index.js so skill and package discovery use the same paths as production.
|
||||
const hostFile = fileURLToPath(new URL('../lib/index.js', import.meta.url))
|
||||
const currentHostFile = fileURLToPath(new URL(`../lib/.volatile-startup-${randomUUID()}.mjs`, import.meta.url))
|
||||
const hostCode = readFileSync(hostFile, 'utf8')
|
||||
const withCurrentSchema = hostCode.replace(/from ["']@deepseek-ai\/schemastery["']/g, `from '${schemaUrl}'`)
|
||||
assert.notEqual(withCurrentSchema, hostCode, 'must replace the external schema peer')
|
||||
writeFileSync(currentHostFile, withCurrentSchema)
|
||||
after(() => unlinkSync(currentHostFile))
|
||||
const currentHost = await import(pathToFileURL(currentHostFile).href)
|
||||
const currentConfig = currentHost.Config
|
||||
const { createVolatile, updateVolatile, isVolatile } = await import(pathToFileURL(peer.resolve('@deepseek-ai/cosmokit/src/volatile.ts')).href)
|
||||
const store = () => globalThis[Symbol.for('dsh-netxops.connection-store')].connection
|
||||
const pause = () => new Promise(resolve => setTimeout(resolve, 40))
|
||||
|
||||
function context(t, settings = { describe: () => [] }) {
|
||||
const previous = process.env.DSH_HOME
|
||||
process.env.DSH_HOME = mkdtempSync(join(tmpdir(), 'netxops-volatile-'))
|
||||
const tools = new Map(), skills = new Map(), listeners = new Map(), prompts = new Map(), effects = [], injected = [], errors = []
|
||||
const register = map => entry => {
|
||||
assert.equal(map.has(entry.name), false, `duplicate ${entry.name}`)
|
||||
map.set(entry.name, entry)
|
||||
return () => map.delete(entry.name)
|
||||
}
|
||||
const ctx = {
|
||||
logger: { info() {}, warn() {}, error(...args) { errors.push(args) } },
|
||||
tools: { register: register(tools) }, skills: { register: register(skills) }, settings,
|
||||
credentials: { async resolve() { return { value: '' } } },
|
||||
systemPrompt: {
|
||||
section(spec) { prompts.set(spec.name, spec); return () => prompts.delete(spec.name) },
|
||||
context(spec) { prompts.set(spec.name, spec); return () => prompts.delete(spec.name) },
|
||||
},
|
||||
get(name) { return this[name] },
|
||||
inject(deps, callback) {
|
||||
injected.push([...deps])
|
||||
if (deps.every(name => this[name])) callback(this)
|
||||
},
|
||||
effect(fn) { const dispose = fn(); if (typeof dispose === 'function') effects.push(dispose); return dispose },
|
||||
on(event, fn) {
|
||||
let list = listeners.get(event)
|
||||
if (!list) listeners.set(event, list = new Set())
|
||||
list.add(fn)
|
||||
return () => list.delete(fn)
|
||||
},
|
||||
emit(event, ...args) { for (const fn of listeners.get(event) ?? []) fn(...args) },
|
||||
}
|
||||
t.after(async () => {
|
||||
for (const dispose of effects.reverse()) dispose()
|
||||
listeners.clear()
|
||||
await pause()
|
||||
if (previous === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previous
|
||||
})
|
||||
return { ctx, tools, skills, prompts, injected, errors }
|
||||
}
|
||||
|
||||
test('current Schema wraps defaults and partial profile starts the shipped Host bundle', async t => {
|
||||
// Existing profiles lack the new target-list fields. They arrive as references
|
||||
// containing empty defaults, which used to throw value.split on activation.
|
||||
const config = currentConfig({
|
||||
installAgentPreset: false, alarmPushEnabled: false, apiUrl: 'http://10.0.0.1:8890/',
|
||||
lang: 'en', toolCallTimeoutMs: 45000, tokenCredentialRef: 'CUSTOM_TOKEN',
|
||||
groupOpsInPreset: false, groupOpsPublic: true, groupKbInPreset: false,
|
||||
thinkingLanguage: 'en', replyLanguage: 'en',
|
||||
})
|
||||
assert.ok(isVolatile(config.groupOpsPresets))
|
||||
assert.equal(config.groupOpsPresets.get(), '')
|
||||
assert.ok(isVolatile(config.installAgentPreset))
|
||||
const mounted = context(t)
|
||||
assert.doesNotThrow(() => currentHost.apply(mounted.ctx, config))
|
||||
assert.equal(store().apiUrl, 'http://10.0.0.1:8890')
|
||||
assert.equal(store().lang, 'en')
|
||||
assert.equal(store().toolCallTimeoutMs, 45000)
|
||||
assert.equal(store().groups.ops.inPreset, false)
|
||||
assert.equal(store().groups.ops.public, true)
|
||||
assert.equal(store().groupKbInPreset, false)
|
||||
assert.equal(mounted.injected.some(deps => deps.length === 1 && deps[0] === 'agentPresets'), false,
|
||||
'false reference must not run the installer')
|
||||
assert.equal(mounted.tools.size, 15)
|
||||
await pause()
|
||||
assert.ok(mounted.skills.has('netx-ops'))
|
||||
assert.equal(globalThis[Symbol.for('dsh-netxops.alarm-push-status')].status.enabled, false)
|
||||
assert.equal(globalThis[Symbol.for('dsh-netxops.alarm-push-status')].status.phase, 'disabled')
|
||||
assert.equal(mounted.errors.length, 0)
|
||||
})
|
||||
|
||||
test('Loader updates read retained references and immediately revoke and republish tools and skills', async t => {
|
||||
const config = currentConfig({ installAgentPreset: false, alarmPushEnabled: false,
|
||||
groupOpsPublic: true, groupOpsPresets: 'standard,review', groupKbPresets: 'review' })
|
||||
const mounted = context(t)
|
||||
currentHost.apply(mounted.ctx, config)
|
||||
await pause()
|
||||
assert.equal(mounted.tools.size, 15)
|
||||
assert.ok(mounted.skills.has('netx-ops'))
|
||||
assert.deepEqual(store().groups.ops.presets, ['standard', 'review'])
|
||||
assert.equal(store().groupKbPresets, 'review')
|
||||
const original = config.toolsEnabled
|
||||
updateVolatile(original, createVolatile(false))
|
||||
updateVolatile(config.groupOpsPresets, createVolatile(''))
|
||||
mounted.ctx.emit('loader/volatile-update', [['toolsEnabled'], ['groupOpsPresets']])
|
||||
assert.equal(config.toolsEnabled, original, 'Loader retains identity')
|
||||
assert.equal(store().toolsEnabled, false)
|
||||
assert.equal(mounted.tools.size, 0, 'revocation is synchronous')
|
||||
await pause()
|
||||
assert.equal(mounted.skills.size, 0)
|
||||
assert.deepEqual(store().groups.ops.presets ?? [], [])
|
||||
updateVolatile(original, createVolatile(true))
|
||||
updateVolatile(config.groupOpsPublic, createVolatile(false))
|
||||
updateVolatile(config.groupOpsPresets, createVolatile('standard'))
|
||||
updateVolatile(config.apiUrl, createVolatile('http://10.0.0.2:8890'))
|
||||
mounted.ctx.emit('loader/volatile-update', [['toolsEnabled'], ['groupOpsPublic'], ['groupOpsPresets'], ['apiUrl']])
|
||||
assert.equal(store().toolsEnabled, true)
|
||||
assert.equal(store().apiUrl, 'http://10.0.0.2:8890')
|
||||
assert.equal(store().groups.ops.public, false)
|
||||
assert.deepEqual(store().groups.ops.presets, ['standard'])
|
||||
await pause()
|
||||
assert.equal(mounted.tools.size, 0)
|
||||
assert.equal(mounted.skills.size, 0)
|
||||
assert.equal(mounted.errors.length, 0)
|
||||
})
|
||||
|
||||
test('legacy scalar settings remain valid and a reference-returning settings scope is read on every change', async t => {
|
||||
let current = host.Config({ installAgentPreset: false, alarmPushEnabled: false, groupOpsPublic: true })
|
||||
let changed
|
||||
const mounted = context(t, {
|
||||
installSection(ctx, ns, schema, base, hooks) {
|
||||
assert.equal(typeof base.installAgentPreset, 'boolean')
|
||||
hooks.setSource(() => current)
|
||||
changed = hooks.onChange
|
||||
changed()
|
||||
},
|
||||
})
|
||||
host.apply(mounted.ctx, current)
|
||||
assert.equal(mounted.tools.size, 15)
|
||||
current = currentConfig({ installAgentPreset: false, alarmPushEnabled: false, toolsEnabled: false, groupOpsPublic: true })
|
||||
changed()
|
||||
assert.equal(store().toolsEnabled, false)
|
||||
assert.equal(mounted.tools.size, 0)
|
||||
await pause()
|
||||
assert.equal(mounted.skills.size, 0)
|
||||
assert.equal(mounted.errors.length, 0)
|
||||
})
|
||||
|
||||
test('policy helpers read official references and malformed preset lists cannot crash or select a target', () => {
|
||||
const config = currentConfig({ groupOpsInPreset: false, groupOpsPublic: true,
|
||||
groupOpsPresets: 'standard,review standard', groupKbPublic: false, groupKbInPreset: false })
|
||||
const groups = capabilityGroupsFromSettings(config)
|
||||
assert.equal(groups.ops.inPreset, false)
|
||||
assert.equal(groups.ops.public, true)
|
||||
assert.deepEqual(groups.ops.presets, ['standard', 'review'])
|
||||
assert.equal(kbEnabledForPlane(config, 'preset'), false)
|
||||
assert.equal(kbEnabledForPlane(config, 'host'), false)
|
||||
assert.equal(host.resolveTokenCredentialRef(createVolatile('CUSTOM_TOKEN')), 'CUSTOM_TOKEN')
|
||||
const merged = host.mergeConfigSkippingNullish(config, {
|
||||
groupOpsPublic: createVolatile(false), apiUrl: createVolatile(null), groupOpsPresets: createVolatile(''),
|
||||
})
|
||||
assert.equal(merged.groupOpsPublic, false)
|
||||
assert.equal(merged.groupOpsPresets, '')
|
||||
assert.equal(merged.apiUrl, 'http://127.0.0.1:8890')
|
||||
for (const value of [null, undefined, 0, true, {}, [], { get: () => 'standard' }]) {
|
||||
assert.deepEqual(parsePresetIds(value), [])
|
||||
}
|
||||
assert.deepEqual(parsePresetIds(createVolatile('standard,review standard')), ['standard', 'review'])
|
||||
})
|
||||
|
||||
test('real Cordis activates the current-schema bundle and delivers the owning fiber update', async t => {
|
||||
const previous = process.env.DSH_HOME
|
||||
process.env.DSH_HOME = mkdtempSync(join(tmpdir(), 'netxops-volatile-cordis-'))
|
||||
const root = new Context(), owners = [], tools = new Map(), skills = new Map()
|
||||
class Tools extends Service {
|
||||
constructor(ctx) { super(ctx, 'tools') }
|
||||
register(entry) {
|
||||
assert.equal(tools.has(entry.name), false)
|
||||
tools.set(entry.name, entry)
|
||||
return () => tools.delete(entry.name)
|
||||
}
|
||||
}
|
||||
class Skills extends Service {
|
||||
constructor(ctx) { super(ctx, 'skills') }
|
||||
register(entry) {
|
||||
assert.equal(skills.has(entry.name), false)
|
||||
skills.set(entry.name, entry)
|
||||
return () => skills.delete(entry.name)
|
||||
}
|
||||
}
|
||||
t.after(async () => {
|
||||
for (const owner of owners.reverse()) await owner.dispose()
|
||||
await pause()
|
||||
if (previous === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previous
|
||||
})
|
||||
owners.push(await root.plugin(Tools))
|
||||
owners.push(await root.plugin(Skills))
|
||||
const owner = await root.plugin({ ...currentHost, name: 'volatile-startup-integration' }, {
|
||||
installAgentPreset: false, alarmPushEnabled: false, groupOpsPublic: true,
|
||||
})
|
||||
owners.push(owner)
|
||||
// FiberState is a const enum and has no JS export in this installed Cordis.
|
||||
assert.equal(owner.state, 2 /* ACTIVE */)
|
||||
assert.ok(isVolatile(owner.config.groupOpsPresets))
|
||||
assert.equal(tools.size, 15)
|
||||
await pause()
|
||||
assert.ok(skills.has('netx-ops'))
|
||||
updateVolatile(owner.config.toolsEnabled, createVolatile(false))
|
||||
const self = Object.create(owner.ctx)
|
||||
self[Context.filter] = ctx => ctx.fiber === owner
|
||||
owner.ctx.emit(self, 'loader/volatile-update', [['toolsEnabled']])
|
||||
assert.equal(owner.state, 2 /* ACTIVE */, 'volatile edit keeps the active fiber')
|
||||
assert.equal(store().toolsEnabled, false)
|
||||
assert.equal(tools.size, 0)
|
||||
await pause()
|
||||
assert.equal(skills.size, 0)
|
||||
const unloadErrors = [], previousError = console.error
|
||||
try {
|
||||
console.error = (...args) => { unloadErrors.push(args) }
|
||||
await owner.dispose()
|
||||
owners.pop()
|
||||
await pause()
|
||||
} finally { console.error = previousError }
|
||||
assert.equal(tools.size, 0)
|
||||
assert.equal(skills.size, 0)
|
||||
assert.deepEqual(unloadErrors, [], 'unload must not remount on an inactive context')
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue