src/services/demo-business-profile.test.ts
Rama main · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
import { once } from 'node:events';
import { createServer } from 'node:net';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { signToolCall } from '../core/contracts/tools';
import { demoToolSpec } from './demo-dataset';
const serverPath = fileURLToPath(new URL('../../demo/business/server.mjs', import.meta.url));
const strongSecret = '4f7b2c91a8d03e65b49c27e018f3d6aa';
async function unusedPort(): Promise<number> {
const reservation = createServer();
await new Promise<void>((resolve, reject) => {
reservation.once('error', reject);
reservation.listen(0, '127.0.0.1', resolve);
});
const address = reservation.address();
assert.ok(address && typeof address === 'object');
const port = address.port;
await new Promise<void>((resolve, reject) => reservation.close((error) => error ? reject(error) : resolve()));
return port;
}
async function waitUntilReady(child: ChildProcess, baseUrl: string, stderr: () => string): Promise<void> {
for (let attempt = 0; attempt < 100; attempt++) {
if (child.exitCode !== null) throw new Error(`demo business 提前退出 (${child.exitCode}): ${stderr()}`);
try {
const response = await fetch(`${baseUrl}/health`);
if (response.ok) return;
} catch {
// 子进程刚启动时连接拒绝属于正常竞态,短暂重试。
}
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error(`demo business 启动超时: ${stderr()}`);
}
async function stop(child: ChildProcess): Promise<void> {
if (child.exitCode !== null) return;
const exited = once(child, 'exit');
child.kill('SIGTERM');
await exited;
}
function signedHeaders(
method: string,
pathWithQuery: string,
body: string,
subject = 'demo-user-001',
jobId = 'contract-test',
signingSecret = strongSecret,
): Record<string, string> {
const timestamp = Math.floor(Date.now() / 1000);
return {
'content-type': 'application/json',
'x-bailing-timestamp': String(timestamp),
'x-bailing-signature': signToolCall(signingSecret, timestamp, method, pathWithQuery, body, subject, jobId),
'x-bailing-on-behalf-of': subject,
'x-bailing-job-id': jobId,
};
}
test('demo business stateless-readonly: 只暴露签名只读工具,不暴露或积累可变状态', { timeout: 10_000 }, async () => {
const port = await unusedPort();
let stderr = '';
const child = spawn(process.execPath, [serverPath], {
env: {
...process.env,
DEMO_HOST: '127.0.0.1',
DEMO_PORT: String(port),
DEMO_PROFILE: 'stateless-readonly',
DEMO_TOOL_SECRET: strongSecret,
// 即使这两项是无效值,只读 profile 也不应触达 Hub 或 client token 路径。
DEMO_HUB_URL: 'http://127.0.0.1:1',
DEMO_CLIENT_TOKEN: 'must-not-be-used',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk: string) => { stderr += chunk; });
const baseUrl = `http://127.0.0.1:${port}`;
try {
await waitUntilReady(child, baseUrl, () => stderr);
const healthResponse = await fetch(`${baseUrl}/health`);
assert.equal(healthResponse.status, 200);
assert.deepEqual(await healthResponse.json(), { ok: true, app: 'demo-business', profile: 'stateless-readonly' });
const rootResponse = await fetch(`${baseUrl}/`);
assert.equal(rootResponse.status, 200);
const root = await rootResponse.text();
assert.doesNotMatch(root, new RegExp('批准|拒绝|authorization: Bearer|/console/'));
const specResponse = await fetch(`${baseUrl}/.well-known/bailing/tools.json`);
assert.equal(specResponse.status, 200);
const spec = await specResponse.json();
assert.deepEqual(spec, demoToolSpec('stateless-readonly'));
assert.deepEqual(Object.keys((spec as { paths: object }).paths).sort(), ['/failure-demo', '/orders']);
const probeBody = JSON.stringify({ subject: 'demo-user-001' });
const probePath = '/.well-known/bailing/authz-probe';
const probeResponse = await fetch(`${baseUrl}${probePath}`, {
method: 'POST',
headers: signedHeaders('POST', probePath, probeBody, 'demo-user-001', 'probe-test'),
body: probeBody,
});
assert.equal(probeResponse.status, 200);
assert.deepEqual(await probeResponse.json(), { authorized: true, subject: 'demo-user-001' });
const orderPath = '/orders?order_no=SO-1001';
const orderResponse = await fetch(`${baseUrl}${orderPath}`, {
headers: signedHeaders('GET', orderPath, ''),
});
assert.equal(orderResponse.status, 200);
const orderResult = await orderResponse.json() as { subject: string; orders: Array<{ order_no: string }> };
assert.equal(orderResult.subject, 'demo-user-001');
assert.deepEqual(orderResult.orders.map((order) => order.order_no), ['SO-1001']);
const failurePath = '/failure-demo';
const failureResponse = await fetch(`${baseUrl}${failurePath}`, {
headers: signedHeaders('GET', failurePath, ''),
});
assert.equal(failureResponse.status, 500);
const mutableRequests: Array<[string, RequestInit]> = [
['/api/state', {}],
['/tickets', { method: 'POST', body: '{}' }],
['/refunds', { method: 'POST', body: '{}' }],
['/approvals', { method: 'POST', body: '{}' }],
['/api/approvals/1/decision', { method: 'POST', body: 'decision=approved' }],
];
for (const [path, init] of mutableRequests) {
const response = await fetch(`${baseUrl}${path}`, init);
assert.equal(response.status, 404, `${path} 在 stateless-readonly 下必须隐藏`);
}
} finally {
await stop(child);
}
});
test('demo business 默认 profile 保持 full-local 工单与状态演示能力', { timeout: 10_000 }, async () => {
const port = await unusedPort();
let stderr = '';
const env = { ...process.env };
delete env.DEMO_PROFILE;
delete env.DEMO_TOOL_SECRET;
const child = spawn(process.execPath, [serverPath], {
env: {
...env,
DEMO_HOST: '127.0.0.1',
DEMO_PORT: String(port),
},
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk: string) => { stderr += chunk; });
const baseUrl = `http://127.0.0.1:${port}`;
try {
await waitUntilReady(child, baseUrl, () => stderr);
const healthResponse = await fetch(`${baseUrl}/health`);
assert.deepEqual(await healthResponse.json(), { ok: true, app: 'demo-business', profile: 'full-local' });
const specResponse = await fetch(`${baseUrl}/.well-known/bailing/tools.json`);
assert.deepEqual(await specResponse.json(), demoToolSpec('full-local'));
const ticketPath = '/tickets';
const ticketBody = JSON.stringify({ order_no: 'SO-1001', title: '需要人工跟进', message: '本地完整 demo 回归' });
const ticketResponse = await fetch(`${baseUrl}${ticketPath}`, {
method: 'POST',
headers: signedHeaders('POST', ticketPath, ticketBody, 'demo-user-001', 'full-local-test', 'demo-tool-secret-change-me'),
body: ticketBody,
});
assert.equal(ticketResponse.status, 200);
const stateResponse = await fetch(`${baseUrl}/api/state`);
assert.equal(stateResponse.status, 200);
const state = await stateResponse.json() as { tickets: Array<{ order_no: string }> };
assert.deepEqual(state.tickets.map((ticket) => ticket.order_no), ['SO-1001']);
} finally {
await stop(child);
}
});
test('demo business stateless-readonly: 弱密钥和占位符启动时 fail-fast', () => {
const weakSecrets = [
'short-secret',
' ',
'demo-tool-secret-change-me',
'test-secret-that-is-long-enough',
'example-secret-that-is-long-enough',
'change-this-secret-right-now',
'replace-this-secret-right-now',
'REPLACE_WITH_32_BYTE_INDEPENDENT_DEMO_TOOL_SECRET',
];
for (const weakSecret of weakSecrets) {
const result = spawnSync(process.execPath, [serverPath], {
env: {
...process.env,
DEMO_HOST: '127.0.0.1',
DEMO_PORT: '0',
DEMO_PROFILE: 'stateless-readonly',
DEMO_TOOL_SECRET: weakSecret,
},
encoding: 'utf8',
timeout: 2_000,
});
assert.notEqual(result.status, 0, `弱密钥 ${weakSecret} 不应启动成功`);
assert.match(result.stderr, /DEMO_TOOL_SECRET 不安全/);
}
});
test('demo business stateless-readonly: 拒绝绑定非回环地址', () => {
const result = spawnSync(process.execPath, [serverPath], {
env: {
...process.env,
DEMO_HOST: '0.0.0.0',
DEMO_PORT: '0',
DEMO_PROFILE: 'stateless-readonly',
DEMO_TOOL_SECRET: strongSecret,
},
encoding: 'utf8',
timeout: 2_000,
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /DEMO_HOST 必须是本机回环地址/);
});