mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
Introduce the Electron desktop shell alongside the existing React renderer and local Bun server boundary. The migration keeps the DesktopHost contract explicit across Tauri, Electron, and browser runtimes while adding Electron main/preload services for dialogs, shell, notifications, updates, tray/window lifecycle, terminal, preview WebContentsView, app mode, and release/package validation.
The commit also carries the latest local main desktop command updates, including agent slash entries and hidden-by-default markdown thinking details, so the packaged Electron build matches the current main UX surface.
Constraint: React renderer, local Bun server, REST/WebSocket, and sidecar boundaries must remain reusable during the migration
Constraint: macOS dev packages are ad-hoc signed and cannot prove Developer ID notarization or Gatekeeper release launch
Rejected: Browser-only smoke validation | it cannot exercise native dialogs, keychain prompts, notification behavior, or packaged app startup
Confidence: medium
Scope-risk: broad
Directive: Do not remove Tauri host support until signed Electron release artifacts pass native OS smoke on macOS, Windows, and Linux
Tested: bun run check:desktop
Tested: cd desktop && bun run check:electron
Tested: CSC_IDENTITY_AUTO_DISCOVERY=false bun run electron📦dir
Tested: bun run test:package-smoke --platform macos --package-kind dir --artifacts-dir desktop/build-artifacts/electron
Tested: Computer Use read packaged Electron app window at desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app
Not-tested: Developer ID signed/notarized Gatekeeper launch
Not-tested: Real OS notification click-to-session action
Not-tested: Windows and Linux packaged app smoke on real hosts
616 lines
21 KiB
TypeScript
616 lines
21 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { currentPackageSmokePlatform, currentReleaseArtifactsDir, lanesForMode } from './modes'
|
|
import { renderJUnitReport, renderMarkdownReport } from './reporter'
|
|
import { runQualityGate, runQualityGateLanes } from './runner'
|
|
import type { LaneDefinition, QualityGateReport } from './types'
|
|
|
|
describe('quality gate modes', () => {
|
|
test('pr mode includes existing path-aware PR checks', () => {
|
|
const lanes = lanesForMode('pr').map((lane) => lane.id)
|
|
expect(lanes).toContain('impact-report')
|
|
expect(lanes).toContain('policy-checks')
|
|
expect(lanes).toContain('desktop-checks')
|
|
expect(lanes).toContain('server-checks')
|
|
expect(lanes).toContain('adapter-checks')
|
|
expect(lanes).toContain('native-checks')
|
|
expect(lanes).toContain('docs-checks')
|
|
expect(lanes).toContain('persistence-upgrade')
|
|
expect(lanes).toContain('quarantine')
|
|
expect(lanes).toContain('coverage')
|
|
expect(lanes.some((lane) => lane.startsWith('baseline:'))).toBe(false)
|
|
})
|
|
|
|
test('baseline mode includes live baseline cases but not native checks', () => {
|
|
const lanes = lanesForMode('baseline').map((lane) => lane.id)
|
|
expect(lanes).toContain('baseline-catalog')
|
|
expect(lanes).toContain('quarantine')
|
|
expect(lanes).toContain('coverage')
|
|
expect(lanes).toContain('baseline:failing-unit:current-runtime')
|
|
expect(lanes).toContain('provider-smoke:current-runtime')
|
|
expect(lanes).toContain('baseline:multi-file-api:current-runtime')
|
|
expect(lanes).toContain('desktop-smoke:agent-browser-chat:current-runtime')
|
|
expect(lanes).not.toContain('native-checks')
|
|
})
|
|
|
|
test('release mode composes PR, baseline, and native lanes', () => {
|
|
const laneDefinitions = lanesForMode('release')
|
|
const lanes = laneDefinitions.map((lane) => lane.id)
|
|
const packageSmokePlatform = currentPackageSmokePlatform()
|
|
expect(lanes).toContain('policy-checks')
|
|
expect(lanes).toContain('desktop-checks')
|
|
expect(lanes).toContain('server-checks')
|
|
expect(lanes).toContain('adapter-checks')
|
|
expect(lanes).toContain('docs-checks')
|
|
expect(lanes).toContain('persistence-upgrade')
|
|
expect(lanes).toContain('quarantine')
|
|
expect(lanes).toContain('coverage')
|
|
expect(lanes).toContain('baseline:failing-unit:current-runtime')
|
|
expect(lanes).toContain('provider-smoke:current-runtime')
|
|
expect(lanes).not.toContain('desktop-smoke:agent-browser-chat:current-runtime')
|
|
if (packageSmokePlatform) {
|
|
expect(lanes).toContain(`desktop-package-smoke:${packageSmokePlatform}`)
|
|
const packageSmokeLane = laneDefinitions.find((lane) => lane.id === `desktop-package-smoke:${packageSmokePlatform}`)
|
|
expect(packageSmokeLane?.command).toContain('--package-kind')
|
|
expect(packageSmokeLane?.command).toContain('release')
|
|
expect(packageSmokeLane?.command).toContain('--artifacts-dir')
|
|
if (packageSmokePlatform === 'macos') {
|
|
expect(packageSmokeLane?.command).toContain('--require-macos-gatekeeper')
|
|
}
|
|
}
|
|
expect(lanes).toContain('native-checks')
|
|
})
|
|
|
|
test('maps host platforms to package-smoke platform names', () => {
|
|
expect(currentPackageSmokePlatform('darwin')).toBe('macos')
|
|
expect(currentPackageSmokePlatform('win32')).toBe('windows')
|
|
expect(currentPackageSmokePlatform('linux')).toBe('linux')
|
|
expect(currentPackageSmokePlatform('freebsd')).toBeNull()
|
|
expect(currentReleaseArtifactsDir('darwin', 'arm64')).toBe('desktop/build-artifacts/macos-arm64')
|
|
expect(currentReleaseArtifactsDir('darwin', 'x64')).toBe('desktop/build-artifacts/macos-x64')
|
|
expect(currentReleaseArtifactsDir('win32', 'x64')).toBe('desktop/build-artifacts/windows-x64')
|
|
expect(currentReleaseArtifactsDir('linux', 'arm64')).toBe('desktop/build-artifacts/linux-arm64')
|
|
expect(currentReleaseArtifactsDir('freebsd', 'x64')).toBeNull()
|
|
})
|
|
|
|
test('baseline mode expands cases across explicit provider/model targets', () => {
|
|
const lanes = lanesForMode('baseline', [
|
|
{ providerId: 'provider-a', modelId: 'model-a', label: 'provider-a-model-a' },
|
|
{ providerId: 'provider-b', modelId: 'model-b', label: 'provider-b-model-b' },
|
|
]).map((lane) => lane.id)
|
|
|
|
expect(lanes).toContain('baseline:failing-unit:provider-a-model-a')
|
|
expect(lanes).toContain('baseline:failing-unit:provider-b-model-b')
|
|
expect(lanes).toContain('baseline:multi-file-api:provider-a-model-a')
|
|
expect(lanes).toContain('baseline:multi-file-api:provider-b-model-b')
|
|
expect(lanes).toContain('provider-smoke:provider-a-model-a')
|
|
expect(lanes).toContain('provider-smoke:provider-b-model-b')
|
|
expect(lanes).toContain('desktop-smoke:agent-browser-chat:provider-a-model-a')
|
|
expect(lanes).toContain('desktop-smoke:agent-browser-chat:provider-b-model-b')
|
|
})
|
|
})
|
|
|
|
describe('runQualityGate', () => {
|
|
test('writes dry-run reports without executing expensive commands', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
try {
|
|
const { report, outputDir } = await runQualityGate({
|
|
mode: 'baseline',
|
|
dryRun: true,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'dry-run-test',
|
|
})
|
|
|
|
expect(report.mode).toBe('baseline')
|
|
expect(report.summary.failed).toBe(0)
|
|
expect(report.summary.skipped).toBeGreaterThan(0)
|
|
expect(report.results.find((result) => result.id === 'coverage')?.category).toBe('coverage')
|
|
expect(report.results.find((result) => result.id.startsWith('baseline:'))?.live).toBe(true)
|
|
expect(readFileSync(join(outputDir, 'report.json'), 'utf8')).toContain('"mode": "baseline"')
|
|
expect(readFileSync(join(outputDir, 'report.md'), 'utf8')).toContain('# Quality Gate Report')
|
|
expect(readFileSync(join(outputDir, 'report.md'), 'utf8')).toContain('## Result Matrix')
|
|
expect(readFileSync(join(outputDir, 'junit.xml'), 'utf8')).toContain('<testsuite name="quality-gate.baseline"')
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('continues through later lanes after a failure so reports show full impact', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
const lanes: LaneDefinition[] = [
|
|
{
|
|
id: 'first-lane',
|
|
title: 'First lane',
|
|
description: 'Fails intentionally',
|
|
kind: 'command',
|
|
command: ['false'],
|
|
requiredForModes: ['pr'],
|
|
},
|
|
{
|
|
id: 'second-lane',
|
|
title: 'Second lane',
|
|
description: 'Still runs',
|
|
kind: 'command',
|
|
command: ['true'],
|
|
requiredForModes: ['pr'],
|
|
},
|
|
]
|
|
|
|
try {
|
|
const { report } = await runQualityGateLanes({
|
|
mode: 'pr',
|
|
dryRun: false,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'continue-after-failure-test',
|
|
}, lanes, async (lane) => ({
|
|
id: lane.id,
|
|
title: lane.title,
|
|
status: lane.id === 'first-lane' ? 'failed' : 'passed',
|
|
command: lane.command,
|
|
durationMs: 1,
|
|
exitCode: lane.id === 'first-lane' ? 1 : 0,
|
|
}))
|
|
|
|
expect(report.results.map((result) => result.id)).toEqual(['first-lane', 'second-lane'])
|
|
expect(report.summary).toEqual({
|
|
passed: 1,
|
|
failed: 1,
|
|
skipped: 0,
|
|
})
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('filters lanes by exact id or prefix selector', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
try {
|
|
const { report } = await runQualityGate({
|
|
mode: 'baseline',
|
|
dryRun: true,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'filter-lanes-test',
|
|
onlyLaneSelectors: ['provider-smoke:*'],
|
|
})
|
|
|
|
expect(report.results.length).toBe(1)
|
|
expect(report.results[0].id).toBe('provider-smoke:current-runtime')
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('command lanes persist per-lane logs', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
const lanes: LaneDefinition[] = [
|
|
{
|
|
id: 'log-lane',
|
|
title: 'Log lane',
|
|
description: 'Writes output',
|
|
kind: 'command',
|
|
command: ['bun', '--version'],
|
|
requiredForModes: ['pr'],
|
|
},
|
|
]
|
|
|
|
try {
|
|
const { report } = await runQualityGateLanes({
|
|
mode: 'pr',
|
|
dryRun: false,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'command-log-test',
|
|
}, lanes)
|
|
|
|
const logPath = report.results[0].logPath
|
|
expect(report.results[0].status).toBe('passed')
|
|
expect(logPath).toBeTruthy()
|
|
expect(readFileSync(logPath!, 'utf8')).toContain('$ bun --version')
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('skips PR local check lanes when the impact report does not require them', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
const lanes: LaneDefinition[] = [
|
|
{
|
|
id: 'impact-report',
|
|
title: 'Impact report',
|
|
description: 'Writes selected local checks',
|
|
kind: 'command',
|
|
command: ['bash', '-lc', [
|
|
'printf "%s\\n"',
|
|
'"# PR impact report"',
|
|
'""',
|
|
'"Changed files: 1"',
|
|
'"Areas: server"',
|
|
'"Labels: none"',
|
|
'"Blocked: no"',
|
|
'""',
|
|
'"## Required local checks"',
|
|
'"- bun run check:server"',
|
|
].join(' ')],
|
|
requiredForModes: ['pr'],
|
|
},
|
|
{
|
|
id: 'desktop-checks',
|
|
title: 'Desktop checks',
|
|
description: 'Should be skipped',
|
|
kind: 'command',
|
|
command: ['bash', '-lc', 'exit 7'],
|
|
impactRequiredCheck: 'bun run check:desktop',
|
|
requiredForModes: ['pr'],
|
|
},
|
|
]
|
|
|
|
try {
|
|
const { report } = await runQualityGateLanes({
|
|
mode: 'pr',
|
|
dryRun: false,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'impact-selected-skip-test',
|
|
}, lanes)
|
|
|
|
expect(report.results.map((result) => result.status)).toEqual(['passed', 'skipped'])
|
|
expect(report.results[1].skipReason).toContain('not required by impact report')
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('runs PR local check lanes selected by the impact report', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
const lanes: LaneDefinition[] = [
|
|
{
|
|
id: 'impact-report',
|
|
title: 'Impact report',
|
|
description: 'Writes selected local checks',
|
|
kind: 'command',
|
|
command: ['bash', '-lc', [
|
|
'printf "%s\\n"',
|
|
'"# PR impact report"',
|
|
'""',
|
|
'"Changed files: 1"',
|
|
'"Areas: desktop"',
|
|
'"Labels: none"',
|
|
'"Blocked: no"',
|
|
'""',
|
|
'"## Required local checks"',
|
|
'"- bun run check:desktop"',
|
|
].join(' ')],
|
|
requiredForModes: ['pr'],
|
|
},
|
|
{
|
|
id: 'desktop-checks',
|
|
title: 'Desktop checks',
|
|
description: 'Should run',
|
|
kind: 'command',
|
|
command: ['bash', '-lc', 'exit 0'],
|
|
impactRequiredCheck: 'bun run check:desktop',
|
|
requiredForModes: ['pr'],
|
|
},
|
|
]
|
|
|
|
try {
|
|
const { report } = await runQualityGateLanes({
|
|
mode: 'pr',
|
|
dryRun: false,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'impact-selected-run-test',
|
|
}, lanes)
|
|
|
|
expect(report.results.map((result) => result.status)).toEqual(['passed', 'passed'])
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('hydrates impact and coverage summaries from generated command artifacts', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
const coverageDir = join(artifactsDir, 'coverage-output')
|
|
mkdirSync(coverageDir, { recursive: true })
|
|
writeFileSync(join(coverageDir, 'coverage-report.md'), '# Coverage Report\n')
|
|
writeFileSync(join(coverageDir, 'coverage-report.json'), JSON.stringify({
|
|
suites: [
|
|
{
|
|
id: 'desktop',
|
|
title: 'Desktop React',
|
|
status: 'passed',
|
|
summary: {
|
|
lines: { pct: 88, covered: 88, total: 100 },
|
|
functions: { pct: 80, covered: 8, total: 10 },
|
|
branches: { pct: 75, covered: 15, total: 20 },
|
|
statements: { pct: 88, covered: 88, total: 100 },
|
|
},
|
|
},
|
|
],
|
|
failures: [],
|
|
}) + '\n')
|
|
|
|
const lanes: LaneDefinition[] = [
|
|
{
|
|
id: 'impact-report',
|
|
title: 'Impact report',
|
|
description: 'Summarize changed areas',
|
|
kind: 'command',
|
|
command: ['bun', 'run', 'check:impact'],
|
|
requiredForModes: ['pr'],
|
|
category: 'scope',
|
|
},
|
|
{
|
|
id: 'coverage',
|
|
title: 'Coverage gate',
|
|
description: 'Run coverage',
|
|
kind: 'command',
|
|
command: ['bun', 'run', 'check:coverage'],
|
|
requiredForModes: ['pr'],
|
|
category: 'coverage',
|
|
},
|
|
]
|
|
|
|
try {
|
|
const { report } = await runQualityGateLanes({
|
|
mode: 'pr',
|
|
dryRun: false,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'artifact-summary-test',
|
|
}, lanes, async (lane, options) => {
|
|
const logPath = join(options.runOutputDir!, 'logs', `${lane.id}.log`)
|
|
mkdirSync(join(options.runOutputDir!, 'logs'), { recursive: true })
|
|
if (lane.id === 'impact-report') {
|
|
writeFileSync(logPath, [
|
|
'# PR impact report',
|
|
'',
|
|
'Changed files: 2',
|
|
'Areas: desktop, server',
|
|
'Labels: none',
|
|
'Blocked: no',
|
|
'',
|
|
'## Required local checks',
|
|
'- `bun run check:desktop`',
|
|
'',
|
|
'## Test coverage signals',
|
|
'- No obvious missing-test signal from changed paths.',
|
|
'',
|
|
'## Risk notes',
|
|
'- Desktop state/API layer changed.',
|
|
].join('\n'))
|
|
} else {
|
|
writeFileSync(logPath, `Coverage report: ${join(coverageDir, 'coverage-report.md')}\nSummary: passed=1 failed=0\n`)
|
|
}
|
|
return {
|
|
id: lane.id,
|
|
title: lane.title,
|
|
status: 'passed',
|
|
command: lane.command,
|
|
durationMs: 1,
|
|
exitCode: 0,
|
|
logPath,
|
|
}
|
|
})
|
|
|
|
expect(report.impact?.changedFiles).toBe(2)
|
|
expect(report.impact?.requiredChecks).toEqual(['`bun run check:desktop`'])
|
|
expect(report.coverage?.suites[0].lines?.pct).toBe(88)
|
|
expect(report.artifacts.map((artifact) => artifact.path)).toContain(join(coverageDir, 'coverage-report.md'))
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('release mode treats skipped live lanes as failures', async () => {
|
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
|
const lanes: LaneDefinition[] = [
|
|
{
|
|
id: 'live-lane',
|
|
title: 'Live lane',
|
|
description: 'Requires release evidence',
|
|
kind: 'provider-smoke',
|
|
live: true,
|
|
requiredForModes: ['release'],
|
|
},
|
|
]
|
|
|
|
try {
|
|
const { report } = await runQualityGateLanes({
|
|
mode: 'release',
|
|
dryRun: false,
|
|
allowLive: false,
|
|
baselineTargets: [],
|
|
rootDir: process.cwd(),
|
|
artifactsDir,
|
|
runId: 'release-live-skipped-test',
|
|
}, lanes, async (lane) => ({
|
|
id: lane.id,
|
|
title: lane.title,
|
|
status: 'skipped',
|
|
durationMs: 1,
|
|
skipReason: 'missing credentials',
|
|
}))
|
|
|
|
expect(report.summary).toEqual({
|
|
passed: 0,
|
|
failed: 1,
|
|
skipped: 0,
|
|
})
|
|
expect(report.results[0].error).toBe('missing credentials')
|
|
} finally {
|
|
rmSync(artifactsDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('renderMarkdownReport', () => {
|
|
test('renders command, skip reason, and summary', () => {
|
|
const report: QualityGateReport = {
|
|
schemaVersion: 1,
|
|
runId: 'example',
|
|
mode: 'pr',
|
|
dryRun: true,
|
|
allowLive: false,
|
|
startedAt: '2026-05-02T00:00:00.000Z',
|
|
finishedAt: '2026-05-02T00:00:01.000Z',
|
|
rootDir: process.cwd(),
|
|
git: {
|
|
sha: 'abc123',
|
|
dirty: true,
|
|
},
|
|
results: [
|
|
{
|
|
id: 'impact-report',
|
|
title: 'Impact report',
|
|
description: 'Summarize changed areas.',
|
|
category: 'scope',
|
|
live: false,
|
|
status: 'skipped',
|
|
command: ['bun', 'run', 'check:impact'],
|
|
durationMs: 1,
|
|
skipReason: 'dry run',
|
|
logPath: '/tmp/impact-report.log',
|
|
},
|
|
],
|
|
summary: {
|
|
passed: 0,
|
|
failed: 0,
|
|
skipped: 1,
|
|
},
|
|
artifacts: [],
|
|
}
|
|
|
|
const markdown = renderMarkdownReport(report)
|
|
expect(markdown).toContain('Skipped: 1')
|
|
expect(markdown).toContain('Test scope')
|
|
expect(markdown).toContain('`bun run check:impact`')
|
|
expect(markdown).toContain('dry run')
|
|
expect(markdown).toContain('/tmp/impact-report.log')
|
|
})
|
|
|
|
test('renders impact and coverage summaries in one markdown report', () => {
|
|
const report: QualityGateReport = {
|
|
schemaVersion: 1,
|
|
runId: 'example',
|
|
mode: 'pr',
|
|
dryRun: false,
|
|
allowLive: false,
|
|
startedAt: '2026-05-02T00:00:00.000Z',
|
|
finishedAt: '2026-05-02T00:00:03.000Z',
|
|
rootDir: process.cwd(),
|
|
git: {
|
|
sha: 'abc123',
|
|
dirty: false,
|
|
},
|
|
results: [
|
|
{
|
|
id: 'coverage',
|
|
title: 'Coverage gate',
|
|
category: 'coverage',
|
|
live: false,
|
|
status: 'passed',
|
|
command: ['bun', 'run', 'check:coverage'],
|
|
durationMs: 2500,
|
|
logPath: '/tmp/coverage.log',
|
|
},
|
|
],
|
|
impact: {
|
|
changedFiles: 3,
|
|
areas: ['desktop', 'server'],
|
|
labels: ['allow-cli-core-change'],
|
|
blocked: false,
|
|
requiredChecks: ['`bun run check:desktop`', '`bun run check:coverage`'],
|
|
testCoverageSignals: ['No obvious missing-test signal from changed paths.'],
|
|
riskNotes: ['Session runtime changed.'],
|
|
},
|
|
coverage: {
|
|
reportPath: '/tmp/coverage-report.md',
|
|
failures: [],
|
|
suites: [
|
|
{
|
|
id: 'desktop',
|
|
title: 'Desktop React',
|
|
status: 'passed',
|
|
lines: { pct: 82, covered: 82, total: 100 },
|
|
functions: { pct: 80, covered: 8, total: 10 },
|
|
branches: { pct: 75, covered: 15, total: 20 },
|
|
statements: { pct: 82, covered: 82, total: 100 },
|
|
},
|
|
],
|
|
},
|
|
artifacts: [{ title: 'Coverage report markdown', path: '/tmp/coverage-report.md' }],
|
|
summary: {
|
|
passed: 1,
|
|
failed: 0,
|
|
skipped: 0,
|
|
},
|
|
}
|
|
|
|
const markdown = renderMarkdownReport(report)
|
|
expect(markdown).toContain('Changed files: 3')
|
|
expect(markdown).toContain('| Desktop React | passed | 82% (82/100)')
|
|
expect(markdown).toContain('Coverage report markdown: /tmp/coverage-report.md')
|
|
})
|
|
|
|
test('renders JUnit report for CI test-summary consumers', () => {
|
|
const report: QualityGateReport = {
|
|
schemaVersion: 1,
|
|
runId: 'example',
|
|
mode: 'release',
|
|
dryRun: false,
|
|
allowLive: true,
|
|
startedAt: '2026-05-02T00:00:00.000Z',
|
|
finishedAt: '2026-05-02T00:00:02.000Z',
|
|
rootDir: process.cwd(),
|
|
git: {
|
|
sha: 'abc123',
|
|
dirty: false,
|
|
},
|
|
results: [
|
|
{
|
|
id: 'provider-smoke:example',
|
|
title: 'Provider smoke',
|
|
category: 'smoke',
|
|
live: true,
|
|
status: 'failed',
|
|
durationMs: 500,
|
|
error: 'API <error>',
|
|
logPath: '/tmp/provider.log',
|
|
},
|
|
],
|
|
summary: {
|
|
passed: 0,
|
|
failed: 1,
|
|
skipped: 0,
|
|
},
|
|
artifacts: [],
|
|
}
|
|
|
|
const junit = renderJUnitReport(report)
|
|
expect(junit).toContain('testsuite name="quality-gate.release"')
|
|
expect(junit).toContain('failures="1"')
|
|
expect(junit).toContain('API <error>')
|
|
expect(junit).toContain('/tmp/provider.log')
|
|
})
|
|
})
|