147 lines
12 KiB
JavaScript
147 lines
12 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { createHash } from 'node:crypto';
|
|
import { sourceFingerprint, combineEvidence, linkCssEvidence } from './guard-evidence.mjs';
|
|
|
|
function evidence(callback) {
|
|
const parent = fs.realpathSync(os.tmpdir());
|
|
const dir = fs.mkdtempSync(path.join(parent, 'hm-evidence-'));
|
|
try {
|
|
fs.writeFileSync(path.join(dir, 'main.tsx'), 'original');
|
|
fs.writeFileSync(path.join(dir, 'contract.ts'), 'public contract');
|
|
const guard = { scope: 'FULL_SCREEN', runtimeRequired: true, target: dir, contractSource: dir, fingerprintFiles: ['main.tsx'], contractFiles: ['contract.ts'], sourceFingerprint: sourceFingerprint(dir, ['main.tsx']), contractFingerprint: sourceFingerprint(dir, ['contract.ts']), staticStatus: 'PASS', status: 'NOT VERIFIED', issues: [{ code: 'RENDER-002', severity: 'NOT VERIFIED' }] };
|
|
const runtime = { sourceFingerprint: guard.sourceFingerprint, contractFingerprint: guard.contractFingerprint, viewports: [390, 768, 1280, 1440].map(width => ({ width, status: 'PASS' })), interactions: [{ name: 'Submit', status: 'PASS' }] };
|
|
callback(guard, runtime, dir);
|
|
} finally { assert.equal(path.dirname(dir), parent); assert.ok(path.basename(dir).startsWith('hm-evidence-')); fs.rmSync(dir, { recursive: true, force: true }); }
|
|
}
|
|
test('matching independent browser evidence leaves original Guard untouched', () => evidence((g, r) => {
|
|
const before = JSON.stringify(g); assert.equal(combineEvidence(g, r).status, 'PASS'); assert.equal(JSON.stringify(g), before);
|
|
}));
|
|
test('changed source and changed public contract invalidate previous browser evidence', () => evidence((g, r, dir) => {
|
|
fs.writeFileSync(path.join(dir, 'main.tsx'), 'modified'); assert.equal(combineEvidence(g, r).status, 'NOT VERIFIED');
|
|
fs.writeFileSync(path.join(dir, 'main.tsx'), 'original'); fs.writeFileSync(path.join(dir, 'contract.ts'), 'modified'); assert.equal(combineEvidence(g, r).status, 'NOT VERIFIED');
|
|
}));
|
|
test('missing viewport or interaction cannot claim runtime PASS', () => evidence((g, r) => {
|
|
assert.equal(combineEvidence(g, { ...r, viewports: r.viewports.slice(1) }).status, 'NOT VERIFIED');
|
|
assert.equal(combineEvidence(g, { ...r, interactions: [] }).status, 'NOT VERIFIED');
|
|
}));
|
|
test('browser evidence cannot clear static FAIL or CSS intent uncertainty', () => evidence((g, r) => {
|
|
assert.equal(combineEvidence({ ...g, staticStatus: 'FAIL', issues: [{ code: 'TOKEN-001' }] }, r).status, 'FAIL');
|
|
assert.equal(combineEvidence({ ...g, staticStatus: 'NOT VERIFIED', issues: [{ code: 'REUSE-003' }] }, r).status, 'NOT VERIFIED');
|
|
assert.equal(combineEvidence(g, { ...r, interactions: [{ status: 'FAIL' }] }).status, 'FAIL');
|
|
}));
|
|
test('unproven React mount cannot become PASS and PARTIAL coverage stays PARTIAL', () => evidence((g, r) => {
|
|
assert.equal(combineEvidence({ ...g, issues: [{ code: 'RENDER-001' }] }, r).status, 'NOT VERIFIED');
|
|
assert.equal(combineEvidence({ ...g, runtimeRequired: false }, r).status, 'NOT VERIFIED');
|
|
assert.equal(combineEvidence({ ...g, scope: 'PARTIAL' }, r).scope, 'PARTIAL');
|
|
}));
|
|
|
|
const cssCheck = { name: 'Badge text', kind: 'COMPUTED_STYLE', width: 390, selector: '[data-slot="badge"]', property: 'color', actual: 'rgb(255, 255, 255)', expected: 'rgb(255, 255, 255)' };
|
|
const styleSite = { id: 'main.tsx:1:color', ruleId: 'STYLE-002', file: 'main.tsx', line: 1, component: 'Badge', selector: '[data-slot="badge"]', property: 'color', staticStatus: 'PASS', designIntentStatus: 'NOT VERIFIED' };
|
|
function siteChecks(site = styleSite) { return [390, 768, 1280, 1440].map(width => ({ ...cssCheck, width, height: 900, siteId: site.id, ruleId: site.ruleId, file: site.file, component: 'Badge', state: 'default', browser: { name: 'Chrome', version: 'test' } })); }
|
|
test('site-specific CSS has independent STATIC RUNTIME DESIGN_INTENT FINAL verdicts', () => evidence((g, r) => {
|
|
g.styleEvidence = { checks: [styleSite] };
|
|
const result = combineEvidence(g, { ...r, cssEvidence: { checks: siteChecks() } });
|
|
assert.equal(result.styleVerification.staticStatus, 'PASS');
|
|
assert.equal(result.styleVerification.runtimeStatus, 'PASS');
|
|
assert.equal(result.styleVerification.designIntentStatus, 'NOT VERIFIED');
|
|
assert.equal(result.status, 'NOT VERIFIED');
|
|
}));
|
|
test('wrong sites, source, rule, browser and state cannot cover CSS uncertainty', () => evidence((g, r) => {
|
|
g.styleEvidence = { checks: [styleSite] };
|
|
for (const change of [{ siteId: 'other' }, { file: 'other.tsx' }, { ruleId: 'OTHER' }, { browser: null }, { state: '' }, { property: 'backgroundColor' }]) {
|
|
const result = combineEvidence(g, { ...r, cssEvidence: { checks: siteChecks().map(c => ({ ...c, ...change })) } });
|
|
assert.equal(result.styleVerification.runtimeStatus, 'NOT VERIFIED');
|
|
}
|
|
}));
|
|
test('observed dynamic Token validates declarations but never approves unobserved branches', () => evidence((g, r) => {
|
|
const site = { id: 'dynamic:1', file: 'main.tsx', line: 1, ruleId: 'TOKEN-002', status: 'NOT VERIFIED' };
|
|
g.tokenEvidence = [{ definitions: ['--hm-valid'], dynamic: [site] }];
|
|
g.staticStatus = 'NOT VERIFIED';
|
|
const checks = siteChecks(site).map(c => ({ ...c, actualExpression: 'var(--hm-valid)', tokens: ['--hm-valid'] }));
|
|
const result = combineEvidence(g, { ...r, cssEvidence: { checks } });
|
|
assert.equal(result.styleVerification.checks[0].runtimeStatus, 'PASS');
|
|
assert.equal(result.styleVerification.checks[0].finalStatus, 'NOT VERIFIED');
|
|
assert.equal(result.status, 'NOT VERIFIED');
|
|
}));
|
|
test('undefined dynamic Token is a runtime FAIL despite computed fallback equality', () => evidence((g, r) => {
|
|
const site = { id: 'dynamic:1', file: 'main.tsx', line: 1, ruleId: 'TOKEN-002', status: 'NOT VERIFIED' };
|
|
g.tokenEvidence = [{ definitions: ['--hm-valid'], dynamic: [site] }];
|
|
const checks = siteChecks(site).map(c => ({ ...c, status: 'PASS', actualExpression: 'var(--hm-missing, #fff)', tokens: ['--hm-missing'] }));
|
|
assert.equal(combineEvidence(g, { ...r, cssEvidence: { checks } }).status, 'FAIL');
|
|
}));
|
|
test('unknown dynamic expressions, incomplete declarations and missing width stay unverified', () => evidence((g, r) => {
|
|
const site = { id: 'dynamic:1', file: 'main.tsx', line: 1, ruleId: 'TOKEN-002', status: 'NOT VERIFIED' };
|
|
g.tokenEvidence = [{ definitions: ['--hm-valid'], dynamic: [site] }];
|
|
for (const checks of [siteChecks(site), siteChecks(site).map(c => ({ ...c, actualExpression: 'var(--hm-valid)', tokens: [] })), siteChecks(site).slice(1)]) {
|
|
assert.equal(combineEvidence(g, { ...r, cssEvidence: { checks } }).styleVerification.runtimeStatus, 'NOT VERIFIED');
|
|
}
|
|
}));
|
|
test('static issue FAIL dominates inconsistent summary PASS and runtime PASS', () => evidence((g, r) => {
|
|
g.issues.push({ severity: 'FAIL', code: 'TOKEN-001' });
|
|
assert.equal(combineEvidence(g, r).status, 'FAIL');
|
|
}));
|
|
test('document references with impossible line numbers are not accepted', () => evidence((g, r, dir) => {
|
|
g.issues.push({ code: 'REUSE-003', file: 'main.tsx', line: 1 });
|
|
const reference = { file: 'contract.ts', line: 1000, sha256: createHash('sha256').update(fs.readFileSync(path.join(dir, 'contract.ts'))).digest('hex') };
|
|
const result = combineEvidence(g, { ...r, cssEvidence: { checks: [cssCheck], intentReviews: [{ issue: { code: 'REUSE-003', file: 'main.tsx', line: 1 }, rationale: 'Recipe', reference }] } });
|
|
assert.equal(result.cssEvidence.intentReviews.length, 0);
|
|
}));
|
|
test('unnamed interactions and duplicate CSS names cannot borrow a successful result', () => evidence((g, r) => {
|
|
assert.equal(combineEvidence(g, { ...r, interactions: [{ status: 'PASS' }] }).status, 'NOT VERIFIED');
|
|
g.styleEvidence = { checks: [styleSite] };
|
|
const checks = siteChecks();
|
|
checks.push({ ...checks[0], actual: 'rgb(0, 0, 0)', status: 'PASS' });
|
|
assert.equal(combineEvidence(g, { ...r, cssEvidence: { checks } }).styleVerification.runtimeStatus, 'FAIL');
|
|
}));
|
|
test('CSS evidence must address the source selector and Component; static uncertainty dominates', () => evidence((g, r) => {
|
|
g.styleEvidence = { checks: [styleSite] };
|
|
for (const change of [{ selector: 'body' }, { component: 'Button' }]) assert.equal(combineEvidence(g, { ...r, cssEvidence: { checks: siteChecks().map(c => ({ ...c, ...change })) } }).styleVerification.runtimeStatus, 'NOT VERIFIED');
|
|
g.issues.push({ code: 'CSS-005', severity: 'NOT VERIFIED' });
|
|
assert.equal(combineEvidence(g, r).status, 'NOT VERIFIED');
|
|
}));
|
|
test('CSS measurements and exact documentation references never approve intent or mutate raw issues', () => evidence((g, r, dir) => {
|
|
g.staticStatus = 'NOT VERIFIED'; g.issues.push({ code: 'REUSE-003', file: 'main.tsx', line: 1 });
|
|
const before = JSON.stringify(g);
|
|
const review = { issue: { code: 'REUSE-003', file: 'main.tsx', line: 1 }, rationale: 'Documented composition', reference: { file: 'contract.ts', line: 1, sha256: createHash('sha256').update(fs.readFileSync(path.join(dir, 'contract.ts'))).digest('hex') } };
|
|
const result = combineEvidence(g, { ...r, cssEvidence: { checks: [cssCheck], intentReviews: [review] } });
|
|
assert.equal(result.cssEvidence.status, 'PASS'); assert.equal(result.runtimeStatus, 'PASS'); assert.equal(result.status, 'NOT VERIFIED');
|
|
assert.equal(result.cssEvidence.intentReviews[0].status, 'NOT VERIFIED'); assert.equal(JSON.stringify(g), before);
|
|
}));
|
|
test('CSS mismatch remains a runtime FAIL even when a supplied status claims PASS', () => evidence((g, r) => {
|
|
const result = combineEvidence(g, { ...r, cssEvidence: { checks: [{ ...cssCheck, actual: 'rgb(0, 0, 0)', status: 'PASS' }] } });
|
|
assert.equal(result.cssEvidence.status, 'FAIL'); assert.equal(result.runtimeStatus, 'FAIL'); assert.equal(result.status, 'FAIL');
|
|
}));
|
|
test('missing, dynamic or invalid style measurements cannot claim CSS verification', () => evidence((g, r) => {
|
|
for (const check of [{ ...cssCheck, actual: '' }, { ...cssCheck, kind: 'DESIGN_INTENT' }, { ...cssCheck, width: 320 }, { ...cssCheck, kind: 'CSS_VARIABLE', property: 'color' }]) {
|
|
assert.equal(combineEvidence(g, { ...r, cssEvidence: { checks: [check] } }).cssEvidence.status, 'NOT VERIFIED');
|
|
}
|
|
assert.equal(combineEvidence(g, { ...r, cssEvidence: null }).cssEvidence.status, 'NOT VERIFIED');
|
|
assert.equal(combineEvidence(g, r).cssEvidence.status, 'NOT VERIFIED');
|
|
}));
|
|
test('changed source rejects supplied CSS checks and stale references do not become intent evidence', () => evidence((g, r, dir) => {
|
|
const runtime = { ...r, cssEvidence: { checks: [cssCheck], intentReviews: [{ issue: { code: 'REUSE-003', file: 'main.tsx', line: 1 }, rationale: 'Example', reference: { file: 'contract.ts', line: 1, sha256: 'stale' } }] } };
|
|
assert.equal(combineEvidence(g, runtime).cssEvidence.intentReviews.length, 0);
|
|
assert.equal(combineEvidence(g, runtime).cssEvidence.status, 'NOT VERIFIED');
|
|
fs.writeFileSync(path.join(dir, 'main.tsx'), 'changed');
|
|
const result = combineEvidence(g, runtime); assert.equal(result.cssEvidence.checks.length, 0); assert.equal(result.runtimeStatus, 'NOT VERIFIED');
|
|
}));
|
|
test('resolved CSS variables are measured values and static violations still dominate', () => evidence((g, r) => {
|
|
const runtime = { ...r, cssEvidence: { checks: [{ ...cssCheck, kind: 'CSS_VARIABLE', property: '--hm-badge-outline-text', actual: '#fff', expected: '#fff' }] } };
|
|
assert.equal(combineEvidence(g, runtime).cssEvidence.status, 'PASS');
|
|
assert.equal(combineEvidence({ ...g, staticStatus: 'FAIL', issues: [{ code: 'TOKEN-001' }] }, runtime).status, 'FAIL');
|
|
}));
|
|
test('CSS review cannot link an outside path even with a correct hash; missing references stay unverified', () => evidence((g, r, dir) => {
|
|
fs.mkdirSync(path.join(dir, 'references'));
|
|
g.contractSource = path.join(dir, 'references');
|
|
g.issues.push({ code: 'REUSE-003', file: 'main.tsx', line: 1 });
|
|
const sha256 = createHash('sha256').update(fs.readFileSync(path.join(dir, 'main.tsx'))).digest('hex');
|
|
for (const reference of [{ file: '../main.tsx', line: 1, sha256 }, { file: path.join(dir, 'main.tsx'), line: 1, sha256 }, { file: 'missing.md', line: 1, sha256 }]) {
|
|
const result = linkCssEvidence(g, { ...r, cssEvidence: { checks: [cssCheck], intentReviews: [{ issue: { code: 'REUSE-003', file: 'main.tsx', line: 1 }, rationale: 'Recipe', reference }] } }, true);
|
|
assert.equal(result.status, 'NOT VERIFIED'); assert.equal(result.intentReviews.length, 0);
|
|
}
|
|
}));
|