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>
97 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
});
|