This commit is contained in:
Matěj Kubíček
2026-09-02 20:38:45 +02:00
parent 0c524ed6af
commit c7948a788a
46 changed files with 10099 additions and 152 deletions
+57
View File
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { evidenceFromManifestStore, FirefoxExtensionWorkerUrl } from '../src/background/c2pa';
describe('C2PA evidence interpretation', () => {
it('recognises a trusted AI digital source type', () => {
const result = evidenceFromManifestStore({
active_manifest: 'example:claim',
validation_state: 'Trusted',
manifests: {
'example:claim': {
signature_info: { issuer: 'Example CA' },
assertions: [{ data: { digitalSourceType: 'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia' } }]
}
}
}, false);
expect(result[0]).toMatchObject({ strength: 'verified', verdict: 'ai', issuer: 'Example CA' });
});
it('does not claim trusted status for a merely valid signer', () => {
const result = evidenceFromManifestStore({
active_manifest: 'example:claim',
validation_state: 'Valid',
manifests: {
'example:claim': { source_type: 'trainedAlgorithmicMedia' }
}
}, false);
expect(result[0]).toMatchObject({ strength: 'detected', verdict: 'ai' });
});
it('prioritises a failed integrity status', () => {
const result = evidenceFromManifestStore({
active_manifest: 'example:claim',
validation_state: 'Invalid',
manifests: { 'example:claim': {} },
validation_status: [{ code: 'assertion.dataHash.mismatch' }]
}, false);
expect(result[0]).toMatchObject({ strength: 'invalid', verdict: 'invalid' });
});
it('returns no evidence when a manifest is absent', () => {
expect(evidenceFromManifestStore({ validation_state: 'Valid' }, false)).toEqual([]);
});
});
describe('Firefox worker URL compatibility', () => {
it('passes the pinned SDK HTTPS guard but resolves to the packaged extension worker', () => {
const packaged = 'moz-extension://12345678-1234-1234-1234-123456789abc/c2pa_worker.js';
const workerUrl = new FirefoxExtensionWorkerUrl(packaged);
expect(workerUrl.protocol).toBe('https:');
expect(workerUrl.toString()).toBe(packaged);
});
it('refuses non-extension worker resources', () => {
expect(() => new FirefoxExtensionWorkerUrl('https://example.com/worker.js')).toThrow(/packaged Firefox extension resource/);
});
});
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { ResultCache, hashBlob } from '../src/background/cache';
describe('result cache', () => {
it('evicts the least recently used entry', () => {
const cache = new ResultCache<number>(2);
cache.set('one', 1);
cache.set('two', 2);
expect(cache.get('one')).toBe(1);
cache.set('three', 3);
expect(cache.get('two')).toBeUndefined();
expect(cache.get('one')).toBe(1);
});
it('hashes identical content consistently', async () => {
const first = await hashBlob(new Blob(['same content']));
const second = await hashBlob(new Blob(['same content']));
expect(first).toBe(second);
expect(first).toMatch(/^[a-f0-9]{64}$/);
});
});
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { analyseDisclosures } from '../src/background/disclosures';
describe('publisher disclosure parsing', () => {
it('recognises explicit AI-origin metadata', () => {
const result = analyseDisclosures([{ source: 'meta:ai-generated', value: 'true' }]);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({ strength: 'disclosed', verdict: 'ai' });
});
it('treats generator metadata as a clue rather than proof', () => {
const result = analyseDisclosures([{ source: 'meta:generator', value: 'Framer AI' }]);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({ strength: 'clue', verdict: 'unknown' });
});
it('ignores ordinary website-generator metadata', () => {
expect(analyseDisclosures([{ source: 'meta:generator', value: 'WordPress 7.0' }])).toEqual([]);
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { analyseCopyPatterns } from '../src/content/heuristics';
import { evidenceFromHeuristics, heuristicScore } from '../src/background/heuristics';
describe('page heuristics', () => {
it('finds dense buzzwords and em-dash cadence without declaring an AI verdict', () => {
const text = 'Supercharge your workflow with our world-class, enterprise-grade platform — move faster — work smarter — without compromise.';
const findings = analyseCopyPatterns(text);
expect(findings.map((finding) => finding.ruleId)).toEqual(expect.arrayContaining([
'copy-marketing-buzzwords',
'copy-em-dash-cadence'
]));
expect(evidenceFromHeuristics(findings).every((item) => item.verdict === 'unknown')).toBe(true);
});
it('adds a diversity point when independent categories agree', () => {
const evidence = evidenceFromHeuristics([
{ ruleId: 'one', category: 'visual', title: 'One', detail: 'One', score: 2, occurrences: 1 },
{ ruleId: 'two', category: 'layout', title: 'Two', detail: 'Two', score: 3, occurrences: 1 }
]);
expect(heuristicScore(evidence)).toBe(6);
});
it('ignores ordinary direct prose', () => {
expect(analyseCopyPatterns('The archive contains monthly invoices. Select a year, then download the required PDF.')).toEqual([]);
});
});
+14
View File
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { inspectImageGeneratorMetadata } from '../src/background/image-metadata';
describe('image generator metadata clues', () => {
it('recognises a Stable Diffusion parameters block', async () => {
const blob = new Blob(['PNG metadata\0Steps: 30, Sampler: Euler a, CFG scale: 7, Seed: 42']);
const evidence = await inspectImageGeneratorMetadata(blob);
expect(evidence[0]).toMatchObject({ kind: 'generator', strength: 'detected', verdict: 'ai' });
});
it('does not flag an ordinary image byte sequence', async () => {
expect(await inspectImageGeneratorMetadata(new Blob(['ordinary image metadata']))).toEqual([]);
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { shouldShowNotice } from '../src/content/notice';
import type { PageStatus, ScanResult } from '../src/shared/types';
function resultWithStatus(status: PageStatus): ScanResult {
return {
scanId: 'scan',
pageUrl: 'https://example.com/',
pageTitle: 'Example',
scannedAt: new Date(0).toISOString(),
summary: {
status,
inspectedImages: 0,
totalImages: 0,
inspectedTextBlocks: 0,
totalTextBlocks: 0,
aiItems: 0,
invalidItems: 0,
disclosedItems: 0,
inaccessibleItems: 0,
heuristicScore: status === 'heuristic-suspected' ? 7 : 0,
heuristicFindings: status === 'heuristic-suspected' ? 3 : 0
},
page: { id: 'page', kind: 'page', label: 'Example', verdict: 'unknown', strength: 'none', evidence: [] },
images: [],
texts: [],
warnings: []
};
}
describe('in-page result visibility', () => {
it('shows meaningful heuristic and provenance results', () => {
expect(shouldShowNotice(resultWithStatus('heuristic-suspected'))).toBe(true);
expect(shouldShowNotice(resultWithStatus('verified-ai'))).toBe(true);
expect(shouldShowNotice(resultWithStatus('invalid-credential'))).toBe(true);
});
it('stays silent on unknown or inaccessible pages', () => {
expect(shouldShowNotice(resultWithStatus('no-supported-evidence'))).toBe(false);
expect(shouldShowNotice(resultWithStatus('unable-to-inspect'))).toBe(false);
});
});
+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest';
import { aggregateSummary, createAssetResult, strongestEvidence, verdictFromEvidence } from '../src/shared/status';
import type { Evidence } from '../src/shared/types';
const evidence = (overrides: Partial<Evidence>): Evidence => ({
id: crypto.randomUUID(),
kind: 'c2pa',
strength: 'none',
verdict: 'unknown',
title: 'Evidence',
detail: 'Test evidence',
...overrides
});
describe('evidence precedence', () => {
it('treats invalid credentials as stronger than positive evidence', () => {
const items = [
evidence({ strength: 'verified', verdict: 'ai' }),
evidence({ strength: 'invalid', verdict: 'invalid' })
];
expect(strongestEvidence(items)).toBe('invalid');
expect(verdictFromEvidence(items)).toBe('invalid');
});
it('never turns empty evidence into a human-origin assertion', () => {
const result = createAssetResult('one', 'image', 'Unsigned image', []);
expect(result.verdict).toBe('unknown');
expect(result.strength).toBe('none');
});
});
describe('page aggregation', () => {
const unknownPage = createAssetResult('page', 'page', 'Page', []);
it('reports verified AI when a trusted AI credential is present', () => {
const image = createAssetResult('image', 'image', 'Image', [
evidence({ strength: 'verified', verdict: 'ai' })
]);
expect(aggregateSummary(unknownPage, [image], [], 1, 0).status).toBe('verified-ai');
});
it('reports mixed provenance when AI and non-AI assertions coexist', () => {
const ai = createAssetResult('ai', 'image', 'AI', [evidence({ strength: 'verified', verdict: 'ai' })]);
const camera = createAssetResult('camera', 'image', 'Camera', [
evidence({ strength: 'verified', verdict: 'not-ai-asserted' })
]);
expect(aggregateSummary(unknownPage, [ai, camera], [], 2, 0).status).toBe('mixed-provenance');
});
it('prioritises invalid credentials', () => {
const invalid = createAssetResult('bad', 'image', 'Invalid', [
evidence({ strength: 'invalid', verdict: 'invalid' })
]);
expect(aggregateSummary(unknownPage, [invalid], [], 1, 0).status).toBe('invalid-credential');
});
it('reports suspicion without asserting AI authorship when heuristic clues cross the threshold', () => {
const suspectedPage = createAssetResult('page', 'page', 'Page', [
evidence({ kind: 'heuristic', strength: 'clue', category: 'visual', score: 3 }),
evidence({ kind: 'heuristic', strength: 'clue', category: 'layout', score: 2 })
]);
const summary = aggregateSummary(suspectedPage, [], [], 0, 0, 6);
expect(summary.status).toBe('heuristic-suspected');
expect(suspectedPage.verdict).toBe('unknown');
expect(summary.heuristicScore).toBe(6);
});
it('does not elevate a single weak style clue', () => {
const page = createAssetResult('page', 'page', 'Page', [
evidence({ kind: 'heuristic', strength: 'clue', category: 'visual', score: 2 })
]);
expect(aggregateSummary(page, [], [], 0, 0, 6).status).toBe('no-supported-evidence');
});
});