130 lines
4.3 KiB
JavaScript
130 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { spawn } from 'node:child_process'
|
|
import { once } from 'node:events'
|
|
import { createServer } from 'node:http'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { WebSocket } from 'ws'
|
|
import { createOntologyMcpServer } from '../../../ontology-core/src/mcp-server.mjs'
|
|
|
|
const INTERNAL_TOKEN = 'hub-ontology-mcp-smoke-token'
|
|
const PAIRING_CODE = 'ONTOLOGYSMOKE1'
|
|
const currentFile = fileURLToPath(import.meta.url)
|
|
const hubRoot = path.resolve(path.dirname(currentFile), '..', '..')
|
|
|
|
const ontologyPort = await availablePort()
|
|
const hubPort = await availablePort()
|
|
const ontologyServer = createOntologyMcpServer({
|
|
internalTokens: [INTERNAL_TOKEN],
|
|
allowedOrigins: [],
|
|
maxBodyBytes: 1024 * 1024,
|
|
})
|
|
const hubProcess = spawn(process.execPath, ['src/server.mjs'], {
|
|
cwd: hubRoot,
|
|
env: {
|
|
...process.env,
|
|
PORT: String(hubPort),
|
|
AI_WORKSPACE_HUB_TOKEN: INTERNAL_TOKEN,
|
|
NODEDC_INTERNAL_ACCESS_TOKEN: INTERNAL_TOKEN,
|
|
NODEDC_ONTOLOGY_CORE_URL: `http://127.0.0.1:${ontologyPort}`,
|
|
NODEDC_AI_WORKSPACE_ASSISTANT_URL: 'http://127.0.0.1:1',
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
})
|
|
let hubOutput = ''
|
|
hubProcess.stdout.on('data', (chunk) => { hubOutput += String(chunk) })
|
|
hubProcess.stderr.on('data', (chunk) => { hubOutput += String(chunk) })
|
|
let worker = null
|
|
|
|
try {
|
|
await new Promise((resolve) => ontologyServer.listen(ontologyPort, '127.0.0.1', resolve))
|
|
await waitForHub(`http://127.0.0.1:${hubPort}/healthz`, hubProcess)
|
|
|
|
worker = new WebSocket(
|
|
`ws://127.0.0.1:${hubPort}/api/ai-workspace/hub?pairingCode=${PAIRING_CODE}&machineName=ontology-smoke`,
|
|
)
|
|
await once(worker, 'open')
|
|
worker.send(JSON.stringify({
|
|
type: 'hello',
|
|
agentVersion: 'smoke',
|
|
protocolVersion: 'ai-workspace-bridge/v1',
|
|
capabilities: ['smoke'],
|
|
}))
|
|
|
|
const response = await fetch(
|
|
`http://127.0.0.1:${hubPort}/api/ai-workspace/hub/v1/ontology-mcp/${PAIRING_CODE}/mcp`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
accept: 'application/json, text/event-stream',
|
|
'mcp-protocol-version': '2025-06-18',
|
|
},
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'ontology_get_entity',
|
|
arguments: { term: 'helius' },
|
|
},
|
|
}),
|
|
},
|
|
)
|
|
const payload = await response.json()
|
|
assert.equal(response.ok, true)
|
|
assert.equal(response.headers.get('mcp-protocol-version'), '2025-06-18')
|
|
assert.equal(payload.result.structuredContent.entity.id, 'gelios.integration')
|
|
|
|
worker.close()
|
|
await once(worker, 'close')
|
|
const offline = await fetch(
|
|
`http://127.0.0.1:${hubPort}/api/ai-workspace/hub/v1/ontology-mcp/${PAIRING_CODE}/mcp`,
|
|
{ method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' },
|
|
)
|
|
assert.equal(offline.status, 503)
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
'pairing_bound_hub_proxy',
|
|
'internal_bearer_replaced_by_hub',
|
|
'ontology_mcp_tool_response',
|
|
'mcp_protocol_header_forwarded',
|
|
'offline_pairing_is_rejected',
|
|
],
|
|
}, null, 2))
|
|
} finally {
|
|
if (worker && worker.readyState === WebSocket.OPEN) worker.close()
|
|
if (hubProcess.exitCode === null && !hubProcess.killed) {
|
|
hubProcess.kill('SIGTERM')
|
|
await once(hubProcess, 'exit').catch(() => {})
|
|
}
|
|
await new Promise((resolve, reject) => ontologyServer.close((error) => error ? reject(error) : resolve()))
|
|
if (hubProcess.exitCode && hubProcess.exitCode !== 0) {
|
|
throw new Error(`hub_smoke_child_failed:${hubOutput.slice(-2000)}`)
|
|
}
|
|
}
|
|
|
|
async function availablePort() {
|
|
const server = createServer()
|
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
const port = server.address().port
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
|
return port
|
|
}
|
|
|
|
async function waitForHub(url, processRef) {
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
if (processRef.exitCode !== null) throw new Error('hub_smoke_child_exited_early')
|
|
try {
|
|
const response = await fetch(url)
|
|
if (response.ok) return
|
|
} catch {}
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
}
|
|
throw new Error('hub_smoke_health_timeout')
|
|
}
|