From e3bc925cce37491f973ead29fd2a6d3dc58de53b Mon Sep 17 00:00:00 2001 From: Dan Kritzinger Date: Mon, 11 May 2026 15:10:13 +0200 Subject: [PATCH] Make Doctor current-state aware --- apps/api/src/doctor.test.ts | 39 ++++++++++++++++++++++++++++++++++ apps/api/src/doctor.ts | 42 +++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/apps/api/src/doctor.test.ts b/apps/api/src/doctor.test.ts index 4e6a4cb..d93cd08 100644 --- a/apps/api/src/doctor.test.ts +++ b/apps/api/src/doctor.test.ts @@ -48,6 +48,45 @@ function deployment(overrides: Partial): DeploymentRow { } describe('VibeStack Doctor', () => { + it('reports a healthy current state when a newer successful deployment supersedes an older failure', async () => { + const packet = await buildDoctorPacket({ + app: { ...app, status: 'running', current_deployment_id: 'succeeded-deployment' }, + deployments: [ + deployment({ + id: 'succeeded-deployment', + version_number: 3, + status: 'succeeded', + error_code: null, + error_message: null, + error_details_json: null + }), + deployment({ + id: 'failed-deployment', + version_number: 2, + status: 'failed', + error_code: 'HEALTH_CHECK_FAILED', + error_message: 'Old failure', + error_details_json: { + checkedUrl: 'http://127.0.0.1:49152/health', + port: 3000, + healthCheckPath: '/health', + logExcerpt: 'Cannot GET /health' + } + }) + ], + secrets: [], + appLogs: ['Listening on 3000', 'DB ready'], + postgres: { enabled: true, logs: [] } + }); + + expect(packet.rootCauseCategory).toBe('healthy'); + expect(packet.relatedDeploymentId).toBe('succeeded-deployment'); + expect(packet.healthCheckResult.status).toBe('passed'); + expect(packet.summary).toContain('latest deployment succeeded'); + expect(packet.suggestedFixPrompt).toContain('No repair is needed'); + expect(packet.evidence.some((item) => item.label === 'Historical failure')).toBe(true); + }); + it('classifies missing health routes from failed health checks', async () => { const packet = await buildDoctorPacket({ app, diff --git a/apps/api/src/doctor.ts b/apps/api/src/doctor.ts index 625bba1..1d0dc81 100644 --- a/apps/api/src/doctor.ts +++ b/apps/api/src/doctor.ts @@ -13,6 +13,7 @@ export type DoctorRootCause = | 'build_failure' | 'container_start_failure' | 'health_check_failure' + | 'healthy' | 'unknown'; export type DoctorEvidence = { @@ -124,11 +125,15 @@ async function getStoredOpenRouterSetting(db: Db): Promise { } export async function buildDoctorPacket(input: DoctorInput): Promise { - const failedDeployment = - input.deployments.find((deployment) => deployment.status === 'failed') ?? - (input.app.status === 'failed' ? input.deployments[0] : undefined); + const latestDeployment = input.deployments[0]; + const latestFailed = latestDeployment?.status === 'failed' ? latestDeployment : undefined; + const historicalFailure = + latestDeployment?.status === 'succeeded' + ? input.deployments.find((deployment) => deployment.status === 'failed') + : undefined; + const failedDeployment = latestFailed ?? (input.app.status === 'failed' ? latestDeployment : undefined); const details = asRecord(failedDeployment?.error_details_json); - const manifest = asRecord(failedDeployment?.manifest ?? input.deployments[0]?.manifest); + const manifest = asRecord(failedDeployment?.manifest ?? latestDeployment?.manifest); const healthPath = stringValue(details.healthCheckPath) ?? stringValue(manifest.healthCheckPath); const checkedUrl = stringValue(details.checkedUrl); const port = numberValue(details.port) ?? numberValue(manifest.port); @@ -140,6 +145,31 @@ export async function buildDoctorPacket(input: DoctorInput): Promise