feat(service): P6.3 route REST + approve/deny → execute to sandbox
RouteService (createBinding/listBindings/simulate/listDecisions/approve/deny/ execute/flushDigest); approve REVIEW→ALLOW + route.approved event → execute via P5 OutboundService (sandbox, idempotent per decision); deny never executes. 7 route endpoints; PORTAL adapter registered. 5 route tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,9 @@ import { MessageService, type MessagePrincipal } from '../messaging/message.serv
|
||||
import { ActorResolver } from '../identity/actor.resolver';
|
||||
import { RuleEngine } from './rule-engine';
|
||||
import { RouteSimulator } from './route-simulator';
|
||||
import { RouteService } from './route.service';
|
||||
import { OutboundService } from '../adapters/outbound.service';
|
||||
import { AdapterRegistry } from '../adapters/adapter.registry';
|
||||
import type { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public';
|
||||
@@ -93,3 +96,39 @@ describe('Route simulator (P6, preview-first)', () => {
|
||||
expect(first.decisions[0]?.reasonCodes).toEqual(second.decisions[0]?.reasonCodes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RouteService approve/deny → execute (P6)', () => {
|
||||
const admin: MessagePrincipal = { userId: 'admin', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Admin' };
|
||||
const svc = () => new RouteService(asService, new RouteSimulator(asService, new RuleEngine()), new OutboundService(asService, new AdapterRegistry()), actors);
|
||||
|
||||
it('approving a REVIEW decision executes the forward to the sandbox', async () => {
|
||||
const { interactionId, scopeId } = await makeInteraction('party at 9 PM');
|
||||
await makeBinding(scopeId, 'seniors', 'SENIORS');
|
||||
const service = svc();
|
||||
await service.simulate(interactionId, ORIGIN);
|
||||
const b = await prisma.iiosRouteBinding.findFirstOrThrow({ where: { destinationRef: 'seniors' } });
|
||||
const decision = await prisma.iiosRouteDecision.findUniqueOrThrow({
|
||||
where: { interactionId_routeBindingId: { interactionId, routeBindingId: b.id } },
|
||||
});
|
||||
expect(decision.decisionState).toBe('REVIEW');
|
||||
|
||||
const approved = await service.approve(decision.id, admin);
|
||||
expect(approved.decisionState).toBe('ALLOW');
|
||||
expect(approved.executed).toBe(true);
|
||||
expect(await prisma.iiosOutboundCommand.count()).toBe(1); // dispatched to sandbox
|
||||
});
|
||||
|
||||
it('denying a decision never executes', async () => {
|
||||
const { interactionId, scopeId } = await makeInteraction('party at 9 PM');
|
||||
await makeBinding(scopeId, 'children', 'CHILD');
|
||||
const service = svc();
|
||||
await service.simulate(interactionId, ORIGIN);
|
||||
const b = await prisma.iiosRouteBinding.findFirstOrThrow({ where: { destinationRef: 'children' } });
|
||||
const decision = await prisma.iiosRouteDecision.findUniqueOrThrow({
|
||||
where: { interactionId_routeBindingId: { interactionId, routeBindingId: b.id } },
|
||||
});
|
||||
// already DENY; approving is rejected, and nothing is sent
|
||||
await expect(service.approve(decision.id, admin)).rejects.toThrow();
|
||||
expect(await prisma.iiosOutboundCommand.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user