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
8 changes: 8 additions & 0 deletions checks/_helpers/collectors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ export function collectD1(context) {
const impactedBrokenRepos = impactedBrokenReposGuarded
? (await d1Query(`SELECT r.owner, r.repo, rtrim((CASE WHEN EXISTS (SELECT 1 FROM skills s WHERE s.owner = r.owner AND s.repo = r.repo) THEN 'skill ' ELSE '' END) || (CASE WHEN EXISTS (SELECT 1 FROM user_starred_repos usr WHERE usr.owner = r.owner AND usr.repo = r.repo) THEN 'star ' ELSE '' END) || (CASE WHEN EXISTS (SELECT 1 FROM skill_subscriptions sub WHERE sub.owner = r.owner AND sub.repo = r.repo) THEN 'subscription ' ELSE '' END) || (CASE WHEN EXISTS (SELECT 1 FROM collection_skills_v2 cs WHERE cs.owner = r.owner AND cs.repo = r.repo) THEN 'collection ' ELSE '' END)) AS reason FROM repos r WHERE ${impactedBrokenReposWhere} ORDER BY r.broken_since DESC`))
: null
// A bare `skill_sync_failures` count made a RED night name no Skills, so
// triage needed a hand-run D1 query two days running (2026-09-23). The
// identities behind the count are archived too, with the same filter so the
// number and the rows can never disagree.
const skillSyncFailures = has('skills')
? (await d1Query(`SELECT owner, repo, name FROM skills WHERE sync_status IS NOT NULL AND sync_status != 'ok' AND last_synced_at >= ${sinceSec} ORDER BY last_synced_at DESC`))
: null
const syncJobs = has('sync_jobs')
? (await d1Query(`SELECT name, cron, stale_after_seconds, last_run_at, last_status, last_error FROM sync_jobs WHERE enabled = 1 AND name != 'daily-health-check' ORDER BY name`))
: null
Expand Down Expand Up @@ -252,6 +259,7 @@ export function collectD1(context) {
pipeline: {
...(await d1Query(`SELECT ${pipelineParts.join(', ')}`))[0],
newly_broken_repos_impacted_identities: impactedBrokenRepos,
skill_sync_failure_identities: skillSyncFailures,
},
cost: withXSpend((await d1Query(`SELECT ${costParts.join(', ')}`))[0]),
syncJobs,
Expand Down
79 changes: 78 additions & 1 deletion test/unit/daily-checkin-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
// @vitest-environment node
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { runExternalChecks } from '@harlan-zw/nuxt-checkin/external'
import { expect, it } from 'vitest'
import { afterEach, expect, it, vi } from 'vitest'
import baselineCheck from '../../checks/external/baseline'
import databaseCheck from '../../checks/external/database'

const boundary = vi.hoisted(() => ({ command: vi.fn() }))
vi.mock('@harlan-zw/nuxt-checkin/external', async (original) => {
const actual = await original<typeof import('@harlan-zw/nuxt-checkin/external')>()
return { ...actual, runCheckCommand: boundary.command }
})
const roots: string[] = []
afterEach(async () => {
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true })))
boundary.command.mockReset()
})

const now = new Date('2026-09-14T00:00:00Z')
it('records a stale window without blocking a successful replacement baseline', async () => {
Expand All @@ -13,3 +28,65 @@ it('marks an invalid comparison window as missing evidence', async () => {
expect(report.coverage).toBe('incomplete')
expect(report.results[0]?.result).toMatchObject({ _tag: 'Warn', coverage: 'incomplete', evidence: { _tag: 'invalid' } })
})

// Issue #285: the gate read `skill_sync_failures` as a bare count, so a RED
// night named no Skills and triage needed a hand-run D1 query two days running.
it('archives the identities behind the skill sync failure count', async () => {
const since = new Date(now.getTime() - 24 * 3_600_000)
const root = await mkdtemp(join(tmpdir(), 'skilld-checkin-sync-'))
roots.push(root)
await mkdir(join(root, 'shared/server'), { recursive: true })
await mkdir(join(root, 'migrations'))
await writeFile(join(root, 'shared/server/x-ingest.ts'), 'export const DAILY_DISCOVERY_READ_BUDGET = 400')
await writeFile(join(root, 'migrations/001.sql'), '')
const skills = [
{ owner: 'anthu', repo: 'vite', name: 'env', sync_status: 'error', last_synced_at: Math.floor(since.getTime() / 1000) + 3600 },
{ owner: 'obra', repo: 'superpowers', name: 'tdd', sync_status: 'path_missing', last_synced_at: Math.floor(since.getTime() / 1000) + 7200 },
{ owner: 'harlan-zw', repo: 'skills', name: 'healthy', sync_status: 'ok', last_synced_at: Math.floor(since.getTime() / 1000) + 3600 },
{ owner: 'stale', repo: 'old', name: 'old-failure', sync_status: 'error', last_synced_at: Math.floor(since.getTime() / 1000) - 1 },
{ owner: 'fresh', repo: 'never-synced', name: 'no-status', sync_status: null, last_synced_at: Math.floor(since.getTime() / 1000) + 3600 },
]
const failed = (sql: string) => {
const bound = Number(sql.match(/last_synced_at >= (\d+)/)![1])
return skills.filter(row => row.sync_status !== null && row.sync_status !== 'ok' && row.last_synced_at >= bound)
}
boundary.command.mockImplementation(async (_context, command: string, args: string[]) => {
let output: unknown = ''
if (command === 'git') {
output = args[0] === 'ls-tree' ? 'migrations/001.sql' : args[0] === 'branch' ? 'main' : args[0] === 'rev-parse' ? 'abc' : ''
return { _tag: 'Ok', stdout: output, stderr: '' }
}
const sql = args[args.indexOf('--command') + 1]!
let rows: unknown[] = []
if (sql.includes('sqlite_master'))
rows = ['skills', 'repos', 'owners', 'users', 'activity', 'sync_jobs', 'jobs', 'failed_jobs', 'd1_migrations'].map(name => ({ name }))
else if (sql.includes('SELECT owner, repo, name FROM skills'))
rows = failed(sql).sort((a, b) => b.last_synced_at - a.last_synced_at).map(({ owner, repo, name }) => ({ owner, repo, name }))
else if (sql.includes('MAX(name)'))
rows = [{ name: '001.sql' }]
else if (sql.includes('AS skills'))
rows = [{ skills: skills.length, broken_repos: 0 }]
else if (sql.includes('AS new_skills'))
rows = [{ new_skills: 0, digests_failed: 0 }]
else if (sql.includes('AS newly_broken_repos_total'))
rows = [{ newly_broken_repos_total: 0, stale_reserved_jobs: 0, skill_sync_failures: failed(sql).length, failed_jobs: 0 }]
else if (sql.includes('AS ai_cost_usd'))
rows = [{ x_discovery_reads_today: 10, x_hot_posts: 0 }]
output = [{ success: true, results: rows }]
return { _tag: 'Ok', stdout: JSON.stringify(output), stderr: '' }
})
const { report } = await runExternalChecks([databaseCheck], { required: [databaseCheck.id] }, { rootDir: root, env: {}, now, since })
expect(report.coverage).toBe('complete')
expect(report.results.find(check => check.id === 'skilld.database')?.result).toMatchObject({
_tag: 'Warn',
evidence: {
pipeline: {
skill_sync_failures: 2,
skill_sync_failure_identities: [
{ owner: 'obra', repo: 'superpowers', name: 'tdd' },
{ owner: 'anthu', repo: 'vite', name: 'env' },
],
},
},
})
})