mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
Desktop startup can fail before React mounts on older WebViews, so an HTML-level watchdog now renders startup diagnostics even when the module bundle never reaches the app code. Persistent provider migration now imports legacy root provider config into cc-haha-owned storage without deleting the old source file, and plugin marketplace cleanup refuses obvious corrupted cache roots or outside paths. Constraint: User explicitly accepted the current reviewed state for landing despite remaining review concerns. Constraint: Global ~/.claude state is user-owned and protected; automatic repair must avoid deleting shared config, transcripts, skills, MCP, plugins, OAuth, adapters, and teams. Rejected: Tell users to delete ~/.claude or ~/.claude/cc-haha | unsafe because it can destroy user-owned Claude state and still may not fix WebView compatibility failures. Confidence: medium Scope-risk: moderate Directive: Do not weaken protected-path checks; future deletion paths should validate real paths and symlink behavior before recursive rm. Tested: bun test src/utils/plugins/installedPluginsManager.test.ts src/utils/plugins/marketplaceManager.test.ts Tested: bun run check:server Tested: cd desktop && bun run test -- --run src/main.test.tsx index-html.test.ts vite-config.test.ts src/theme/globals.test.ts Tested: cd desktop && bun run build Tested: bun run check:coverage Not-tested: live macOS 12/Safari 15 WKWebView startup on an affected machine. Not-tested: H5 diagnostic URL redaction and symlink-escape hardening are known follow-up risks from review.
238 lines
6.1 KiB
TypeScript
238 lines
6.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import {
|
|
evaluateChangedLineCoverage,
|
|
evaluateThresholds,
|
|
parseChangedLinesFromDiff,
|
|
parseLcov,
|
|
prefixRelativeLcovSourcePaths,
|
|
} from './coverage'
|
|
|
|
describe('coverage gate helpers', () => {
|
|
test('parses lcov totals into percentages', () => {
|
|
const summary = parseLcov([
|
|
'TN:',
|
|
'SF:src/a.ts',
|
|
'FNF:4',
|
|
'FNH:3',
|
|
'BRF:10',
|
|
'BRH:7',
|
|
'LF:20',
|
|
'LH:18',
|
|
'end_of_record',
|
|
'SF:src/b.ts',
|
|
'FNF:1',
|
|
'FNH:1',
|
|
'LF:10',
|
|
'LH:5',
|
|
'end_of_record',
|
|
].join('\n'))
|
|
|
|
expect(summary.lines.pct).toBe(76.67)
|
|
expect(summary.functions.pct).toBe(80)
|
|
expect(summary.branches.pct).toBe(70)
|
|
})
|
|
|
|
test('filters lcov records to an explicit source scope', () => {
|
|
const summary = parseLcov([
|
|
'TN:',
|
|
'SF:/repo/src/server/routes.ts',
|
|
'LF:10',
|
|
'LH:8',
|
|
'FNF:2',
|
|
'FNH:2',
|
|
'end_of_record',
|
|
'SF:/repo/desktop/src-tauri/target/generated.js',
|
|
'LF:100',
|
|
'LH:0',
|
|
'FNF:10',
|
|
'FNH:0',
|
|
'end_of_record',
|
|
].join('\n'), {
|
|
rootDir: '/repo',
|
|
scope: {
|
|
id: 'server-api',
|
|
title: 'Server/API',
|
|
includePrefixes: ['src/server/'],
|
|
},
|
|
})
|
|
|
|
expect(summary.lines.pct).toBe(80)
|
|
expect(summary.functions.pct).toBe(100)
|
|
})
|
|
|
|
test('prefixes package-relative lcov source paths for changed-line coverage', () => {
|
|
const lcov = prefixRelativeLcovSourcePaths([
|
|
'TN:',
|
|
'SF:src/stores/updateStore.ts',
|
|
'LF:1',
|
|
'LH:1',
|
|
'end_of_record',
|
|
'SF:/repo/desktop/src/main.tsx',
|
|
'LF:1',
|
|
'LH:0',
|
|
'end_of_record',
|
|
'SF:desktop/src/App.tsx',
|
|
'LF:1',
|
|
'LH:1',
|
|
'end_of_record',
|
|
].join('\n'), 'desktop')
|
|
|
|
expect(lcov).toContain('SF:desktop/src/stores/updateStore.ts')
|
|
expect(lcov).toContain('SF:/repo/desktop/src/main.tsx')
|
|
expect(lcov).toContain('SF:desktop/src/App.tsx')
|
|
})
|
|
|
|
test('evaluates changed executable line coverage', () => {
|
|
const changedLines = parseChangedLinesFromDiff([
|
|
'diff --git a/src/server/routes.ts b/src/server/routes.ts',
|
|
'--- a/src/server/routes.ts',
|
|
'+++ b/src/server/routes.ts',
|
|
'@@ -10,0 +11,2 @@',
|
|
'+const covered = true',
|
|
'+const uncovered = false',
|
|
].join('\n'))
|
|
|
|
const failures = evaluateChangedLineCoverage(
|
|
changedLines,
|
|
new Map([
|
|
['src/server/routes.ts', {
|
|
suiteId: 'server-api',
|
|
executableLines: new Set([11, 12]),
|
|
coveredLines: new Set([11]),
|
|
}],
|
|
]),
|
|
[{
|
|
id: 'server-api',
|
|
title: 'Server/API',
|
|
includePrefixes: ['src/server/'],
|
|
}],
|
|
90,
|
|
).failures
|
|
|
|
expect(failures).toEqual(['changed-lines: coverage 50% is below minimum 90%'])
|
|
})
|
|
|
|
test('excludes non-instrumented desktop styles from changed-line coverage', () => {
|
|
const changedLines = parseChangedLinesFromDiff([
|
|
'diff --git a/desktop/src/theme/globals.css b/desktop/src/theme/globals.css',
|
|
'--- a/desktop/src/theme/globals.css',
|
|
'+++ b/desktop/src/theme/globals.css',
|
|
'@@ -10,0 +11,2 @@',
|
|
'+.sidebar {',
|
|
'+ color: var(--color-text-primary);',
|
|
'+}',
|
|
'diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx',
|
|
'--- a/desktop/src/main.tsx',
|
|
'+++ b/desktop/src/main.tsx',
|
|
'@@ -20,0 +21,1 @@',
|
|
'+bootstrapDesktopApp()',
|
|
].join('\n'))
|
|
|
|
const result = evaluateChangedLineCoverage(
|
|
changedLines,
|
|
new Map([
|
|
['desktop/src/main.tsx', {
|
|
suiteId: 'desktop',
|
|
executableLines: new Set([21]),
|
|
coveredLines: new Set([21]),
|
|
}],
|
|
]),
|
|
[{
|
|
id: 'desktop',
|
|
title: 'Desktop',
|
|
includePrefixes: ['desktop/src/'],
|
|
excludeSuffixes: ['.css'],
|
|
}],
|
|
90,
|
|
)
|
|
|
|
expect(result.files).toEqual([{
|
|
file: 'desktop/src/main.tsx',
|
|
suiteId: 'desktop',
|
|
covered: 1,
|
|
total: 1,
|
|
pct: 100,
|
|
}])
|
|
expect(result.failures).toEqual([])
|
|
})
|
|
|
|
test('reports minimum threshold failures', () => {
|
|
const failures = evaluateThresholds([
|
|
{
|
|
id: 'root-server',
|
|
title: 'Root',
|
|
status: 'passed',
|
|
command: ['bun', 'test'],
|
|
durationMs: 1,
|
|
logPath: 'coverage.log',
|
|
summary: {
|
|
lines: { total: 100, covered: 79, pct: 79 },
|
|
functions: { total: 10, covered: 9, pct: 90 },
|
|
branches: { total: 10, covered: 8, pct: 80 },
|
|
statements: { total: 100, covered: 79, pct: 79 },
|
|
},
|
|
},
|
|
], {
|
|
schemaVersion: 1,
|
|
minimums: {
|
|
'root-server': {
|
|
lines: 80,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(failures).toEqual(['root-server: lines coverage 79% is below minimum 80%'])
|
|
})
|
|
|
|
test('treats failed suite execution as a coverage failure', () => {
|
|
const failures = evaluateThresholds([
|
|
{
|
|
id: 'desktop',
|
|
title: 'Desktop',
|
|
status: 'failed',
|
|
command: ['bun', 'run', 'test'],
|
|
durationMs: 1,
|
|
logPath: 'coverage.log',
|
|
error: 'coverage command exited with 1',
|
|
},
|
|
], {
|
|
schemaVersion: 1,
|
|
minimums: {},
|
|
})
|
|
|
|
expect(failures).toEqual(['desktop: coverage command exited with 1'])
|
|
})
|
|
|
|
test('does not require every suite to exist in the ratchet baseline', () => {
|
|
const failures = evaluateThresholds([
|
|
{
|
|
id: 'new-suite',
|
|
title: 'New suite',
|
|
status: 'passed',
|
|
command: ['bun', 'test'],
|
|
durationMs: 1,
|
|
logPath: 'coverage.log',
|
|
summary: {
|
|
lines: { total: 100, covered: 90, pct: 90 },
|
|
functions: { total: 10, covered: 9, pct: 90 },
|
|
branches: { total: 10, covered: 8, pct: 80 },
|
|
statements: { total: 100, covered: 90, pct: 90 },
|
|
},
|
|
},
|
|
], {
|
|
schemaVersion: 1,
|
|
minimums: {
|
|
'new-suite': {
|
|
branches: 85,
|
|
},
|
|
},
|
|
ratchet: {
|
|
baselinePath: 'scripts/quality-gate/coverage-baseline.json',
|
|
allowedDropPercent: 0,
|
|
},
|
|
})
|
|
|
|
expect(failures).toEqual(['new-suite: branches coverage 80% is below minimum 85%'])
|
|
})
|
|
})
|