feat(p9): real-egress proof (env-gated HttpProvider) + /health bindings + smoke
Task 9.5: /health now reports the egress provider backing each channel (sandbox vs http). scripts/smoke-capability.mjs proves the slice end to end: sandbox egress SENT through the broker; EMAIL routed to the env-gated HttpProvider and actually delivered to a LOCAL echo server (real egress, no external network, no credentials); idempotent replay; /health bindings. App wires CapabilityModule. P9 slice-1 verification: 103 tests green, pnpm -r build all packages+demos, boundary clean, capability smoke PASS, all prior smokes (realtime/inbox/support/ adapter/route/ai/calendar) still PASS through the broker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// P9 slice-1 smoke: the Capability Broker as governed egress.
|
||||
// (a) sandbox egress SENT via the broker (WEBHOOK, no network);
|
||||
// (b) REAL egress via the env-gated HttpProvider → a LOCAL echo server (EMAIL);
|
||||
// (c) idempotent replay; (d) /health shows provider bindings.
|
||||
// Requires the service started with IIOS_PROVIDER_URL_EMAIL=http://127.0.0.1:4599/echo.
|
||||
import 'dotenv/config';
|
||||
import http from 'node:http';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200';
|
||||
const APP_ID = 'portal-demo';
|
||||
const APP_SECRET = JSON.parse(process.env.APP_SECRETS ?? '{"portal-demo":"dev-secret"}')[APP_ID];
|
||||
const token = jwt.sign({ sub: 'egress', appId: APP_ID, orgId: `org_${APP_ID}` }, APP_SECRET, { algorithm: 'HS256', expiresIn: '1h' });
|
||||
const assert = (c, m) => { if (!c) { console.error('✗', m); process.exit(1); } console.log('✓', m); };
|
||||
|
||||
async function send(channelType, target, payload, idempotencyKey) {
|
||||
const r = await fetch(`${SERVICE}/v1/adapters/${channelType}/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ target, payload, idempotencyKey }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`send ${channelType} ${r.status}: ${await r.text()}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// Local echo server the real HttpProvider will POST to (no external network).
|
||||
const received = [];
|
||||
const echo = http.createServer((req, res) => {
|
||||
let b = '';
|
||||
req.on('data', (c) => (b += c));
|
||||
req.on('end', () => { try { received.push(JSON.parse(b || '{}')); } catch { received.push({}); } res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true}'); });
|
||||
});
|
||||
await new Promise((r) => echo.listen(4599, '127.0.0.1', r));
|
||||
|
||||
try {
|
||||
// (a) sandbox egress through the broker
|
||||
const sandboxCmd = await send('WEBHOOK', 'user@x', { text: 'hi via broker' }, `cap-sb-${Date.now()}`);
|
||||
assert(sandboxCmd.status === 'SENT' && String(sandboxCmd.providerRef).startsWith('sim-'), 'sandbox egress SENT through the broker (no network)');
|
||||
|
||||
// (b) real egress via env-gated HttpProvider → local echo server
|
||||
const before = received.length;
|
||||
const httpCmd = await send('EMAIL', 'buyer@yamuna', { text: 'real egress proof' }, `cap-http-${Date.now()}`);
|
||||
assert(httpCmd.status === 'SENT' && String(httpCmd.providerRef).startsWith('http-'), 'EMAIL egress routed to the real HttpProvider');
|
||||
assert(received.length === before + 1, 'local echo server actually received the POST (real egress, no external network)');
|
||||
assert(received[received.length - 1]?.payload?.text === 'real egress proof', 'echo received the exact payload');
|
||||
|
||||
// (c) idempotent replay → one command
|
||||
const key = `cap-idem-${Date.now()}`;
|
||||
const a = await send('EMAIL', 'buyer@yamuna', { text: 'x' }, key);
|
||||
const b = await send('EMAIL', 'buyer@yamuna', { text: 'x' }, key);
|
||||
assert(a.id === b.id, 'idempotent replay → one command');
|
||||
|
||||
// (d) /health shows provider bindings
|
||||
const health = await (await fetch(`${SERVICE}/health`)).json();
|
||||
assert(health.egressProviders?.EMAIL === 'http' && health.egressProviders?.WEBHOOK === 'sandbox', '/health shows EMAIL=http, WEBHOOK=sandbox bindings');
|
||||
|
||||
console.log('\nP9 capability smoke: PASS');
|
||||
} finally {
|
||||
echo.close();
|
||||
}
|
||||
process.exit(0);
|
||||
@@ -12,6 +12,7 @@ import { AdaptersModule } from './adapters/adapters.module';
|
||||
import { RoutingModule } from './routing/routing.module';
|
||||
import { AiModule } from './ai/ai.module';
|
||||
import { CalendarModule } from './calendar/calendar.module';
|
||||
import { CapabilityModule } from './capability/capability.module';
|
||||
import { HealthController } from './health.controller';
|
||||
import { DevController } from './dev/dev.controller';
|
||||
|
||||
@@ -30,6 +31,7 @@ import { DevController } from './dev/dev.controller';
|
||||
RoutingModule,
|
||||
AiModule,
|
||||
CalendarModule,
|
||||
CapabilityModule,
|
||||
],
|
||||
controllers: [HealthController, DevController],
|
||||
})
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma/prisma.service';
|
||||
import { CapabilityProviderRegistry } from './capability/capability.registry';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly providers: CapabilityProviderRegistry,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async check(): Promise<{ status: string; db: boolean }> {
|
||||
async check(): Promise<{ status: string; db: boolean; egressProviders: Record<string, string> }> {
|
||||
let db = false;
|
||||
try {
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
@@ -14,6 +18,7 @@ export class HealthController {
|
||||
} catch {
|
||||
db = false;
|
||||
}
|
||||
return { status: 'ok', db };
|
||||
// Ops view: which provider (sandbox vs http) backs each egress channel.
|
||||
return { status: 'ok', db, egressProviders: this.providers.bindings() };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user