183 lines
6.1 KiB
TypeScript
183 lines
6.1 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { RuntimeScope } from '@mosaicstack/types';
|
|
import {
|
|
MatrixNativeRuntimeTransport,
|
|
type MatrixFetchLike,
|
|
} from './matrix-native-runtime-transport.js';
|
|
|
|
const scope: RuntimeScope = {
|
|
actorId: 'operator-1',
|
|
tenantId: 'tenant-a',
|
|
channelId: 'cli',
|
|
correlationId: 'corr-1',
|
|
};
|
|
|
|
const bindings = [
|
|
{
|
|
id: 'native-1',
|
|
runtimeId: '@native-worker:example.test',
|
|
roomId: '!room:example.test',
|
|
remoteUserId: '@native-worker:example.test',
|
|
state: 'active' as const,
|
|
createdAt: '2026-07-13T00:00:00.000Z',
|
|
updatedAt: '2026-07-13T00:00:00.000Z',
|
|
},
|
|
];
|
|
|
|
function response(
|
|
status: number,
|
|
body: unknown = {},
|
|
): ReturnType<MatrixFetchLike> extends Promise<infer T> ? T : never {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
json: async () => body,
|
|
text: async () => JSON.stringify(body),
|
|
} as never;
|
|
}
|
|
|
|
function transport(fetchImpl: MatrixFetchLike): MatrixNativeRuntimeTransport {
|
|
return new MatrixNativeRuntimeTransport({
|
|
homeserverUrl: 'https://matrix.example.test/base',
|
|
accessToken: 'test-token',
|
|
userId: '@mosaic:example.test',
|
|
sessions: bindings,
|
|
fetchImpl,
|
|
});
|
|
}
|
|
|
|
describe('MatrixNativeRuntimeTransport', (): void => {
|
|
it('uses only configured session-to-room bindings and idempotency-derived Matrix transactions', async (): Promise<void> => {
|
|
const fetchImpl = vi
|
|
.fn<MatrixFetchLike>()
|
|
.mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
|
|
.mockResolvedValueOnce(response(200, { event_id: '$sent' }));
|
|
const matrix = transport(fetchImpl);
|
|
|
|
await matrix.send('native-1', { content: 'hello', idempotencyKey: 'message-1' }, scope);
|
|
|
|
expect(fetchImpl).toHaveBeenNthCalledWith(
|
|
1,
|
|
'https://matrix.example.test/base/_matrix/client/v3/account/whoami',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({ Authorization: 'Bearer test-token' }),
|
|
}),
|
|
);
|
|
const [url, init] = fetchImpl.mock.calls[1]!;
|
|
expect(url).toMatch(
|
|
/^https:\/\/matrix\.example\.test\/base\/_matrix\/client\/v3\/rooms\/!room%3Aexample\.test\/send\/m\.room\.message\/mosaic-send-[A-Za-z0-9_-]+$/,
|
|
);
|
|
expect(init?.method).toBe('PUT');
|
|
expect(JSON.parse(init?.body ?? '')).toEqual({
|
|
msgtype: 'm.text',
|
|
body: 'hello',
|
|
'mosaic.runtime.v1': {
|
|
session_id: 'native-1',
|
|
runtime_id: '@native-worker:example.test',
|
|
actor_id: 'operator-1',
|
|
tenant_id: 'tenant-a',
|
|
channel_id: 'cli',
|
|
correlation_id: 'corr-1',
|
|
idempotency_key: 'message-1',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('sends a termination only to the bound room with the exact approval reference', async (): Promise<void> => {
|
|
const fetchImpl = vi
|
|
.fn<MatrixFetchLike>()
|
|
.mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
|
|
.mockResolvedValueOnce(response(200, { event_id: '$terminated' }));
|
|
const matrix = transport(fetchImpl);
|
|
|
|
await matrix.terminate('native-1', 'approval-1', scope);
|
|
|
|
const [url, init] = fetchImpl.mock.calls[1]!;
|
|
expect(url).toMatch(
|
|
/^https:\/\/matrix\.example\.test\/base\/_matrix\/client\/v3\/rooms\/!room%3Aexample\.test\/send\/mosaic\.runtime\.terminate\/mosaic-terminate-[A-Za-z0-9_-]+$/,
|
|
);
|
|
expect(JSON.parse(init?.body ?? '')).toEqual({
|
|
session_id: 'native-1',
|
|
runtime_id: '@native-worker:example.test',
|
|
actor_id: 'operator-1',
|
|
tenant_id: 'tenant-a',
|
|
channel_id: 'cli',
|
|
correlation_id: 'corr-1',
|
|
approval_ref: 'approval-1',
|
|
});
|
|
});
|
|
|
|
it('fails closed when Matrix whoami does not match the configured native identity', async (): Promise<void> => {
|
|
const fetchImpl = vi
|
|
.fn<MatrixFetchLike>()
|
|
.mockResolvedValue(response(200, { user_id: '@other:example.test' }));
|
|
const matrix = transport(fetchImpl);
|
|
|
|
await expect(matrix.listSessions(scope)).rejects.toMatchObject({ code: 'forbidden' });
|
|
});
|
|
|
|
it('rejects unbound session IDs without using caller-supplied Matrix room data', async (): Promise<void> => {
|
|
const fetchImpl = vi
|
|
.fn<MatrixFetchLike>()
|
|
.mockResolvedValue(response(200, { user_id: '@mosaic:example.test' }));
|
|
const matrix = transport(fetchImpl);
|
|
|
|
await expect(matrix.verifySession('!attacker-room:example.test', scope)).rejects.toMatchObject({
|
|
code: 'not_found',
|
|
});
|
|
});
|
|
|
|
it('maps replay-cursor events from only the configured remote identity', async (): Promise<void> => {
|
|
const fetchImpl = vi
|
|
.fn<MatrixFetchLike>()
|
|
.mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
|
|
.mockResolvedValueOnce(
|
|
response(200, {
|
|
next_batch: 'next-cursor',
|
|
rooms: {
|
|
join: {
|
|
'!room:example.test': {
|
|
timeline: {
|
|
events: [
|
|
{
|
|
type: 'mosaic.runtime.event',
|
|
sender: '@intruder:example.test',
|
|
content: { 'mosaic.runtime.v1': { type: 'message.delta', content: 'nope' } },
|
|
},
|
|
{
|
|
type: 'mosaic.runtime.event',
|
|
sender: '@native-worker:example.test',
|
|
origin_server_ts: 1_784_246_400_000,
|
|
content: {
|
|
'mosaic.runtime.v1': {
|
|
session_id: 'native-1',
|
|
type: 'message.delta',
|
|
content: 'accepted',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const matrix = transport(fetchImpl);
|
|
|
|
const events = [];
|
|
for await (const event of matrix.stream('native-1', 'prior-cursor', scope)) events.push(event);
|
|
|
|
expect(events).toEqual([
|
|
{
|
|
type: 'message.delta',
|
|
sessionId: 'native-1',
|
|
cursor: 'next-cursor',
|
|
occurredAt: '2026-07-17T00:00:00.000Z',
|
|
content: 'accepted',
|
|
},
|
|
]);
|
|
expect(fetchImpl.mock.calls[1]?.[0]).toContain('since=prior-cursor');
|
|
});
|
|
});
|