Files
stack/apps/api/src/bridge/bridge.module.spec.ts
Jason Woltje 4ac21d1a3a feat(#170): Implement mosaic-bridge module for Discord
Created the mosaic-bridge module to enable Discord integration for
chat-based control of Mosaic Stack. This module provides the foundation
for receiving commands via Discord and forwarding them to the stitcher
for job orchestration.

Key Features:
- Discord bot connection and authentication
- Command parsing (@mosaic fix, status, cancel, verbose, quiet, help)
- Thread management for job updates
- Chat provider interface for future platform extensibility
- Noise management (low/medium/high verbosity levels)

Implementation Details:
- Created IChatProvider interface for platform abstraction
- Implemented DiscordService with Discord.js
- Basic command parsing (detailed parsing in #171)
- Thread creation for job-specific updates
- Configuration via environment variables

Commands Supported:
- @mosaic fix <issue> - Start job for issue
- @mosaic status <job> - Get job status (placeholder)
- @mosaic cancel <job> - Cancel running job (placeholder)
- @mosaic verbose <job> - Stream full logs (placeholder)
- @mosaic quiet - Reduce notifications (placeholder)
- @mosaic help - Show available commands

Testing:
- 23/23 tests passing (TDD approach)
- Unit tests for Discord service
- Module integration tests
- 100% coverage of critical paths

Quality Gates:
- Typecheck: PASSED
- Lint: PASSED
- Build: PASSED
- Tests: PASSED (23/23)

Environment Variables:
- DISCORD_BOT_TOKEN - Bot authentication token
- DISCORD_GUILD_ID - Server/Guild ID (optional)
- DISCORD_CONTROL_CHANNEL_ID - Channel for commands

Files Created:
- apps/api/src/bridge/bridge.module.ts
- apps/api/src/bridge/discord/discord.service.ts
- apps/api/src/bridge/interfaces/chat-provider.interface.ts
- apps/api/src/bridge/index.ts
- Full test coverage

Dependencies Added:
- discord.js@latest

Next Steps:
- Issue #171: Implement detailed command parsing
- Issue #172: Add Herald integration for job updates
- Future: Add Slack, Matrix support via IChatProvider

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:26:40 -06:00

97 lines
2.6 KiB
TypeScript

import { Test, TestingModule } from "@nestjs/testing";
import { BridgeModule } from "./bridge.module";
import { DiscordService } from "./discord/discord.service";
import { StitcherService } from "../stitcher/stitcher.service";
import { PrismaService } from "../prisma/prisma.service";
import { BullMqService } from "../bullmq/bullmq.service";
import { describe, it, expect, beforeEach, vi } from "vitest";
// Mock discord.js
const mockReadyCallbacks: Array<() => void> = [];
const mockClient = {
login: vi.fn().mockImplementation(async () => {
mockReadyCallbacks.forEach((cb) => cb());
return Promise.resolve();
}),
destroy: vi.fn().mockResolvedValue(undefined),
on: vi.fn(),
once: vi.fn().mockImplementation((event: string, callback: () => void) => {
if (event === "ready") {
mockReadyCallbacks.push(callback);
}
}),
user: { tag: "TestBot#1234" },
channels: {
fetch: vi.fn(),
},
guilds: {
fetch: vi.fn(),
},
};
vi.mock("discord.js", () => {
return {
Client: class MockClient {
login = mockClient.login;
destroy = mockClient.destroy;
on = mockClient.on;
once = mockClient.once;
user = mockClient.user;
channels = mockClient.channels;
guilds = mockClient.guilds;
},
Events: {
ClientReady: "ready",
MessageCreate: "messageCreate",
Error: "error",
},
GatewayIntentBits: {
Guilds: 1 << 0,
GuildMessages: 1 << 9,
MessageContent: 1 << 15,
},
};
});
describe("BridgeModule", () => {
let module: TestingModule;
beforeEach(async () => {
// Set environment variables
process.env.DISCORD_BOT_TOKEN = "test-token";
process.env.DISCORD_GUILD_ID = "test-guild-id";
process.env.DISCORD_CONTROL_CHANNEL_ID = "test-channel-id";
// Clear ready callbacks
mockReadyCallbacks.length = 0;
module = await Test.createTestingModule({
imports: [BridgeModule],
})
.overrideProvider(PrismaService)
.useValue({})
.overrideProvider(BullMqService)
.useValue({})
.compile();
// Clear all mocks
vi.clearAllMocks();
});
it("should be defined", () => {
expect(module).toBeDefined();
});
it("should provide DiscordService", () => {
const discordService = module.get<DiscordService>(DiscordService);
expect(discordService).toBeDefined();
expect(discordService).toBeInstanceOf(DiscordService);
});
it("should provide StitcherService", () => {
const stitcherService = module.get<StitcherService>(StitcherService);
expect(stitcherService).toBeDefined();
expect(stitcherService).toBeInstanceOf(StitcherService);
});
});