Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESFAnh2t9HmLwng8oW95St
668 lines
30 KiB
TypeScript
668 lines
30 KiB
TypeScript
import 'reflect-metadata';
|
|
import { Global, Module } from '@nestjs/common';
|
|
import { Test, type TestingModule } from '@nestjs/testing';
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
import type { HarnessAdapter, HarnessConversationService } from '@mosaicstack/types';
|
|
import { AgentService } from '../agent/agent.service.js';
|
|
import { AuthGuard } from '../auth/auth.guard.js';
|
|
import { CommandsModule } from '../commands/commands.module.js';
|
|
import { HarnessModule } from '../harness/harness.module.js';
|
|
import { ChatModule } from './chat.module.js';
|
|
import { ChatGateway } from './chat.gateway.js';
|
|
import { HarnessRegistry } from '../harness/harness.registry.js';
|
|
import {
|
|
HARNESS_CONVERSATION_SERVICE,
|
|
HARNESS_CONVERSATION_SERVICE_UNAVAILABLE,
|
|
HARNESS_REGISTRY,
|
|
type HarnessConversationServiceBinding,
|
|
} from '../harness/harness.tokens.js';
|
|
import { ChatRuntimeRouter } from './chat-runtime-router.js';
|
|
import {
|
|
ChatRuntimeUnavailableError,
|
|
type ChatRuntime,
|
|
type ChatRuntimeMode,
|
|
} from './chat-runtime.js';
|
|
import { AppModule } from '../app.module.js';
|
|
import { ProviderService } from '../agent/provider.service.js';
|
|
|
|
/**
|
|
* Task Five, Step One (router). Proves the `ChatRuntimeRouter` resolves exactly one
|
|
* runtime by mode, fails closed at init when `pi-rpc` preconditions are unmet, and
|
|
* never downgrades `pi-rpc` to embedded execution. Red-first: the router is an
|
|
* unimplemented stub, so every behavioural assertion below fails until Step Three.
|
|
*/
|
|
|
|
const embedded: ChatRuntime = { kind: 'embedded' };
|
|
const harness: ChatRuntime = { kind: 'harness' };
|
|
|
|
/** A structurally-complete, non-sentinel conversation service. Its methods are never invoked here. */
|
|
const boundConversationService = {
|
|
attach: () => Promise.reject(new Error('unused')),
|
|
detach: () => Promise.reject(new Error('unused')),
|
|
send: () => Promise.reject(new Error('unused')),
|
|
|
|
subscribeFrom: async function* () {
|
|
throw new Error('unused');
|
|
},
|
|
} as unknown as HarnessConversationService;
|
|
|
|
function registryWith(adapterIds: readonly string[]): HarnessRegistry {
|
|
const registry = new HarnessRegistry();
|
|
for (const id of adapterIds) {
|
|
registry.register({
|
|
id,
|
|
describe: () => Promise.reject(new Error('unused')),
|
|
catalog: () => Promise.reject(new Error('unused')),
|
|
create: () => Promise.reject(new Error('unused')),
|
|
resume: () => Promise.reject(new Error('unused')),
|
|
} as HarnessAdapter);
|
|
}
|
|
return registry;
|
|
}
|
|
|
|
function buildRouter(
|
|
mode: ChatRuntimeMode,
|
|
opts: { adapters: readonly string[]; service: HarnessConversationServiceBinding },
|
|
): ChatRuntimeRouter {
|
|
return new ChatRuntimeRouter(registryWith(opts.adapters), opts.service, embedded, harness, mode);
|
|
}
|
|
|
|
/**
|
|
* Tear down a module that was deliberately driven to a fail-closed init.
|
|
* `NestApplicationContext.close()` re-awaits the module's `initializationPromise` before disposing
|
|
* (nest-application-context.js:127); when `init()` rejected, that await re-throws the SAME typed
|
|
* startup error, this time into teardown. Each caller here has already captured and asserted that
|
|
* exact `ChatRuntimeUnavailableError` via `initError`, so the re-throw is expected teardown noise —
|
|
* swallow ONLY that error, and surface anything else so a genuine teardown fault still fails loudly.
|
|
*/
|
|
async function closeIgnoringFailedInit(moduleRef: TestingModule): Promise<void> {
|
|
await moduleRef.close().catch((err: unknown) => {
|
|
if (err instanceof ChatRuntimeUnavailableError) return;
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
describe('ChatRuntimeRouter', () => {
|
|
it('resolves only the harness runtime in pi-rpc mode when pi adapter and conversation service are present', () => {
|
|
const router = buildRouter('pi-rpc', {
|
|
adapters: ['pi'],
|
|
service: boundConversationService,
|
|
});
|
|
|
|
expect(() => router.onModuleInit()).not.toThrow();
|
|
expect(router.active).toBe(harness);
|
|
expect(router.active.kind).toBe('harness');
|
|
});
|
|
|
|
it('resolves only the embedded runtime in legacy mode and skips the pi preconditions', () => {
|
|
// Empty registry + unavailable service: legacy must ignore both and still start.
|
|
const router = buildRouter('legacy', {
|
|
adapters: [],
|
|
service: HARNESS_CONVERSATION_SERVICE_UNAVAILABLE,
|
|
});
|
|
|
|
expect(() => router.onModuleInit()).not.toThrow();
|
|
expect(router.active).toBe(embedded);
|
|
expect(router.active.kind).toBe('embedded');
|
|
});
|
|
|
|
it('fails closed at init when pi-rpc mode has no registered pi adapter', () => {
|
|
const router = buildRouter('pi-rpc', {
|
|
adapters: [],
|
|
service: boundConversationService,
|
|
});
|
|
|
|
expect(() => router.onModuleInit()).toThrow(ChatRuntimeUnavailableError);
|
|
try {
|
|
router.onModuleInit();
|
|
expect.unreachable('onModuleInit must throw when the pi adapter is absent');
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(ChatRuntimeUnavailableError);
|
|
expect((err as ChatRuntimeUnavailableError).reason).toBe('adapter_unavailable');
|
|
expect((err as ChatRuntimeUnavailableError).code).toBe('runtime_unsupported');
|
|
}
|
|
});
|
|
|
|
it('fails closed at init when pi-rpc mode has the unavailable conversation-service sentinel', () => {
|
|
const router = buildRouter('pi-rpc', {
|
|
adapters: ['pi'],
|
|
service: HARNESS_CONVERSATION_SERVICE_UNAVAILABLE,
|
|
});
|
|
|
|
try {
|
|
router.onModuleInit();
|
|
expect.unreachable('onModuleInit must throw when the conversation service is unbound');
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(ChatRuntimeUnavailableError);
|
|
expect((err as ChatRuntimeUnavailableError).reason).toBe('conversation_service_unavailable');
|
|
expect((err as ChatRuntimeUnavailableError).code).toBe('runtime_unsupported');
|
|
}
|
|
});
|
|
|
|
it('never falls back to embedded execution when pi-rpc preconditions are unmet', () => {
|
|
const router = buildRouter('pi-rpc', {
|
|
adapters: [],
|
|
service: HARNESS_CONVERSATION_SERVICE_UNAVAILABLE,
|
|
});
|
|
|
|
expect(() => router.onModuleInit()).toThrow(ChatRuntimeUnavailableError);
|
|
// A failed pi-rpc init must not silently expose the embedded runtime.
|
|
expect(() => router.active).toThrow();
|
|
let leaked: ChatRuntime | undefined;
|
|
try {
|
|
leaked = router.active;
|
|
} catch {
|
|
leaked = undefined;
|
|
}
|
|
expect(leaked).not.toBe(embedded);
|
|
});
|
|
|
|
it('exposes only fixed, browser-safe failure text (no raw provider or exception detail)', () => {
|
|
const router = buildRouter('pi-rpc', {
|
|
adapters: [],
|
|
service: boundConversationService,
|
|
});
|
|
|
|
try {
|
|
router.onModuleInit();
|
|
expect.unreachable('onModuleInit must throw');
|
|
} catch (err) {
|
|
const message = (err as ChatRuntimeUnavailableError).message;
|
|
expect(message).toBe(
|
|
'The pi-rpc chat runtime is unavailable: no "pi" harness adapter is registered.',
|
|
);
|
|
expect(message).not.toMatch(/Error:|\bat \b|node_modules|Symbol\(/);
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Task Five, Step Two — group 1 (real Nest module-graph readiness).
|
|
*
|
|
* The unit suite above constructs the router directly. This group drives the SAME contract
|
|
* through a real NestJS graph: it imports the production `HarnessModule` (the proven-booting
|
|
* idiom from harness.controller.spec.ts) so the router resolves the REAL, empty `HarnessRegistry`
|
|
* via the real `HARNESS_REGISTRY` token, then runs the router's `OnModuleInit` through the Nest
|
|
* lifecycle (`moduleRef.init()`). Red-first: the router is an unimplemented stub whose
|
|
* `onModuleInit` throws a generic Error, so:
|
|
* - readiness cases fail because the graph never comes up (init rejects), and
|
|
* - fail-closed cases fail because a generic stub throw is NOT the SPECIFIC typed
|
|
* `ChatRuntimeUnavailableError` (reason/code) the contract demands — a stub that
|
|
* "throws anything" cannot mask these greens.
|
|
* The router is NOT wired into a production module yet, so it is provided here via a factory
|
|
* over the real registry token. Importing the real `ChatModule` bare is deliberately avoided:
|
|
* it injects `AgentService` without importing `AgentModule`, so its graph fails to RESOLVE — a
|
|
* collection/DI error, not a behavioural red. `next` is untouched; nothing here implements the router.
|
|
*/
|
|
describe('ChatRuntimeRouter — real Nest module-graph readiness (Task Five, Step Two group 1)', () => {
|
|
async function bootRouterGraph(
|
|
mode: ChatRuntimeMode,
|
|
opts: { adapters: readonly string[]; service: HarnessConversationServiceBinding },
|
|
) {
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [HarnessModule],
|
|
providers: [
|
|
{
|
|
provide: ChatRuntimeRouter,
|
|
useFactory: (registry: HarnessRegistry) =>
|
|
new ChatRuntimeRouter(registry, opts.service, embedded, harness, mode),
|
|
inject: [HARNESS_REGISTRY],
|
|
},
|
|
],
|
|
})
|
|
// The imported HarnessModule's controllers reference AuthGuard (an HTTP-only concern,
|
|
// never exercised here); stub it so the graph resolves. The registry is NOT overridden —
|
|
// group 1 asserts against the genuine production HarnessRegistry.
|
|
.overrideGuard(AuthGuard)
|
|
.useValue({ canActivate: () => true })
|
|
.compile();
|
|
|
|
// Resolve the production registry singleton and register the requested adapters ON IT, so
|
|
// the router (which injects the same singleton) sees them when its lifecycle hook runs.
|
|
const registry = moduleRef.get<HarnessRegistry>(HARNESS_REGISTRY, { strict: false });
|
|
for (const id of opts.adapters) {
|
|
registry.register({
|
|
id,
|
|
describe: () => Promise.reject(new Error('unused')),
|
|
catalog: () => Promise.reject(new Error('unused')),
|
|
create: () => Promise.reject(new Error('unused')),
|
|
resume: () => Promise.reject(new Error('unused')),
|
|
} as HarnessAdapter);
|
|
}
|
|
return moduleRef;
|
|
}
|
|
|
|
// Capture an init rejection without letting a resolved init masquerade as success.
|
|
const initError = (moduleRef: { init(): Promise<unknown> }): Promise<unknown> =>
|
|
moduleRef.init().then(
|
|
() => new Error('module init resolved but the contract requires it to reject'),
|
|
(err: unknown) => err,
|
|
);
|
|
|
|
it('brings the graph up and resolves only the harness runtime in pi-rpc mode (pi adapter + bound service)', async () => {
|
|
const moduleRef = await bootRouterGraph('pi-rpc', {
|
|
adapters: ['pi'],
|
|
service: boundConversationService,
|
|
});
|
|
try {
|
|
await moduleRef.init();
|
|
const router = moduleRef.get(ChatRuntimeRouter, { strict: false });
|
|
expect(router.active).toBe(harness);
|
|
expect(router.active.kind).toBe('harness');
|
|
} finally {
|
|
await moduleRef.close();
|
|
}
|
|
});
|
|
|
|
it('brings the graph up in legacy mode over the REAL empty HarnessRegistry and resolves only the embedded runtime', async () => {
|
|
const moduleRef = await bootRouterGraph('legacy', {
|
|
adapters: [],
|
|
service: HARNESS_CONVERSATION_SERVICE_UNAVAILABLE,
|
|
});
|
|
try {
|
|
// Defense-in-depth: the production module wires the genuine registry, empty by default —
|
|
// guards against a test-double registry silently satisfying the readiness check.
|
|
const registry = moduleRef.get<HarnessRegistry>(HARNESS_REGISTRY, { strict: false });
|
|
expect(registry).toBeInstanceOf(HarnessRegistry);
|
|
expect(registry.list()).toHaveLength(0);
|
|
|
|
await moduleRef.init();
|
|
const router = moduleRef.get(ChatRuntimeRouter, { strict: false });
|
|
expect(router.active).toBe(embedded);
|
|
expect(router.active.kind).toBe('embedded');
|
|
} finally {
|
|
await moduleRef.close();
|
|
}
|
|
});
|
|
|
|
it('fails closed at module init when pi-rpc mode has no registered pi adapter (specific typed error, not a stub throw)', async () => {
|
|
const moduleRef = await bootRouterGraph('pi-rpc', {
|
|
adapters: [],
|
|
service: boundConversationService,
|
|
});
|
|
try {
|
|
const err = await initError(moduleRef);
|
|
expect(err).toBeInstanceOf(ChatRuntimeUnavailableError);
|
|
expect((err as ChatRuntimeUnavailableError).reason).toBe('adapter_unavailable');
|
|
expect((err as ChatRuntimeUnavailableError).code).toBe('runtime_unsupported');
|
|
} finally {
|
|
await closeIgnoringFailedInit(moduleRef);
|
|
}
|
|
});
|
|
|
|
it('fails closed at module init when pi-rpc mode has the unavailable conversation-service sentinel', async () => {
|
|
const moduleRef = await bootRouterGraph('pi-rpc', {
|
|
adapters: ['pi'],
|
|
service: HARNESS_CONVERSATION_SERVICE_UNAVAILABLE,
|
|
});
|
|
try {
|
|
const err = await initError(moduleRef);
|
|
expect(err).toBeInstanceOf(ChatRuntimeUnavailableError);
|
|
expect((err as ChatRuntimeUnavailableError).reason).toBe('conversation_service_unavailable');
|
|
expect((err as ChatRuntimeUnavailableError).code).toBe('runtime_unsupported');
|
|
} finally {
|
|
await closeIgnoringFailedInit(moduleRef);
|
|
}
|
|
});
|
|
|
|
it('surfaces only fixed, browser-safe failure text when the graph fails closed (no stub/exception detail)', async () => {
|
|
const moduleRef = await bootRouterGraph('pi-rpc', {
|
|
adapters: [],
|
|
service: boundConversationService,
|
|
});
|
|
try {
|
|
const err = await initError(moduleRef);
|
|
expect(err).toBeInstanceOf(ChatRuntimeUnavailableError);
|
|
const message = (err as ChatRuntimeUnavailableError).message;
|
|
expect(message).toBe(
|
|
'The pi-rpc chat runtime is unavailable: no "pi" harness adapter is registered.',
|
|
);
|
|
expect(message).not.toMatch(/Error:|\bat \b|node_modules|Symbol\(|not implemented/);
|
|
} finally {
|
|
await closeIgnoringFailedInit(moduleRef);
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Task Five, Step Two — group 1b (production ChatModule wiring, declaration proof).
|
|
*
|
|
* Correction #1 (Scrappy fe3e02) asked for a red that imports the real `ChatModule` and calls
|
|
* `module.init()`. Investigated and found impractical/masking-prone: `ChatModule` provides
|
|
* `ChatGateway`, whose 10-argument constructor injects app-global providers (AgentService, AUTH,
|
|
* BRAIN, RoutingEngineService) plus the Commands/GC/Mcp/Reload subsystems across a forwardRef
|
|
* cycle. Booting it in isolation is a full-app integration boot — "override only unrelated
|
|
* dependencies" balloons into faking ~4 subsystems, and `overrideProvider` cannot even grant the
|
|
* cross-module export-scope visibility ChatGateway needs (probe: `ChatGateway` unresolved at
|
|
* `CommandExecutorService`). That is exactly the STOP-and-return branch of the directive.
|
|
*
|
|
* The faithful, unmaskable cover instead of a fragile boot: read the PRODUCTION `ChatModule`'s own
|
|
* Nest `@Module` metadata to prove it DECLARES the exclusive router provider and imports the real
|
|
* `HarnessModule` (the genuine registry source). This inspects the actual module object — not
|
|
* source text, not a test factory — so nothing can mask it. Group 1 above separately proves the
|
|
* router RESOLVES against the real, empty `HarnessRegistry` through the Nest lifecycle; the union
|
|
* of the two covers "the router is wired through ChatModule to the real registry" without the
|
|
* impractical single-graph boot. RED today (ChatModule provides only ChatGateway and imports only
|
|
* CommandsModule); GREEN once Step Three registers the router and imports HarnessModule.
|
|
*/
|
|
describe('ChatModule production wiring (Task Five, Step Two group 1b — declaration proof)', () => {
|
|
// Unwrap a forwardRef(() => Module) import to the module it references; pass others through.
|
|
const resolveImport = (imp: unknown): unknown =>
|
|
imp &&
|
|
typeof imp === 'object' &&
|
|
typeof (imp as { forwardRef?: unknown }).forwardRef === 'function'
|
|
? (imp as { forwardRef: () => unknown }).forwardRef()
|
|
: imp;
|
|
|
|
// A provider entry is either a class (shorthand) or a { provide, ... } object; take its token.
|
|
const providerToken = (provider: unknown): unknown =>
|
|
typeof provider === 'function' ? provider : (provider as { provide?: unknown })?.provide;
|
|
|
|
it('declares the exclusive ChatRuntimeRouter as a provider on the production ChatModule', () => {
|
|
const providers: unknown[] = Reflect.getMetadata('providers', ChatModule) ?? [];
|
|
expect(providers.map(providerToken)).toContain(ChatRuntimeRouter);
|
|
});
|
|
|
|
it('imports the real HarnessModule into the production ChatModule (registry source, not a test double)', () => {
|
|
const imports: unknown[] = Reflect.getMetadata('imports', ChatModule) ?? [];
|
|
expect(imports.map(resolveImport)).toContain(HarnessModule);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Task Five, Step Two — group 1c (bounded real-`ChatModule` boot).
|
|
*
|
|
* Scrappy adjudication d67d2b (option c): boot the ACTUAL production `ChatModule` as the SUT and
|
|
* assert the exclusive router resolves THROUGH it — the single-graph proof group 1 (router over the
|
|
* real registry) and group 1b (production-module metadata) each cover only a half of. The heavy,
|
|
* UNRELATED cycle is the only thing bounded away, per the established isolation pattern in
|
|
* `apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts`:
|
|
* - `CommandsModule` (drags the Commands <-> Reload <-> Chat forwardRef cycle plus GC/Mcp/queue)
|
|
* is replaced wholesale with an empty module via `.overrideModule(...).useModule(...)`;
|
|
* - `ChatGateway` (10-arg constructor, an HTTP/socket concern never exercised here) is replaced
|
|
* with an inert value;
|
|
* - the sole legacy-controller dependency, `AgentService`, is supplied by a tiny `@Global()` stub;
|
|
* - the HTTP-only `AuthGuard` is stubbed.
|
|
* Nothing about the router, `HarnessModule`, the registry, or the conversation-service binding is
|
|
* faked in the production-legacy case — those are retrieved from the REAL `ChatModule` graph. Mode
|
|
* is driven only through the production `CHAT_HARNESS_RUNTIME` env contract (`resolveChatRuntimeMode`).
|
|
*
|
|
* Red-first: today `ChatModule` neither imports `HarnessModule` nor provides `ChatRuntimeRouter`, so
|
|
* the booted graph contains no router/registry/conversation-service tokens. `init()` may resolve
|
|
* (there is no router lifecycle hook yet to reject), so every case fails on the MISSING actual
|
|
* router/registry/service wiring — not on unrelated DI, which is bounded away. GREEN at Step Three
|
|
* once `ChatModule` imports `HarnessModule`, provides the exclusive router, and binds the
|
|
* conversation-service token (defaulting to the unavailable sentinel).
|
|
*/
|
|
describe('ChatModule bounded real boot (Task Five, Step Two group 1c)', () => {
|
|
// The unrelated heavy cycle, replaced wholesale — not stubbed provider-by-provider.
|
|
@Module({})
|
|
class EmptyCommandsModule {}
|
|
|
|
// The ONLY genuine legacy dependency of the real ChatController, supplied inertly and globally so
|
|
// the pre-refactor controller instantiates without dragging AgentModule into the graph.
|
|
@Global()
|
|
@Module({
|
|
providers: [{ provide: AgentService, useValue: {} }],
|
|
exports: [AgentService],
|
|
})
|
|
class LegacyControllerDepsModule {}
|
|
|
|
const ORIGINAL_RUNTIME_ENV = process.env['CHAT_HARNESS_RUNTIME'];
|
|
afterEach(() => {
|
|
if (ORIGINAL_RUNTIME_ENV === undefined) delete process.env['CHAT_HARNESS_RUNTIME'];
|
|
else process.env['CHAT_HARNESS_RUNTIME'] = ORIGINAL_RUNTIME_ENV;
|
|
});
|
|
|
|
/**
|
|
* Boot the real ChatModule with only the unrelated cycle bounded away. `mode` is set through the
|
|
* genuine production env contract before providers instantiate. The optional overrides replace
|
|
* the registry / conversation-service the router injects, exercising the pi-rpc precondition
|
|
* branches through the ACTUAL module (they are no-ops today because those tokens are not yet in
|
|
* the graph — which is exactly why the router-retrieval assertions go red).
|
|
*/
|
|
async function bootChatModule(
|
|
mode: ChatRuntimeMode,
|
|
overrides: {
|
|
registryAdapters?: readonly string[];
|
|
conversationService?: HarnessConversationServiceBinding;
|
|
} = {},
|
|
): Promise<TestingModule> {
|
|
if (mode === 'pi-rpc') process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
|
|
else delete process.env['CHAT_HARNESS_RUNTIME'];
|
|
|
|
let builder = Test.createTestingModule({
|
|
imports: [LegacyControllerDepsModule, ChatModule],
|
|
})
|
|
.overrideModule(CommandsModule)
|
|
.useModule(EmptyCommandsModule)
|
|
.overrideProvider(ChatGateway)
|
|
.useValue({})
|
|
.overrideGuard(AuthGuard)
|
|
.useValue({ canActivate: () => true });
|
|
|
|
if (overrides.registryAdapters) {
|
|
builder = builder
|
|
.overrideProvider(HARNESS_REGISTRY)
|
|
.useValue(registryWith(overrides.registryAdapters));
|
|
}
|
|
if (overrides.conversationService !== undefined) {
|
|
builder = builder
|
|
.overrideProvider(HARNESS_CONVERSATION_SERVICE)
|
|
.useValue(overrides.conversationService);
|
|
}
|
|
return builder.compile();
|
|
}
|
|
|
|
// Capture an init rejection without letting a resolved init masquerade as success.
|
|
const initError = (moduleRef: TestingModule): Promise<unknown> =>
|
|
moduleRef.init().then(
|
|
() => new Error('module init resolved but the contract requires it to reject'),
|
|
(err: unknown) => err,
|
|
);
|
|
|
|
it('legacy mode: the actual router resolves the embedded runtime, the actual registry is empty, and the conversation-service token is the unavailable sentinel', async () => {
|
|
const moduleRef = await bootChatModule('legacy');
|
|
try {
|
|
await moduleRef.init();
|
|
const router = moduleRef.get(ChatRuntimeRouter, { strict: false });
|
|
expect(router.active.kind).toBe('embedded');
|
|
|
|
const registry = moduleRef.get<HarnessRegistry>(HARNESS_REGISTRY, { strict: false });
|
|
expect(registry).toBeInstanceOf(HarnessRegistry);
|
|
expect(registry.list()).toHaveLength(0);
|
|
|
|
const service = moduleRef.get<HarnessConversationServiceBinding>(
|
|
HARNESS_CONVERSATION_SERVICE,
|
|
{
|
|
strict: false,
|
|
},
|
|
);
|
|
expect(service).toBe(HARNESS_CONVERSATION_SERVICE_UNAVAILABLE);
|
|
} finally {
|
|
await moduleRef.close();
|
|
}
|
|
});
|
|
|
|
it('pi-rpc mode over the REAL empty registry fails closed at init with the typed adapter-unavailable error', async () => {
|
|
const moduleRef = await bootChatModule('pi-rpc');
|
|
try {
|
|
const err = await initError(moduleRef);
|
|
expect(err).toBeInstanceOf(ChatRuntimeUnavailableError);
|
|
expect((err as ChatRuntimeUnavailableError).reason).toBe('adapter_unavailable');
|
|
expect((err as ChatRuntimeUnavailableError).code).toBe('runtime_unsupported');
|
|
} finally {
|
|
await closeIgnoringFailedInit(moduleRef);
|
|
}
|
|
});
|
|
|
|
it('pi-rpc mode with a pi adapter present but the sentinel conversation service fails closed with the typed conversation-service-unavailable error', async () => {
|
|
const moduleRef = await bootChatModule('pi-rpc', {
|
|
registryAdapters: ['pi'],
|
|
conversationService: HARNESS_CONVERSATION_SERVICE_UNAVAILABLE,
|
|
});
|
|
try {
|
|
const err = await initError(moduleRef);
|
|
expect(err).toBeInstanceOf(ChatRuntimeUnavailableError);
|
|
expect((err as ChatRuntimeUnavailableError).reason).toBe('conversation_service_unavailable');
|
|
expect((err as ChatRuntimeUnavailableError).code).toBe('runtime_unsupported');
|
|
} finally {
|
|
await closeIgnoringFailedInit(moduleRef);
|
|
}
|
|
});
|
|
|
|
it('pi-rpc mode with a pi adapter and a bound conversation service: the actual router selects the harness runtime', async () => {
|
|
const moduleRef = await bootChatModule('pi-rpc', {
|
|
registryAdapters: ['pi'],
|
|
conversationService: boundConversationService,
|
|
});
|
|
try {
|
|
await moduleRef.init();
|
|
const router = moduleRef.get(ChatRuntimeRouter, { strict: false });
|
|
expect(router.active.kind).toBe('harness');
|
|
} finally {
|
|
await moduleRef.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Task Five, Step Two — group 2 (WHOLE production `AppModule` boot, legacy end-to-end wiring).
|
|
*
|
|
* The groups above bound away the heavy cycle to isolate the router. This group instead boots the
|
|
* ACTUAL production `AppModule` (the exact graph `main.ts` runs) in the default LEGACY chat-runtime
|
|
* mode, overriding ONLY the storage/network side-effect adapters so the boot is bounded and offline
|
|
* — never the chat/router/harness/reload/commands surface under test. The bounded fakes are exactly
|
|
* the disk/network leaves:
|
|
* - `ProviderService` (the #1 hang risk: its real `onModuleInit` starts an unref'd health-check
|
|
* `setInterval` and fetches Ollama over HTTP) → inert no-op instance;
|
|
* - `DB_HANDLE`/`DB` → a fake Drizzle-shaped handle that satisfies `runPgliteMigrations` (the local
|
|
* tier's `DatabaseModule.onModuleInit`) AND `DefaultRoutingRulesSeed.onModuleInit` (which reads a
|
|
* system-rule count — the fake reports rules already present so the seed insert is skipped),
|
|
* opening no real database;
|
|
* - `STORAGE_ADAPTER`/`MEMORY`/`MEMORY_ADAPTER`/`AUTH`/`BRAIN`/`LOG_SERVICE` → inert fakes so no
|
|
* storage/auth/log backend is contacted.
|
|
* Local tier (the repo's `mosaic.config.json`) already disables BullMQ/Redis and the queue handles;
|
|
* Discord/Telegram/MCP plugins are env-gated and disarmed by deleting their tokens. Nothing about the
|
|
* router, `ChatModule`, `HarnessModule`, or `ChatGateway` is faked — those come from the REAL graph.
|
|
*
|
|
* The boot+init MUST SUCCEED cleanly (proven by `beforeAll` completing and the ChatGateway test
|
|
* passing). Red-first: on this branch `ChatRuntimeRouter` is registered in NO module (ChatModule
|
|
* provides only ChatGateway), so `moduleRef.get(ChatRuntimeRouter)` throws `UnknownElementException`
|
|
* — a WIRING gap, NOT an init failure. That single retrieval is the intended behavioural red; it
|
|
* flips green once Step Three registers the exclusive router. The ChatGateway retrieval and its
|
|
* browser-facing method surface are asserted alongside and pass today, pinning that the boot itself
|
|
* is healthy so the router failure cannot be mistaken for a mis-shaped fake or an unbounded side
|
|
* effect.
|
|
*/
|
|
describe('AppModule production boot — legacy ChatRuntimeRouter wiring (Task Five, Step Two group 2)', () => {
|
|
// A Drizzle-shaped fake that satisfies both DB consumers reached during a local-tier init:
|
|
// • runPgliteMigrations(): reads handle.db.$client.exec + handle.db.execute(SELECT hashes);
|
|
// exec is a no-op and execute yields an empty ledger, so migration statements no-op through.
|
|
// • DefaultRoutingRulesSeed.seedDefaultRules(): db.select().from().where() must resolve to a
|
|
// row set — we report a non-zero system-rule count so the seeding INSERT branch is skipped.
|
|
const fakeDb = {
|
|
$client: { exec: async (): Promise<void> => {} },
|
|
execute: async (): Promise<{ rows: unknown[] }> => ({ rows: [] }),
|
|
select: () => ({
|
|
from: () => ({
|
|
where: async (): Promise<Array<{ count: number }>> => [{ count: 1 }],
|
|
}),
|
|
}),
|
|
insert: () => ({ values: async (): Promise<void> => {} }),
|
|
};
|
|
const fakeDbHandle = { db: fakeDb, close: async (): Promise<void> => {} };
|
|
const fakeStorageAdapter = {
|
|
name: 'fake',
|
|
migrate: async (): Promise<void> => {},
|
|
close: async (): Promise<void> => {},
|
|
};
|
|
// Inert stand-in for the real ProviderService: no health-check interval, no Ollama fetch.
|
|
const fakeProviderService = {
|
|
onModuleInit: async (): Promise<void> => {},
|
|
onModuleDestroy: (): void => {},
|
|
getRegistry: () => ({
|
|
getAvailable: () => [],
|
|
getAll: () => [],
|
|
find: () => undefined,
|
|
}),
|
|
getDefaultModel: () => undefined,
|
|
listAvailableModels: () => [],
|
|
listProviders: () => [],
|
|
getAdapter: () => undefined,
|
|
getProvidersHealth: () => [],
|
|
};
|
|
const fakeBrain = { conversations: {}, agents: {} };
|
|
|
|
const BOOT_TIMEOUT_MS = 120_000;
|
|
|
|
let moduleRef: TestingModule;
|
|
let envSnapshot: Record<string, string | undefined>;
|
|
|
|
beforeAll(async () => {
|
|
envSnapshot = { ...process.env };
|
|
// Env hygiene: disarm the network-facing plugins/adapters and pin the legacy runtime mode.
|
|
delete process.env['DATABASE_URL'];
|
|
delete process.env['DISCORD_BOT_TOKEN'];
|
|
delete process.env['TELEGRAM_BOT_TOKEN'];
|
|
delete process.env['MCP_SERVERS'];
|
|
delete process.env['CHAT_HARNESS_RUNTIME']; // resolveChatRuntimeMode → 'legacy'
|
|
process.env['MOSAIC_STORAGE_TIER'] = 'local';
|
|
|
|
moduleRef = await Test.createTestingModule({ imports: [AppModule] })
|
|
// Storage/network side-effect adapters ONLY — never the router/chat/harness surface under test.
|
|
.overrideProvider('DB_HANDLE')
|
|
.useValue(fakeDbHandle)
|
|
.overrideProvider('DB')
|
|
.useValue(fakeDb)
|
|
.overrideProvider('STORAGE_ADAPTER')
|
|
.useValue(fakeStorageAdapter)
|
|
.overrideProvider('AUTH')
|
|
.useValue({})
|
|
.overrideProvider('BRAIN')
|
|
.useValue(fakeBrain)
|
|
.overrideProvider('LOG_SERVICE')
|
|
.useValue({})
|
|
.overrideProvider('MEMORY')
|
|
.useValue({})
|
|
.overrideProvider('MEMORY_ADAPTER')
|
|
.useValue({})
|
|
.overrideProvider(ProviderService)
|
|
.useValue(fakeProviderService)
|
|
.compile();
|
|
|
|
// The boot itself MUST succeed cleanly — a rejection here is a bounding failure, not the red.
|
|
await moduleRef.init();
|
|
}, BOOT_TIMEOUT_MS);
|
|
|
|
afterAll(async () => {
|
|
if (moduleRef) await moduleRef.close();
|
|
for (const key of Object.keys(process.env)) {
|
|
if (!(key in envSnapshot)) delete process.env[key];
|
|
}
|
|
for (const [key, value] of Object.entries(envSnapshot)) {
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
});
|
|
|
|
// Passes TODAY: the real ChatGateway is provided by the real ChatModule and its browser-facing
|
|
// surface exists. This pins that the whole-AppModule boot came up healthy, so the router failure
|
|
// below is unambiguously a wiring gap and not a mis-shaped fake or an unbounded side effect.
|
|
it('boots the whole AppModule and exposes the real ChatGateway with its browser-facing methods', () => {
|
|
const gateway = moduleRef.get(ChatGateway, { strict: false });
|
|
expect(typeof gateway.broadcastReload).toBe('function');
|
|
expect(typeof gateway.getModelOverride).toBe('function');
|
|
expect(typeof gateway.setModelOverride).toBe('function');
|
|
expect(typeof gateway.broadcastSessionInfo).toBe('function');
|
|
});
|
|
|
|
// RED TODAY: ChatRuntimeRouter is registered in no module on this branch, so this retrieval throws
|
|
// UnknownElementException — the intended red-first wiring failure. GREEN once Step Three registers
|
|
// the exclusive router in the production graph, where legacy mode resolves the embedded runtime.
|
|
it('resolves the exclusive ChatRuntimeRouter to the embedded runtime in legacy mode', () => {
|
|
const router = moduleRef.get(ChatRuntimeRouter, { strict: false });
|
|
expect(router.active.kind).toBe('embedded');
|
|
});
|
|
});
|