test: Wired EventBridge into Daemon lifecycle with /gsd-verbose slash c…
- "packages/daemon/src/commands.ts" - "packages/daemon/src/discord-bot.ts" - "packages/daemon/src/daemon.ts" - "packages/daemon/src/index.ts" - "packages/daemon/src/discord-bot.test.ts" GSD-Task: S04/T04
This commit is contained in:
parent
f26ec3a55d
commit
6ef99ee727
5 changed files with 107 additions and 2 deletions
|
|
@ -35,6 +35,21 @@ export function buildCommands(): RESTPostAPIChatInputApplicationCommandsJSONBody
|
|||
.setName('gsd-stop')
|
||||
.setDescription('Stop a running GSD session')
|
||||
.toJSON(),
|
||||
new SlashCommandBuilder()
|
||||
.setName('gsd-verbose')
|
||||
.setDescription('Set event verbosity level for this channel')
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('level')
|
||||
.setDescription('Verbosity level')
|
||||
.setRequired(false)
|
||||
.addChoices(
|
||||
{ name: 'default', value: 'default' },
|
||||
{ name: 'verbose', value: 'verbose' },
|
||||
{ name: 'quiet', value: 'quiet' },
|
||||
),
|
||||
)
|
||||
.toJSON(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { Logger } from './logger.js';
|
|||
import { SessionManager } from './session-manager.js';
|
||||
import { scanForProjects } from './project-scanner.js';
|
||||
import { DiscordBot, validateDiscordConfig } from './discord-bot.js';
|
||||
import { EventBridge } from './event-bridge.js';
|
||||
|
||||
/**
|
||||
* Core daemon class — ties config + logger together with lifecycle management.
|
||||
|
|
@ -15,6 +16,7 @@ export class Daemon {
|
|||
private readonly onSigint: () => void;
|
||||
private sessionManager: SessionManager | undefined;
|
||||
private discordBot: DiscordBot | undefined;
|
||||
private eventBridge: EventBridge | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly config: DaemonConfig,
|
||||
|
|
@ -51,6 +53,25 @@ export class Daemon {
|
|||
sessionManager: this.sessionManager,
|
||||
});
|
||||
await this.discordBot.login();
|
||||
|
||||
// Wire up EventBridge after bot is ready
|
||||
const channelManager = this.discordBot.getChannelManager();
|
||||
const client = this.discordBot.getClient();
|
||||
if (channelManager && client) {
|
||||
this.eventBridge = new EventBridge({
|
||||
sessionManager: this.sessionManager,
|
||||
channelManager,
|
||||
client,
|
||||
config: this.config,
|
||||
logger: this.logger,
|
||||
ownerId: this.config.discord.owner_id,
|
||||
});
|
||||
this.discordBot.setEventBridge(this.eventBridge);
|
||||
this.eventBridge.start();
|
||||
this.logger.info('event bridge wired');
|
||||
} else {
|
||||
this.logger.warn('event bridge skipped — channel manager or client not available');
|
||||
}
|
||||
} catch (err) {
|
||||
// Log error but don't abort daemon startup — bot is optional
|
||||
this.logger.error('discord bot login failed', {
|
||||
|
|
@ -74,6 +95,11 @@ export class Daemon {
|
|||
return this.sessionManager;
|
||||
}
|
||||
|
||||
/** Accessor for the event bridge (available after start() with Discord configured). */
|
||||
getEventBridge(): EventBridge | undefined {
|
||||
return this.eventBridge;
|
||||
}
|
||||
|
||||
/** Idempotent shutdown: log, cleanup sessions, close logger, exit. */
|
||||
async shutdown(): Promise<void> {
|
||||
if (this.shuttingDown) return;
|
||||
|
|
@ -91,6 +117,12 @@ export class Daemon {
|
|||
this.keepaliveTimer = undefined;
|
||||
}
|
||||
|
||||
// Stop EventBridge before Discord bot destroy
|
||||
if (this.eventBridge) {
|
||||
await this.eventBridge.stop();
|
||||
this.eventBridge = undefined;
|
||||
}
|
||||
|
||||
// Destroy Discord bot before session cleanup
|
||||
if (this.discordBot) {
|
||||
await this.discordBot.destroy();
|
||||
|
|
|
|||
|
|
@ -440,11 +440,12 @@ describe('ChannelManager', () => {
|
|||
describe('buildCommands', () => {
|
||||
it('returns array with correct command names', () => {
|
||||
const commands = buildCommands();
|
||||
assert.equal(commands.length, 3);
|
||||
assert.equal(commands.length, 4);
|
||||
const names = commands.map((c) => c.name);
|
||||
assert.ok(names.includes('gsd-status'), 'should include gsd-status');
|
||||
assert.ok(names.includes('gsd-start'), 'should include gsd-start');
|
||||
assert.ok(names.includes('gsd-stop'), 'should include gsd-stop');
|
||||
assert.ok(names.includes('gsd-verbose'), 'should include gsd-verbose');
|
||||
});
|
||||
|
||||
it('each command has a description', () => {
|
||||
|
|
|
|||
|
|
@ -13,11 +13,12 @@ import {
|
|||
type Interaction,
|
||||
type Guild,
|
||||
} from 'discord.js';
|
||||
import type { DaemonConfig } from './types.js';
|
||||
import type { DaemonConfig, VerbosityLevel } from './types.js';
|
||||
import type { Logger } from './logger.js';
|
||||
import type { SessionManager } from './session-manager.js';
|
||||
import { ChannelManager } from './channel-manager.js';
|
||||
import { buildCommands, registerGuildCommands, formatSessionStatus } from './commands.js';
|
||||
import type { EventBridge } from './event-bridge.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure helpers — exported for testability
|
||||
|
|
@ -67,6 +68,7 @@ export class DiscordBot {
|
|||
private client: Client | null = null;
|
||||
private destroyed = false;
|
||||
private channelManager: ChannelManager | null = null;
|
||||
private eventBridge: EventBridge | null = null;
|
||||
|
||||
private readonly config: NonNullable<DaemonConfig['discord']>;
|
||||
private readonly logger: Logger;
|
||||
|
|
@ -171,6 +173,22 @@ export class DiscordBot {
|
|||
return this.channelManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying discord.js Client, or null if not logged in.
|
||||
* Used by Daemon to pass to EventBridge as BridgeClient.
|
||||
*/
|
||||
getClient(): Client | null {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the EventBridge reference so the bot can dispatch /gsd-verbose commands.
|
||||
* Called by Daemon after creating the EventBridge.
|
||||
*/
|
||||
setEventBridge(bridge: EventBridge): void {
|
||||
this.eventBridge = bridge;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private: interaction handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -212,6 +230,25 @@ export class DiscordBot {
|
|||
});
|
||||
});
|
||||
break;
|
||||
case 'gsd-verbose': {
|
||||
if (!this.eventBridge) {
|
||||
interaction.reply({ content: 'Event bridge not available.', ephemeral: true }).catch((err) => {
|
||||
this.logger.warn('gsd-verbose reply failed', {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
const level = (interaction.options.getString('level') ?? 'default') as VerbosityLevel;
|
||||
const channelId = interaction.channelId;
|
||||
this.eventBridge.getVerbosityManager().setLevel(channelId, level);
|
||||
interaction.reply({ content: `Verbosity set to **${level}** for this channel.`, ephemeral: true }).catch((err) => {
|
||||
this.logger.warn('gsd-verbose reply failed', {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
interaction.reply({ content: 'Unknown command', ephemeral: true }).catch((err) => {
|
||||
this.logger.warn('unknown command reply failed', {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export type {
|
|||
ProjectInfo,
|
||||
ProjectMarker,
|
||||
StartSessionOptions,
|
||||
FormattedEvent,
|
||||
VerbosityLevel,
|
||||
} from './types.js';
|
||||
export { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js';
|
||||
export { resolveConfigPath, loadConfig, validateConfig } from './config.js';
|
||||
|
|
@ -22,3 +24,21 @@ export type { DiscordBotOptions } from './discord-bot.js';
|
|||
export { ChannelManager, sanitizeChannelName } from './channel-manager.js';
|
||||
export type { ChannelManagerOptions } from './channel-manager.js';
|
||||
export { buildCommands, formatSessionStatus, registerGuildCommands } from './commands.js';
|
||||
export { EventBridge } from './event-bridge.js';
|
||||
export type { BridgeClient, EventBridgeOptions } from './event-bridge.js';
|
||||
export { MessageBatcher } from './message-batcher.js';
|
||||
export type { SendPayload, SendFn, BatcherLogger, BatcherOptions } from './message-batcher.js';
|
||||
export { VerbosityManager, shouldShowAtLevel } from './verbosity.js';
|
||||
export {
|
||||
formatToolStart,
|
||||
formatToolEnd,
|
||||
formatMessage,
|
||||
formatBlocker,
|
||||
formatCompletion,
|
||||
formatError,
|
||||
formatCostUpdate,
|
||||
formatSessionStarted,
|
||||
formatTaskTransition,
|
||||
formatGenericEvent,
|
||||
formatEvent,
|
||||
} from './event-formatter.js';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue