From b03802e4eef9ddf264eee2f1d570478b62c4a6ea Mon Sep 17 00:00:00 2001 From: Harlan GitHub Agent Date: Wed, 30 Sep 2026 08:30:13 +1000 Subject: [PATCH] fix(checkin): archive the thrown cause of a failed probe --- checks/_helpers/cause.mjs | 16 ++++++++++++++++ checks/_helpers/collectors.mjs | 9 ++++++--- checks/external/analytics.ts | 5 ++--- checks/external/ci.ts | 5 ++--- checks/external/database.ts | 5 ++--- checks/external/deploy.ts | 5 +++-- checks/external/git.ts | 5 ++--- checks/external/report.ts | 5 +++-- checks/external/workers.ts | 5 +++-- test/unit/external-checkin-collectors.test.ts | 9 +++++++++ 10 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 checks/_helpers/cause.mjs diff --git a/checks/_helpers/cause.mjs b/checks/_helpers/cause.mjs new file mode 100644 index 00000000..2be4f3af --- /dev/null +++ b/checks/_helpers/cause.mjs @@ -0,0 +1,16 @@ +import { unavailable } from '@harlan-zw/nuxt-checkin/external' + +/** + * The published runOne archives every thrown error as the generic + * 'Check threw an exception.' and the CLI passes no onError callback, so a + * collector failure reached the daily archive without its cause (2026-09-29: + * five Cloudflare failures, no reason). The collector-driven checks wrap + * their run body with this guard and archive the error message instead. + * @param {(context: import('@harlan-zw/nuxt-checkin/external').ExternalCheckContext) => Promise} run + * @returns {(context: import('@harlan-zw/nuxt-checkin/external').ExternalCheckContext) => Promise} a run that archives a thrown cause as Unavailable + */ +export function withCause(run) { + return async context => run(context).catch((error) => { + return unavailable(error instanceof Error && error.message.trim() ? error.message : 'Check evidence collection failed.') + }) +} diff --git a/checks/_helpers/collectors.mjs b/checks/_helpers/collectors.mjs index ac16b22f..ade16814 100644 --- a/checks/_helpers/collectors.mjs +++ b/checks/_helpers/collectors.mjs @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs' import { readdir, readFile } from 'node:fs/promises' -import { join } from 'node:path' +import { basename, join } from 'node:path' import { readBoundedResponseText, runCheckCommand } from '@harlan-zw/nuxt-checkin/external' import { ANALYTICS_ACCOUNT_TAG, buildCopyQuery, buildCopyTotalsQuery, buildWorkersQuery, collectWorkflowRuns, parseWorkflowName, runListArgs, summarizeCopies, summarizeWorkflowRuns } from './observability.mjs' @@ -25,8 +25,11 @@ async function run(context, command, args) { }) : baseEnv const result = await runCheckCommand({ ...context, env }, command, args, { maxBytes: 20 * 1024 * 1024 }) - if (result._tag === 'Err') - throw new Error(result.reason) + if (result._tag === 'Err') { + // The shared CLI's Err carries no stderr, so name the failing command: + // the archive then says which probe broke, not just that a command did. + throw new Error(`${basename(command)} ${args[0] ?? ''} failed: ${result.reason}`.trim()) + } return result.stdout.trim() } async function commandJson(context, command, args) { diff --git a/checks/external/analytics.ts b/checks/external/analytics.ts index 78c0a441..14e0fe35 100644 --- a/checks/external/analytics.ts +++ b/checks/external/analytics.ts @@ -1,9 +1,8 @@ import { defineExternalCheck, pass } from '@harlan-zw/nuxt-checkin/external' +import { withCause } from '../_helpers/cause.mjs' import { collectAnalytics } from '../_helpers/collectors.mjs' export default defineExternalCheck({ id: 'skilld.analytics', - async run(context) { - return pass(await collectAnalytics(context)) - }, + run: withCause(async context => pass(await collectAnalytics(context))), }) diff --git a/checks/external/ci.ts b/checks/external/ci.ts index 0d4283b3..a08f0feb 100644 --- a/checks/external/ci.ts +++ b/checks/external/ci.ts @@ -1,10 +1,9 @@ import { defineExternalCheck } from '@harlan-zw/nuxt-checkin/external' +import { withCause } from '../_helpers/cause.mjs' import { collectCI } from '../_helpers/collectors.mjs' import { evaluateCI } from '../_helpers/policy.mjs' export default defineExternalCheck({ id: 'skilld.ci', - async run(context) { - return evaluateCI(await collectCI(context)) - }, + run: withCause(async context => evaluateCI(await collectCI(context))), }) diff --git a/checks/external/database.ts b/checks/external/database.ts index 425b5319..7a9a65d9 100644 --- a/checks/external/database.ts +++ b/checks/external/database.ts @@ -1,10 +1,9 @@ import { defineExternalCheck } from '@harlan-zw/nuxt-checkin/external' +import { withCause } from '../_helpers/cause.mjs' import { collectD1 } from '../_helpers/collectors.mjs' import { evaluateD1 } from '../_helpers/policy.mjs' export default defineExternalCheck({ id: 'skilld.database', - async run(context) { - return evaluateD1(await collectD1(context), context.now, context.since) - }, + run: withCause(async context => evaluateD1(await collectD1(context), context.now, context.since)), }) diff --git a/checks/external/deploy.ts b/checks/external/deploy.ts index cbad31a3..ad2b46d0 100644 --- a/checks/external/deploy.ts +++ b/checks/external/deploy.ts @@ -1,10 +1,11 @@ import { defineExternalCheck, pass } from '@harlan-zw/nuxt-checkin/external' +import { withCause } from '../_helpers/cause.mjs' import { collectDeploy } from '../_helpers/collectors.mjs' export default defineExternalCheck({ id: 'skilld.deploy', - async run(context) { + run: withCause(async (context) => { const data = await collectDeploy(context) return data.latest?.versionId ? pass(data) : { _tag: 'Warn' as const, reason: 'Deployed Worker identity is unavailable.', evidence: data, coverage: 'incomplete' as const } - }, + }), }) diff --git a/checks/external/git.ts b/checks/external/git.ts index 1d9f73e5..d75d7fb6 100644 --- a/checks/external/git.ts +++ b/checks/external/git.ts @@ -1,9 +1,8 @@ import { defineExternalCheck, pass } from '@harlan-zw/nuxt-checkin/external' +import { withCause } from '../_helpers/cause.mjs' import { collectGit } from '../_helpers/collectors.mjs' export default defineExternalCheck({ id: 'skilld.git', - async run(context) { - return pass(await collectGit(context)) - }, + run: withCause(async context => pass(await collectGit(context))), }) diff --git a/checks/external/report.ts b/checks/external/report.ts index 4a578d6f..5745c263 100644 --- a/checks/external/report.ts +++ b/checks/external/report.ts @@ -1,9 +1,10 @@ import { defineExternalCheck, defineReportCheck, unavailable } from '@harlan-zw/nuxt-checkin/external' +import { withCause } from '../_helpers/cause.mjs' import { collectDeploy } from '../_helpers/collectors.mjs' export default defineExternalCheck({ id: 'skilld.report', - async run(context) { + run: withCause(async (context) => { const deployment = (await collectDeploy(context)).latest?.versionId if (!deployment) return unavailable('Deployed Worker identity is unavailable.') @@ -17,5 +18,5 @@ export default defineExternalCheck({ required: ['skilld.daily-health', 'skilld.daily-health-coverage'], maxAgeMs: 300_000, }).run({ ...context, event: { rootDir: context.rootDir, since: context.since, previous: context.previous, clock: context.clock, env: { ...context.env, CHECKIN_DEPLOYMENT: deployment } } }) - }, + }), }) diff --git a/checks/external/workers.ts b/checks/external/workers.ts index 4543ae76..79952b40 100644 --- a/checks/external/workers.ts +++ b/checks/external/workers.ts @@ -1,10 +1,11 @@ import { defineExternalCheck, pass, warn } from '@harlan-zw/nuxt-checkin/external' +import { withCause } from '../_helpers/cause.mjs' import { collectWorkers } from '../_helpers/collectors.mjs' export default defineExternalCheck({ id: 'skilld.workers', - async run(context) { + run: withCause(async (context) => { const data = await collectWorkers(context) return data.nonOk.length ? warn('Worker requests have non-success outcomes.', data) : pass(data) - }, + }), }) diff --git a/test/unit/external-checkin-collectors.test.ts b/test/unit/external-checkin-collectors.test.ts index e8ca2933..2074c625 100644 --- a/test/unit/external-checkin-collectors.test.ts +++ b/test/unit/external-checkin-collectors.test.ts @@ -76,6 +76,15 @@ it('rejects oversized Worker analytics evidence', async () => { expect(report.results[0]?.result._tag).toBe('Unavailable') }) +// The published runOne archives every thrown error as the generic +// 'Check threw an exception.' and the CLI passes no onError callback, so the +// 2026-09-29 run's five Cloudflare failures reached the archive with no cause. +it('archives the collector failure cause instead of the generic throw message', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Cloudflare API is unreachable.'))) + const { report } = await runExternalChecks([workersCheck], { required: [workersCheck.id] }, { env: { CLOUDFLARE_USAGE_TOKEN: 'test-token' } }) + expect(report.results[0]?.result).toEqual({ _tag: 'Unavailable', reason: 'Cloudflare API is unreachable.' }) +}) + // The ranked top page caps at 50 groups, so totals summed from that page // silently undercount once a day holds more groups than the cap. The totals // must come from their own untruncated rollup.