v0.3.2
This commit is contained in:
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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}$/);
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user