Fix preset publication, volatile settings and client form binding

This commit is contained in:
oliver 2026-10-11 12:54:02 +08:00
parent fe0fe70d33
commit 0b9a3a82bc
66 changed files with 8546 additions and 3690 deletions

View file

@ -0,0 +1,19 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { apiUrlField } from '../src/client/card-form.ts'
test('apiUrlField accepts http(s) and normalizes trailing slash', () => {
const field = apiUrlField('apiUrl')
assert.deepEqual(field.parse('http://127.0.0.1:8890/'), {
kind: 'set',
value: 'http://127.0.0.1:8890',
})
assert.deepEqual(field.parse(''), { kind: 'clear' })
})
test('apiUrlField rejects unsafe URLs', () => {
const field = apiUrlField('apiUrl')
assert.equal(field.parse('file:///tmp'), undefined)
assert.equal(field.parse('http://user:pass@h/'), undefined)
assert.equal(field.parse('not-a-url'), undefined)
})

View file

@ -0,0 +1,112 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile, stat } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const read = (path) => readFile(resolve(root, path), 'utf8')
test('package declares Desktop 0.2 client contract', async () => {
const pkg = JSON.parse(await read('package.json'))
assert.equal(pkg.version, '0.1.69')
assert.equal(pkg.engines.node, '>=20')
assert.equal(pkg.dsh.client.immediately, false)
assert.deepEqual(pkg.dsh.client.inject, [
'@deepseek-ai/dsh-client-ui-slots',
'@deepseek-ai/dsh-client-locale',
])
assert.ok(!pkg.dsh.client.inject.includes('@deepseek-ai/dsh-client-runtime'))
assert.equal(pkg.exports['./cordis.patch.yml'], './cordis.bundle.patch.yml')
assert.ok(!pkg.files.includes('lib/agent-tools-shared.js'))
for (const name of [
'@deepseek-ai/cordis',
'@deepseek-ai/dsh-credentials',
'@deepseek-ai/dsh-settings',
'@deepseek-ai/dsh-tools',
'@deepseek-ai/schemastery',
]) {
assert.equal(pkg.peerDependencies[name], '*', `${name} should be open peer *`)
}
})
test('agent-tools entries are self-contained (no shared chunk)', async () => {
for (const name of [
'agent-tools.js',
'agent-tools-ops.js',
'agent-tools-nms.js',
'agent-tools-common.js',
'agent-tools-topology.js',
'agent-tools-biz-monitor.js',
]) {
const entry = await read(`lib/${name}`)
assert.doesNotMatch(entry, /agent-tools-shared/)
assert.match(entry, /applyGroupToolsPlugin|function apply/)
const info = await stat(resolve(root, `lib/${name}`))
assert.ok(info.size > 20_000, `${name} should be a full bundle, got ${info.size}`)
}
})
test('browser half uses deferred configForms and distinct section id', async () => {
const client = await read('lib/client.js')
assert.match(client, /name:\s*["']settings\.section["']/)
assert.match(client, /NETXOPS_SECTION_ID\s*=\s*["']dsh-netxops["']/)
assert.match(client, /NETXOPS_NS\s*=\s*["']netxops["']/)
assert.match(client, /id:\s*NETXOPS_SECTION_ID/)
assert.match(client, /configForms poll/)
assert.match(client, /createDeferredScope/)
assert.match(client, /tools\.status|TOOLS_STATUS_ENDPOINT|toolsStatus/)
assert.doesNotMatch(client, /inject\(\[["']configForms["']\]/)
assert.doesNotMatch(client, /inject\(\[["']settingsScope["']\]/)
assert.match(client, /name:\s*["']settings\.plugin\.item["']/)
assert.match(client, /key:\s*id/)
assert.doesNotMatch(client, /name:\s*["']plugins\.item["']/)
assert.doesNotMatch(client, /dsh-client-runtime/)
assert.doesNotMatch(client, /mcpServers|python -m netx_mcp/)
})
test('host Config marks scalar fields volatile for 0.2 projection', async () => {
const host = await read('lib/index.js')
assert.match(host, /function vol\(/)
assert.match(host, /\.volatile\s*\(/)
assert.match(host, /NETXOPS_SETTINGS_NAMESPACE\s*=\s*["']netxops["']/)
assert.match(host, /document-updated/)
assert.match(host, /normalizeApiUrl/)
assert.match(host, /mergeHostConfigForHeal/)
assert.match(host, /writePatchIfUnchanged/)
assert.match(host, /groupOpsPublic:\s*vol\(z\.boolean\(\)\.default\(false\)\)/)
assert.doesNotMatch(host, /inject\(\[["']settingsScope["']\]/)
})
test('client validates apiUrl and defaults ops public off', async () => {
const client = await read('lib/client.js')
assert.match(client, /apiUrlField\s*\(/)
assert.match(client, /apiUrlInvalid/)
assert.match(client, /groupOpsPublic:\s*false/)
assert.match(client, /booleanFieldPersistFalse\s*\(\s*["']groupOpsPublic["']\s*\)/)
})
test('tools plugin subscribes before first remount (connection race)', async () => {
const tools = await read('lib/agent-tools.js')
const watchIdx = tools.indexOf('watchNetxConnection')
const remountIdx = tools.indexOf('remountTools()')
assert.ok(watchIdx >= 0 && remountIdx >= 0)
assert.ok(
watchIdx < remountIdx,
'watchNetxConnection must be registered before the first remountTools()',
)
assert.match(tools, /setInterval/)
assert.match(tools, /requireNetxConnection|provisionalNetxConnection/)
})
test('host hard-injects tools; soft credentials + provisional publish', async () => {
const host = await read('lib/index.js')
assert.match(host, /inject\s*=\s*\[\s*["']tools["']\s*\]/)
assert.match(host, /provisional connection published/)
assert.match(host, /plane:\s*["']host["']/)
assert.match(host, /tools\.status/)
assert.match(host, /netxops-mount-status\.json/)
assert.match(host, /additionalProperties:\s*!0|additionalProperties:\s*true/)
assert.match(host, /inject\(\[["']credentials["']\]/)
assert.doesNotMatch(host, /export const inject = \[["']credentials["']\]/)
})

View file

@ -0,0 +1,61 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
getNetxConnection,
publishNetxConnection,
watchNetxConnection,
} from '../src/netx/runtime.ts'
import { DEFAULT_CAPABILITY_GROUPS } from '../src/netx/capability-groups.ts'
function clearConnection() {
publishNetxConnection({
apiUrl: '',
token: '',
lang: 'zh',
toolCallTimeoutMs: 1000,
groups: DEFAULT_CAPABILITY_GROUPS,
groupKbInPreset: false,
groupKbPublic: false,
})
// runtime has no reset — overwrite with a sentinel then ignore
}
test('watch-then-remount does not miss an in-flight publish', () => {
let mounts = 0
const remount = () => {
if (getNetxConnection()?.apiUrl) mounts += 1
}
// Bad order (pre-0.1.58): remount → publish → watch → miss
mounts = 0
remount()
publishNetxConnection({
apiUrl: 'http://127.0.0.1:8890',
token: 't',
lang: 'zh',
toolCallTimeoutMs: 1000,
groups: DEFAULT_CAPABILITY_GROUPS,
groupKbInPreset: true,
groupKbPublic: false,
})
const stopBad = watchNetxConnection(remount)
assert.equal(mounts, 0, 'bad order misses the publish')
stopBad()
// Good order: watch → publish → remount (and watch also fires)
mounts = 0
clearConnection()
const stopGood = watchNetxConnection(remount)
publishNetxConnection({
apiUrl: 'http://127.0.0.1:8890',
token: 't',
lang: 'zh',
toolCallTimeoutMs: 1000,
groups: DEFAULT_CAPABILITY_GROUPS,
groupKbInPreset: true,
groupKbPublic: false,
})
remount()
assert.ok(mounts >= 1, 'good order sees connection')
stopGood()
})

56
tests/heal-merge.test.mjs Normal file
View file

@ -0,0 +1,56 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
mergeHostConfigForHeal,
NETXOPS_HOST_CONFIG_DEFAULTS,
writePatchIfUnchanged,
} from '../src/netx/heal-profile-entry.ts'
test('mergeHostConfigForHeal fills missing defaults only', () => {
const merged = mergeHostConfigForHeal({
apiUrl: 'http://custom:8890',
groupOpsPublic: false,
})
assert.equal(merged.apiUrl, 'http://custom:8890')
assert.equal(merged.groupOpsPublic, false)
assert.equal(merged.lang, NETXOPS_HOST_CONFIG_DEFAULTS.lang)
assert.equal(merged.tokenCredentialRef, NETXOPS_HOST_CONFIG_DEFAULTS.tokenCredentialRef)
assert.equal(merged.groupBizMonitorInPreset, NETXOPS_HOST_CONFIG_DEFAULTS.groupBizMonitorInPreset)
})
test('mergeHostConfigForHeal preserves unknown user keys', () => {
const merged = mergeHostConfigForHeal({
apiUrl: 'http://127.0.0.1:8890',
customSiteTag: 'hq-edge-1',
})
assert.equal(merged.customSiteTag, 'hq-edge-1')
assert.equal(merged.apiUrl, 'http://127.0.0.1:8890')
})
test('mergeHostConfigForHeal never overwrites empty-string user values with defaults', () => {
const merged = mergeHostConfigForHeal({
kbRoot: '',
imBotId: '',
})
assert.equal(merged.kbRoot, '')
assert.equal(merged.imBotId, '')
})
test('NETXOPS_HOST_CONFIG_DEFAULTS keeps ops public off', () => {
assert.equal(NETXOPS_HOST_CONFIG_DEFAULTS.groupOpsPublic, false)
})
test('writePatchIfUnchanged aborts when file raced', () => {
const dir = mkdtempSync(join(tmpdir(), 'netxops-heal-'))
const path = join(dir, 'cordis.patch.yml')
writeFileSync(path, 'v1\n', 'utf8')
assert.equal(writePatchIfUnchanged(path, 'v1\n', 'v2\n'), true)
assert.equal(readFileSync(path, 'utf8'), 'v2\n')
// Caller still holds stale expectedRaw after another writer changed the file.
writeFileSync(path, 'other\n', 'utf8')
assert.equal(writePatchIfUnchanged(path, 'v2\n', 'v3\n'), false)
assert.equal(readFileSync(path, 'utf8'), 'other\n')
})

41
tests/http-url.test.mjs Normal file
View file

@ -0,0 +1,41 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { normalizeApiUrl } from '../src/netx/http.ts'
import { alarmSubscribeUrl } from '../src/netx/alarm-push.ts'
test('normalizeApiUrl accepts http(s) roots and strips trailing slash', () => {
assert.deepEqual(normalizeApiUrl('http://127.0.0.1:8890/'), {
ok: true,
url: 'http://127.0.0.1:8890',
})
assert.deepEqual(normalizeApiUrl('https://netx.example.com/api/'), {
ok: true,
url: 'https://netx.example.com/api',
})
assert.deepEqual(normalizeApiUrl(' http://192.168.1.10:8890 '), {
ok: true,
url: 'http://192.168.1.10:8890',
})
})
test('normalizeApiUrl rejects empty, bad schemes, and credentials-in-URL', () => {
assert.equal(normalizeApiUrl('').error, 'empty')
assert.equal(normalizeApiUrl('not a url').error, 'invalid_url')
assert.equal(normalizeApiUrl('file:///etc/passwd').error, 'unsupported_scheme')
assert.equal(normalizeApiUrl('javascript:alert(1)').error, 'unsupported_scheme')
assert.equal(normalizeApiUrl('http://user:pass@127.0.0.1:8890').error, 'credentials_in_url')
})
test('alarmSubscribeUrl only promotes http(s) to ws(s)', () => {
assert.equal(
alarmSubscribeUrl('http://127.0.0.1:8890'),
'ws://127.0.0.1:8890/v1/integrations/dsh-alarm/ws',
)
assert.equal(
alarmSubscribeUrl('https://netx.example.com/'),
'wss://netx.example.com/v1/integrations/dsh-alarm/ws',
)
assert.equal(alarmSubscribeUrl('file:///tmp'), '')
assert.equal(alarmSubscribeUrl('http://user:secret@127.0.0.1:8890'), '')
assert.equal(alarmSubscribeUrl(''), '')
})

View file

@ -0,0 +1,347 @@
/**
* Smoke: prove dsh-netxops/tools + host can register netx__* against a fake registry.
* Run: node tests/smoke-register-tools.mjs
*/
import assert from 'node:assert/strict'
import { pathToFileURL } from 'node:url'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const STORE_KEY = Symbol.for('dsh-netxops.connection-store')
function connectionStore() {
return globalThis[STORE_KEY]
}
function resetConnectionStore() {
const state = connectionStore()
if (state) {
state.connection = undefined
state.listeners.clear()
}
}
function createLogger(tag) {
const lines = []
const log = (level, ...args) => {
const msg = args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ')
lines.push(`[${level}] ${msg}`)
console.log(`[${tag}/${level}]`, ...args)
}
return {
lines,
info: (...a) => log('info', ...a),
warn: (...a) => log('warn', ...a),
error: (...a) => log('error', ...a),
debug: (...a) => log('debug', ...a),
}
}
/**
* Minimal cordis-like Context: tools.register records names;
* inject runs when all named services exist on ctx; effect runs immediately.
*/
function createFakeCtx(services = {}) {
const registered = []
const effects = []
const logger = createLogger(services._tag ?? 'ctx')
const ctx = {
logger,
tools: {
register(entry) {
const name = entry?.name ?? String(entry)
registered.push(name)
return () => {
const i = registered.indexOf(name)
if (i >= 0) registered.splice(i, 1)
}
},
},
skills: {
register() {
return () => {}
},
},
credentials: {
async resolve() {
return { value: 'smoke-token' }
},
},
settings: {
installSection() {},
describe() {
return []
},
get() {
return undefined
},
},
systemPrompt: {
section() {
return () => {}
},
context() {
return () => {}
},
},
get(name) {
return ctx[name]
},
inject(deps, callback) {
const list = Array.isArray(deps) ? deps : [deps]
const missing = list.filter((d) => ctx[d] === undefined && services[d] === undefined)
if (missing.length > 0) {
// Soft-inject semantics: wait forever (never call) when deps absent.
return
}
// Merge optional service overrides, then invoke.
for (const d of list) {
if (services[d] !== undefined) ctx[d] = services[d]
}
try {
callback(ctx)
} catch (error) {
logger.error('inject callback threw: %s', error)
throw error
}
},
effect(factory, label) {
try {
const dispose = factory()
effects.push({ label, dispose })
return typeof dispose === 'function' ? dispose : () => {}
} catch (error) {
logger.error('effect %s threw: %s', label, error)
throw error
}
},
on() {
return () => {}
},
...services,
}
// Ensure tools always present unless explicitly removed
if (services.tools === undefined && ctx.tools) {
// keep default
}
return { ctx, registered, effects, logger }
}
function disposeAll(effects) {
for (const { dispose } of effects.splice(0)) {
try {
if (typeof dispose === 'function') dispose()
} catch {
// ignore
}
}
}
async function smokeAgentTools() {
console.log('\n=== A) agent-tools apply (provisional connection) ===')
resetConnectionStore()
const mod = await import(pathToFileURL(join(root, 'lib/agent-tools.js')).href)
assert.equal(mod.name, 'netxops-tools')
assert.deepEqual(mod.inject, ['tools'])
assert.equal(typeof mod.apply, 'function')
const { ctx, registered, effects, logger } = createFakeCtx({ _tag: 'agent-tools' })
// Only tools — skills inject should soft-skip (not block tool registration)
delete ctx.skills
let thrown = null
try {
mod.apply(ctx)
} catch (error) {
thrown = error
console.error('agent-tools apply threw:', error)
}
const netx = registered.filter((n) => String(n).startsWith('netx__'))
const hasQuery = netx.includes('netx__queryNmsAlarms')
const sample = netx.slice(0, 12)
console.log('registered count:', registered.length)
console.log('netx__ count:', netx.length)
console.log('sample:', sample)
console.log('has netx__queryNmsAlarms:', hasQuery)
console.log('logger errors:', logger.lines.filter((l) => l.startsWith('[error]')))
disposeAll(effects)
return {
ok: thrown === null && hasQuery && netx.length >= 10,
thrown,
count: netx.length,
names: netx,
sample,
hasQuery,
}
}
async function smokeHostPublish() {
console.log('\n=== B) host apply (provisional publish + public tools) ===')
resetConnectionStore()
const mod = await import(pathToFileURL(join(root, 'lib/index.js')).href)
assert.equal(mod.name, 'netxops')
assert.equal(typeof mod.apply, 'function')
assert.equal(typeof mod.Config, 'function')
const publicRegistered = []
const { ctx, effects, logger } = createFakeCtx({
_tag: 'host',
tools: {
register(entry) {
const name = entry?.name ?? String(entry)
publicRegistered.push(name)
return () => {
const i = publicRegistered.indexOf(name)
if (i >= 0) publicRegistered.splice(i, 1)
}
},
},
})
const config = mod.Config({
apiUrl: 'http://127.0.0.1:8890',
installAgentPreset: false,
alarmPushEnabled: false,
groupOpsInPreset: true,
groupOpsPublic: true,
groupBizMonitorInPreset: true,
groupBizMonitorPublic: false,
groupTopologyInPreset: false,
groupTopologyPublic: false,
groupKbInPreset: false,
groupKbPublic: false,
})
let thrown = null
try {
mod.apply(ctx, config)
} catch (error) {
thrown = error
console.error('host apply threw:', error)
}
// Allow async publish() (credentials resolve) one tick
await new Promise((r) => setTimeout(r, 50))
const store = connectionStore()
const conn = store?.connection
const provisionalOk = Boolean(conn?.apiUrl)
const hostNetx = publicRegistered.filter((n) => String(n).startsWith('netx__'))
const hasQueryHost = hostNetx.includes('netx__queryNmsAlarms')
console.log('connection published:', provisionalOk, conn?.apiUrl, 'tokenConfigured=', Boolean(conn?.token?.trim()))
console.log('host netx__ count:', hostNetx.length)
console.log('host sample:', hostNetx.slice(0, 8))
console.log('has host netx__queryNmsAlarms:', hasQueryHost)
console.log('logger errors:', logger.lines.filter((l) => l.startsWith('[error]')))
disposeAll(effects)
return {
ok: thrown === null && provisionalOk && hasQueryHost && hostNetx.length >= 5,
thrown,
provisionalOk,
apiUrl: conn?.apiUrl,
tokenConfigured: Boolean(conn?.token?.trim()),
publicCount: hostNetx.length,
publicSample: hostNetx.slice(0, 8),
hasQueryPublic: hasQueryHost,
}
}
async function smokeHostThenAgent() {
console.log('\n=== C) host publish then agent-tools remount ===')
resetConnectionStore()
const host = await import(pathToFileURL(join(root, 'lib/index.js')).href)
const tools = await import(pathToFileURL(join(root, 'lib/agent-tools.js')).href)
const hostBag = createFakeCtx({ _tag: 'host-c' })
// Host without tools — only publish path
delete hostBag.ctx.tools
delete hostBag.ctx.skills
const agentBag = createFakeCtx({ _tag: 'agent-c' })
delete agentBag.ctx.skills
host.apply(
hostBag.ctx,
host.Config({
apiUrl: 'http://10.0.0.1:8890',
installAgentPreset: false,
alarmPushEnabled: false,
groupOpsInPreset: true,
groupOpsPublic: false,
groupKbInPreset: false,
}),
)
await new Promise((r) => setTimeout(r, 50))
tools.apply(agentBag.ctx)
await new Promise((r) => setTimeout(r, 20))
const netx = agentBag.registered.filter((n) => String(n).startsWith('netx__'))
const store = connectionStore()?.connection
console.log('live apiUrl:', store?.apiUrl)
console.log('agent netx__ count:', netx.length)
console.log('has query:', netx.includes('netx__queryNmsAlarms'))
disposeAll(hostBag.effects)
disposeAll(agentBag.effects)
return {
ok: netx.includes('netx__queryNmsAlarms') && store?.apiUrl?.includes('10.0.0.1'),
count: netx.length,
apiUrl: store?.apiUrl,
}
}
const results = {
agentTools: await smokeAgentTools(),
host: await smokeHostPublish(),
combined: await smokeHostThenAgent(),
}
console.log('\n=== SUMMARY ===')
console.log(JSON.stringify({
agentTools: {
pass: results.agentTools.ok,
count: results.agentTools.count,
hasQuery: results.agentTools.hasQuery,
sample: results.agentTools.sample,
error: results.agentTools.thrown ? String(results.agentTools.thrown) : null,
},
host: {
pass: results.host.ok,
provisionalOk: results.host.provisionalOk,
apiUrl: results.host.apiUrl,
tokenConfigured: results.host.tokenConfigured,
publicCount: results.host.publicCount,
publicSample: results.host.publicSample,
error: results.host.thrown ? String(results.host.thrown) : null,
},
combined: {
pass: results.combined.ok,
count: results.combined.count,
apiUrl: results.combined.apiUrl,
},
}, null, 2))
const pass = results.agentTools.ok && results.host.ok && results.combined.ok
if (!pass) {
console.error('\nSMOKE FAIL')
process.exitCode = 1
} else {
console.log('\nSMOKE PASS')
}

View file

@ -0,0 +1,93 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
DEFAULT_API_URL,
DEFAULT_LANG,
DEFAULT_TOKEN_REF,
DEFAULT_TOOL_CALL_TIMEOUT_MS,
mergeConfigSkippingNullish,
resolveApiUrlForConnection,
resolveLang,
resolveTokenCredentialRef,
resolveToolCallTimeoutMs,
} from '../src/index.ts'
const base = {
apiUrl: 'http://127.0.0.1:8890',
lang: 'zh',
thinkingLanguage: 'auto',
replyLanguage: 'follow-user',
tokenCredentialRef: 'NETX_API_TOKEN',
toolCallTimeoutMs: 120_000,
installAgentPreset: true,
alarmPushEnabled: false,
alarmDeliverDsh: true,
alarmDeliverIm: false,
imTargets: '',
imBotId: '',
imTargetId: '',
nmsProvider: 'zte-ume',
groupOpsInPreset: true,
groupOpsPublic: false,
groupTopologyInPreset: false,
groupTopologyPublic: false,
groupBizMonitorInPreset: true,
groupBizMonitorPublic: false,
kbRoot: '',
groupKbInPreset: true,
groupKbPublic: false,
}
test('mergeConfigSkippingNullish ignores null/undefined volatile fields', () => {
const merged = mergeConfigSkippingNullish(base, {
apiUrl: null,
toolCallTimeoutMs: null,
lang: 'en',
groupOpsPublic: undefined,
})
assert.equal(merged.apiUrl, 'http://127.0.0.1:8890')
assert.equal(merged.toolCallTimeoutMs, 120_000)
assert.equal(merged.lang, 'en')
assert.equal(merged.groupOpsPublic, false)
})
test('resolveToolCallTimeoutMs rejects null / NaN / non-positive', () => {
assert.equal(resolveToolCallTimeoutMs(null), DEFAULT_TOOL_CALL_TIMEOUT_MS)
assert.equal(resolveToolCallTimeoutMs(undefined), DEFAULT_TOOL_CALL_TIMEOUT_MS)
assert.equal(resolveToolCallTimeoutMs(Number.NaN), DEFAULT_TOOL_CALL_TIMEOUT_MS)
assert.equal(resolveToolCallTimeoutMs(0), DEFAULT_TOOL_CALL_TIMEOUT_MS)
assert.equal(resolveToolCallTimeoutMs(-1), DEFAULT_TOOL_CALL_TIMEOUT_MS)
assert.equal(resolveToolCallTimeoutMs(90_000), 90_000)
})
test('resolveApiUrlForConnection falls back on null/empty; rejects bad schemes', () => {
assert.deepEqual(resolveApiUrlForConnection(null), {
url: DEFAULT_API_URL,
usedDefault: true,
})
assert.deepEqual(resolveApiUrlForConnection(''), {
url: DEFAULT_API_URL,
usedDefault: true,
})
assert.deepEqual(resolveApiUrlForConnection('http://10.0.0.1:8890/'), {
url: 'http://10.0.0.1:8890',
usedDefault: false,
})
assert.equal(resolveApiUrlForConnection('file:///tmp').url, '')
assert.equal(resolveApiUrlForConnection('file:///tmp').error, 'unsupported_scheme')
})
test('resolveTokenCredentialRef heals null/invalid to NETX_API_TOKEN', () => {
assert.equal(resolveTokenCredentialRef(null), DEFAULT_TOKEN_REF)
assert.equal(resolveTokenCredentialRef(''), DEFAULT_TOKEN_REF)
assert.equal(resolveTokenCredentialRef('bad-name'), DEFAULT_TOKEN_REF)
assert.equal(resolveTokenCredentialRef('NETX_API_TOKEN'), 'NETX_API_TOKEN')
assert.equal(resolveTokenCredentialRef('CUSTOM_TOKEN'), 'CUSTOM_TOKEN')
})
test('resolveLang heals null/empty to zh', () => {
assert.equal(resolveLang(null), DEFAULT_LANG)
assert.equal(resolveLang(undefined), DEFAULT_LANG)
assert.equal(resolveLang(''), DEFAULT_LANG)
assert.equal(resolveLang('en'), 'en')
})

View 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')
})