mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-20 07:40:46 +08:00
- Drop the TypeScript netx tool implementations and the hard-coded TOOLS_BY_GROUP list; register netx__* tools from the server's tools/list (group from _meta.netx.group) and forward tools/call. - Thin Streamable HTTP client on standard fetch (fetchImpl injectable); cached catalog when netx is unreachable, refresh to reconnect. - disabledTools (deny list; new server tools follow their group) and readOnlyOnly (only readOnlyHint=true tools), re-registered live. - Settings card: per-group expandable tool rows with switches and a write marker; skills append a disabled-tools note. - Toggles only control model visibility; netx enforces security. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
4.2 KiB
JavaScript
106 lines
4.2 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
import { createScope, scopeOf } from '@deepseek-ai/dsh-scope'
|
|
import { mkdtempSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { installPresetPublication } from '../src/netx/preset-publication.ts'
|
|
import { publishNetxConnection } from '../src/netx/runtime.ts'
|
|
import { capabilityGroupsFromSettings } from '../src/netx/capability-groups.ts'
|
|
import { OPS_TOOL_NAMES, clearMcpCatalogSeed, seedMcpCatalog } from './mcp-test-catalog.mjs'
|
|
|
|
const layers = new Map()
|
|
const skillLayers = new Map()
|
|
let live = []
|
|
class Tools extends Service {
|
|
constructor(ctx) { super(ctx, 'tools') }
|
|
register(entry) {
|
|
const key = scopeOf(this.ctx)
|
|
let layer = layers.get(key)
|
|
if (!layer) layers.set(key, layer = new Map())
|
|
if (layer.has(entry.name)) throw new Error(`duplicate tool ${entry.name}`)
|
|
layer.set(entry.name, entry)
|
|
return () => layer.delete(entry.name)
|
|
}
|
|
}
|
|
class Skills extends Service {
|
|
constructor(ctx) { super(ctx, 'skills') }
|
|
register(entry) {
|
|
const key = scopeOf(this.ctx)
|
|
let layer = skillLayers.get(key)
|
|
if (!layer) skillLayers.set(key, layer = new Map())
|
|
if (layer.has(entry.name)) throw new Error(`duplicate skill ${entry.name}`)
|
|
layer.set(entry.name, entry)
|
|
return () => layer.delete(entry.name)
|
|
}
|
|
}
|
|
class Agents extends Service {
|
|
constructor(ctx) { super(ctx, 'agents') }
|
|
list() { return live }
|
|
}
|
|
class Presets extends Service {
|
|
constructor(ctx) { super(ctx, 'agentPresets') }
|
|
composedPreset(ctx) { return ctx.presetId }
|
|
}
|
|
const wait = () => new Promise(resolve => setTimeout(resolve, 50))
|
|
|
|
test('selected publication uses each exact session scope and cleans up on off, switch, and unload', async t => {
|
|
const previous = process.env.DSH_HOME
|
|
process.env.DSH_HOME = mkdtempSync(join(tmpdir(), 'netxops-selected-'))
|
|
seedMcpCatalog()
|
|
const ctx = new Context()
|
|
const owners = []
|
|
t.after(async () => {
|
|
for (const owner of owners.reverse()) await owner.dispose()
|
|
clearMcpCatalogSeed()
|
|
if (previous === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = previous
|
|
layers.clear(); skillLayers.clear(); live = []
|
|
})
|
|
owners.push(await ctx.plugin(Tools))
|
|
owners.push(await ctx.plugin(Skills))
|
|
owners.push(await ctx.plugin(Agents))
|
|
owners.push(await ctx.plugin(Presets))
|
|
const scopes = []
|
|
owners.push(await ctx.plugin(Object.assign(inner => {
|
|
for (const id of ['standard', 'review']) {
|
|
const key = {}
|
|
const scope = createScope(inner, key)
|
|
const agent = { id, ctx: scope.ctx.extend({ presetId: id }) }
|
|
scopes.push({ key, scope, agent })
|
|
}
|
|
}, { inject: ['tools', 'skills'] })))
|
|
live = scopes.map(row => row.agent)
|
|
const publish = (overrides = {}) => publishNetxConnection({
|
|
apiUrl: 'http://127.0.0.1:8890', token: '', lang: 'zh', toolCallTimeoutMs: 1000,
|
|
toolsEnabled: true,
|
|
groups: capabilityGroupsFromSettings({ groupOpsPresets: 'standard', groupBizMonitorInPreset: false }),
|
|
groupKbInPreset: false, groupKbPresets: '', ...overrides,
|
|
})
|
|
publish()
|
|
const owner = await ctx.plugin(installPresetPublication)
|
|
owners.push(owner)
|
|
await wait()
|
|
assert.equal(layers.get(scopes[0].key)?.size, OPS_TOOL_NAMES.length)
|
|
assert.equal(skillLayers.get(scopes[0].key)?.has('netx-ops'), true)
|
|
assert.equal(layers.get(scopes[1].key)?.size ?? 0, 0)
|
|
publish({ toolsEnabled: false })
|
|
await wait()
|
|
assert.equal(layers.get(scopes[0].key)?.size, 0)
|
|
assert.equal(skillLayers.get(scopes[0].key)?.size, 0)
|
|
publish()
|
|
await wait()
|
|
assert.equal(layers.get(scopes[0].key)?.size, OPS_TOOL_NAMES.length)
|
|
scopes[0].agent.ctx = scopes[0].agent.ctx.extend({ presetId: 'review' })
|
|
await ctx.serial('agent-preset/selected', 'standard', 'review')
|
|
await wait()
|
|
assert.equal(layers.get(scopes[0].key)?.size, 0)
|
|
scopes[1].agent.ctx = scopes[1].agent.ctx.extend({ presetId: 'standard' })
|
|
await ctx.serial('agent/created', { agent: scopes[1].agent })
|
|
await wait()
|
|
assert.equal(layers.get(scopes[1].key)?.size, OPS_TOOL_NAMES.length)
|
|
await owner.dispose()
|
|
await wait()
|
|
assert.equal(layers.get(scopes[1].key)?.size, 0)
|
|
assert.equal(skillLayers.get(scopes[1].key)?.size, 0)
|
|
})
|