Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions __tests__/hostApi/debugHook.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { createTransport } from '@novasamatech/host-api';
import { createAccountsProvider } from '@novasamatech/host-api-wrapper';
import type { HostApiDebugMessageEvent } from '@novasamatech/host-container';
import { createContainer, onHostApiDebugMessage } from '@novasamatech/host-container';

import { describe, expect, it, vi } from 'vitest';

import { createHostApiProviders } from './__mocks__/hostApiProviders.js';

function setup(productId?: string) {
const providers = createHostApiProviders();
const container = createContainer(providers.host, productId !== undefined ? { productId } : {});
const sdkTransport = createTransport(providers.sdk);
const accountsProvider = createAccountsProvider(sdkTransport);
return { container, providers, sdkTransport, accountsProvider };
}

describe('host-container debug hook', () => {
it('tags container-level events with the productId from createContainer', async () => {
const { container, accountsProvider } = setup('product.alpha');

const events: HostApiDebugMessageEvent[] = [];
container.onDebugMessage(event => events.push(event));

// any flow that produces traffic — the default unhandled getUserId triggers a request/response
await accountsProvider.getProductAccount('product.alpha', 0);

expect(events.length).toBeGreaterThan(0);
for (const event of events) {
expect(event.productId).toBe('product.alpha');
}
});

it('leaves productId undefined when none is supplied', async () => {
const { container, accountsProvider } = setup();

const events: HostApiDebugMessageEvent[] = [];
container.onDebugMessage(event => events.push(event));

await accountsProvider.getProductAccount('product.alpha', 0);

expect(events.length).toBeGreaterThan(0);
for (const event of events) {
expect(event.productId).toBeUndefined();
}
});

it('global onHostApiDebugMessage aggregates events across multiple containers tagged by productId', async () => {
const a = setup('product.alpha');
const b = setup('product.beta');

const seen: HostApiDebugMessageEvent[] = [];
const unsubscribe = onHostApiDebugMessage(event => seen.push(event));

await a.accountsProvider.getProductAccount('product.alpha', 0);
await b.accountsProvider.getProductAccount('product.beta', 0);

const ids = new Set(seen.map(e => e.productId));
expect(ids.has('product.alpha')).toBe(true);
expect(ids.has('product.beta')).toBe(true);

unsubscribe();
a.container.dispose();
b.container.dispose();
});

it('container-level and global hook observe the same events for one container', async () => {
const { container, accountsProvider } = setup('product.alpha');

const containerEvents: HostApiDebugMessageEvent[] = [];
const globalEvents: HostApiDebugMessageEvent[] = [];

container.onDebugMessage(event => containerEvents.push(event));
const unsubscribeGlobal = onHostApiDebugMessage(event => {
if (event.productId === 'product.alpha') globalEvents.push(event);
});

await accountsProvider.getProductAccount('product.alpha', 0);

expect(containerEvents.length).toBe(globalEvents.length);
expect(containerEvents.length).toBeGreaterThan(0);
expect(containerEvents).toEqual(globalEvents);

unsubscribeGlobal();
});

it('disposes the global-bus forwarder when the container is disposed', async () => {
const { container, accountsProvider } = setup('product.gamma');

const seen = vi.fn<(e: HostApiDebugMessageEvent) => void>();
const unsubscribe = onHostApiDebugMessage(event => {
if (event.productId === 'product.gamma') seen(event);
});

await accountsProvider.getProductAccount('product.gamma', 0);
const beforeDispose = seen.mock.calls.length;
expect(beforeDispose).toBeGreaterThan(0);

container.dispose();
seen.mockClear();

// a fresh, separately tagged container should not surface as 'product.gamma'
const fresh = setup('product.delta');
await fresh.accountsProvider.getProductAccount('product.delta', 0);

expect(seen).not.toHaveBeenCalled();

unsubscribe();
fresh.container.dispose();
});

it('container-level unsubscribe stops further events without affecting the global bus', async () => {
const { container, accountsProvider } = setup('product.alpha');

const containerListener = vi.fn<(e: HostApiDebugMessageEvent) => void>();
const globalListener = vi.fn<(e: HostApiDebugMessageEvent) => void>();
const unsubscribeContainer = container.onDebugMessage(containerListener);
const unsubscribeGlobal = onHostApiDebugMessage(event => {
if (event.productId === 'product.alpha') globalListener(event);
});

await accountsProvider.getProductAccount('product.alpha', 0);
expect(containerListener).toHaveBeenCalled();
expect(globalListener).toHaveBeenCalled();

unsubscribeContainer();
containerListener.mockClear();
globalListener.mockClear();

await accountsProvider.getProductAccount('product.alpha', 0);
expect(containerListener).not.toHaveBeenCalled();
expect(globalListener).toHaveBeenCalled();

unsubscribeGlobal();
container.dispose();
});
});
34 changes: 4 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/host-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export type {
ConnectionStatus,
DebugMessageEvent,
HostApiMethod,
Logger,
RequestHandler,
Subscription,
SubscriptionHandler,
Transport,
} from './types.js';
export type { MessagePayloadSchema } from './protocol/messageCodec.js';
export type { Provider } from './provider.js';
export { createRequestId } from './helpers.js';

Expand Down
Loading