160 lines
5.1 KiB
TypeScript
160 lines
5.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { InMemoryDurableSessionStore } from '@mosaicstack/agent';
|
|
import {
|
|
createDiscordIngressEnvelope,
|
|
type DiscordIngressPayload,
|
|
} from '@mosaicstack/discord-plugin';
|
|
import { InteractionController } from '../../agent/interaction.controller.js';
|
|
import { RuntimeProviderService } from '../../agent/runtime-provider-registry.service.js';
|
|
import { TessDurableSessionService } from '../../agent/tess-durable-session.service.js';
|
|
import { ChatGateway } from '../../chat/chat.gateway.js';
|
|
import { CommandAuthorizationService } from '../../commands/command-authorization.service.js';
|
|
|
|
const SERVICE_TOKEN = 'test-discord-service-token';
|
|
const envKeys = [
|
|
'DISCORD_SERVICE_TOKEN',
|
|
'DISCORD_SERVICE_TENANT_ID',
|
|
'DISCORD_INTERACTION_BINDINGS',
|
|
'DISCORD_ALLOWED_GUILD_IDS',
|
|
'DISCORD_ALLOWED_CHANNEL_IDS',
|
|
'DISCORD_ALLOWED_USER_IDS',
|
|
'MOSAIC_AGENT_NAME',
|
|
] as const;
|
|
const priorEnv = new Map<string, string | undefined>();
|
|
|
|
function payload(content: string, messageId: string, correlationId: string): DiscordIngressPayload {
|
|
return {
|
|
content,
|
|
messageId,
|
|
correlationId,
|
|
guildId: 'guild-1',
|
|
channelId: 'channel-1',
|
|
userId: 'discord-admin-1',
|
|
conversationId: 'conversation-1',
|
|
};
|
|
}
|
|
|
|
function authorization(): CommandAuthorizationService {
|
|
const entries = new Map<string, string>();
|
|
return new CommandAuthorizationService(
|
|
{
|
|
select: () => ({
|
|
from: () => ({ where: () => ({ limit: async () => [{ role: 'admin' }] }) }),
|
|
}),
|
|
} as never,
|
|
{
|
|
get: async (key: string) => entries.get(key) ?? null,
|
|
set: async (key: string, value: string) => entries.set(key, value),
|
|
del: async (key: string) => Number(entries.delete(key)),
|
|
},
|
|
);
|
|
}
|
|
|
|
describe('Tess Discord/CLI durable-session integration', () => {
|
|
afterEach(() => {
|
|
for (const key of envKeys) {
|
|
const value = priorEnv.get(key);
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
priorEnv.clear();
|
|
});
|
|
|
|
it('enrolls through the CLI surface then resolves the same durable session from Discord', async () => {
|
|
for (const key of envKeys) priorEnv.set(key, process.env[key]);
|
|
process.env['MOSAIC_AGENT_NAME'] = 'Nova';
|
|
process.env['DISCORD_SERVICE_TOKEN'] = SERVICE_TOKEN;
|
|
process.env['DISCORD_SERVICE_TENANT_ID'] = 'tenant-1';
|
|
process.env['DISCORD_ALLOWED_GUILD_IDS'] = 'guild-1';
|
|
process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-1';
|
|
process.env['DISCORD_ALLOWED_USER_IDS'] = 'discord-admin-1';
|
|
process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
|
|
{
|
|
instanceId: 'Nova',
|
|
guildId: 'guild-1',
|
|
channelId: 'channel-1',
|
|
pairedUsers: {
|
|
'discord-admin-1': { role: 'admin', mosaicUserId: 'mosaic-admin-1' },
|
|
},
|
|
},
|
|
]);
|
|
|
|
const durable = new TessDurableSessionService(
|
|
new InMemoryDurableSessionStore() as never,
|
|
{} as never,
|
|
);
|
|
const enrollmentRuntime = {
|
|
listSessions: vi.fn().mockResolvedValue([{ id: 'runtime-1' }]),
|
|
};
|
|
const controller = new InteractionController(enrollmentRuntime as never, durable);
|
|
await controller.enroll(
|
|
'Nova',
|
|
'conversation-1',
|
|
{ providerId: 'fleet', runtimeSessionId: 'runtime-1' },
|
|
{ id: 'mosaic-admin-1', tenantId: 'tenant-1' },
|
|
'cli-enrollment-correlation',
|
|
);
|
|
|
|
const authz = authorization();
|
|
const terminated = vi.fn().mockResolvedValue(undefined);
|
|
const runtime = new RuntimeProviderService(
|
|
{
|
|
require: () => ({
|
|
capabilities: async () => ({ supported: ['session.terminate'] }),
|
|
terminate: terminated,
|
|
}),
|
|
} as never,
|
|
{ record: async () => undefined } as never,
|
|
{
|
|
consume: (approvalId, action) =>
|
|
authz.consumeRuntimeTerminationApproval(approvalId, action),
|
|
},
|
|
);
|
|
const gateway = new ChatGateway(
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
authz,
|
|
runtime,
|
|
durable,
|
|
);
|
|
const client = { data: { discordService: true }, emit: vi.fn() };
|
|
|
|
await gateway.handleDiscordApproval(
|
|
client as never,
|
|
createDiscordIngressEnvelope(
|
|
payload('/approve', 'approve-1', 'discord-approve-correlation'),
|
|
SERVICE_TOKEN,
|
|
),
|
|
);
|
|
const approval = client.emit.mock.calls.find(
|
|
([event]) => event === 'discord:approval',
|
|
)?.[1] as {
|
|
approvalId: string;
|
|
success: boolean;
|
|
};
|
|
expect(approval.success).toBe(true);
|
|
|
|
await gateway.handleDiscordStop(
|
|
client as never,
|
|
createDiscordIngressEnvelope(
|
|
payload(`/stop ${approval.approvalId}`, 'stop-1', 'discord-stop-correlation'),
|
|
SERVICE_TOKEN,
|
|
),
|
|
);
|
|
|
|
expect(terminated).toHaveBeenCalledWith(
|
|
'runtime-1',
|
|
approval.approvalId,
|
|
expect.objectContaining({ actorId: 'mosaic-admin-1' }),
|
|
);
|
|
expect(client.emit).toHaveBeenCalledWith('discord:stop', {
|
|
correlationId: 'discord-stop-correlation',
|
|
success: true,
|
|
});
|
|
});
|
|
});
|