Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/plugins/telegram/commands/__tests__/doctor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { Bot, Context } from 'grammy'
import type { OpenACPCore } from '../../../../core/index.js'

const doctorMocks = vi.hoisted(() => {
const state = {
options: [] as unknown[],
runAll: vi.fn(),
DoctorEngine: undefined as unknown as ReturnType<typeof vi.fn>,
}
state.DoctorEngine = vi.fn(function DoctorEngineMock(options: unknown) {
state.options.push(options)
return { runAll: state.runAll }
})
return state
})

vi.mock('../../../../core/doctor/index.js', () => ({
DoctorEngine: doctorMocks.DoctorEngine,
}))

const instanceRoot = '/tmp/openacp-test/.openacp'

function makeCore(): OpenACPCore {
return {
instanceContext: { root: instanceRoot },
} as OpenACPCore
}

function makeReport(pendingFixes: unknown[] = []) {
return {
categories: [
{ name: 'Config', results: [{ status: 'pass', message: 'Config valid' }] },
],
pendingFixes,
summary: { passed: 1, warnings: 0, failed: 0, fixed: 0 },
}
}

function makeCtx(): Context {
return {
chat: { id: 123 },
reply: vi.fn().mockResolvedValue({ message_id: 456 }),
api: { editMessageText: vi.fn().mockResolvedValue(undefined) },
} as unknown as Context
}

describe('Telegram doctor command', () => {
beforeEach(() => {
doctorMocks.options = []
doctorMocks.DoctorEngine.mockClear()
doctorMocks.runAll.mockReset()
})

it('passes the active instance root to DoctorEngine', async () => {
doctorMocks.runAll.mockResolvedValue(makeReport())
const { handleDoctor } = await import('../doctor.js')

await handleDoctor(makeCtx(), makeCore())

expect(doctorMocks.DoctorEngine).toHaveBeenCalledWith({ dataDir: instanceRoot })
})

it('uses the active instance root when rerunning after a doctor fix', async () => {
const fix = vi.fn().mockResolvedValue({ success: true, message: 'fixed' })
doctorMocks.runAll
.mockResolvedValueOnce(makeReport([{ message: 'Missing config', fix }]))
.mockResolvedValueOnce(makeReport())

const { handleDoctor, setupDoctorCallbacks } = await import('../doctor.js')
const handlers: Array<{ pattern: RegExp | string; handler: (ctx: Context) => Promise<void> }> = []
const bot = {
callbackQuery: (pattern: RegExp | string, handler: (ctx: Context) => Promise<void>) => {
handlers.push({ pattern, handler })
},
}

await handleDoctor(makeCtx(), makeCore())
setupDoctorCallbacks(bot as unknown as Bot, makeCore())

const fixHandler = handlers.find(({ pattern }) => pattern instanceof RegExp)
expect(fixHandler).toBeDefined()

await fixHandler!.handler({
callbackQuery: {
data: 'm:doctor:fix:0',
message: { chat: { id: 123 }, message_id: 456 },
},
answerCallbackQuery: vi.fn().mockResolvedValue(undefined),
editMessageText: vi.fn().mockResolvedValue(undefined),
} as unknown as Context)

expect(fix).toHaveBeenCalledOnce()
expect(doctorMocks.options).toEqual([
{ dataDir: instanceRoot },
{ dataDir: instanceRoot },
])
})
})
11 changes: 6 additions & 5 deletions src/plugins/telegram/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Bot, Context } from "grammy";
import { InlineKeyboard } from "grammy";
import type { OpenACPCore } from "../../../core/index.js";
import { DoctorEngine } from "../../../core/doctor/index.js";
import type { DoctorReport, PendingFix } from "../../../core/doctor/types.js";
import { createChildLogger } from "../../../core/utils/log.js";
Expand Down Expand Up @@ -47,11 +48,11 @@ function escapeHtml(text: string): string {
* edits the message with the report. Pending fixes are stored per-message so
* the `m:doctor:fix:<index>` callbacks know which fixes to apply.
*/
export async function handleDoctor(ctx: Context): Promise<void> {
export async function handleDoctor(ctx: Context, core: OpenACPCore): Promise<void> {
const statusMsg = await ctx.reply("🩺 Running diagnostics...", { parse_mode: "HTML" });

try {
const engine = new DoctorEngine();
const engine = new DoctorEngine({ dataDir: core.instanceContext.root });
const report = await engine.runAll();
const { text, keyboard } = renderReport(report);

Expand All @@ -75,7 +76,7 @@ export async function handleDoctor(ctx: Context): Promise<void> {
}
}

export function setupDoctorCallbacks(bot: Bot): void {
export function setupDoctorCallbacks(bot: Bot, core: OpenACPCore): void {
bot.callbackQuery(/^m:doctor:fix:/, async (ctx) => {
const data = ctx.callbackQuery.data;
const index = parseInt(data.replace("m:doctor:fix:", ""), 10);
Expand All @@ -99,7 +100,7 @@ export function setupDoctorCallbacks(bot: Bot): void {
try {
const result = await pending.fix();
if (result.success) {
const engine = new DoctorEngine();
const engine = new DoctorEngine({ dataDir: core.instanceContext.root });
const report = await engine.runAll();
const { text, keyboard } = renderReport(report);

Expand All @@ -120,6 +121,6 @@ export function setupDoctorCallbacks(bot: Bot): void {

bot.callbackQuery("m:doctor", async (ctx) => {
try { await ctx.answerCallbackQuery(); } catch { /* */ }
await handleDoctor(ctx);
await handleDoctor(ctx, core);
});
}
2 changes: 1 addition & 1 deletion src/plugins/telegram/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function setupAllCallbacks(
setupSettingsCallbacks(bot, core, getAssistantSession ?? (() => undefined));

// Doctor handlers — must be before broad m: handler
setupDoctorCallbacks(bot);
setupDoctorCallbacks(bot, core);

// Tunnel callbacks — must be before broad m: handler
setupTunnelCallbacks(bot, core);
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/telegram/commands/telegram-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const TELEGRAM_OVERRIDES: Record<
agents: (ctx, core) => handleAgents(ctx, core),
model: (ctx, core) => handleModel(ctx, core),
sessions: (ctx, core) => handleTopics(ctx, core),
doctor: (ctx) => handleDoctor(ctx),
doctor: (ctx, core) => handleDoctor(ctx, core),
update: (ctx, core) => handleUpdate(ctx, core),
restart: (ctx, core) => handleRestart(ctx, core),
help: (ctx) => handleHelp(ctx),
Expand Down