// Reviewer identity — the attribution Studio writes into the audit // trail must never look better than it is. // // Background (usertest 2026-07-26, legal persona): the hub copies // the client's `reviewer` string into `decided_by` unchecked, so a // bare "stefan@studio" in an export reads like a proven identity // while being an arbitrary client claim. Until the hub derives the // value from the authenticated caller (contract: // docs/reviewer-identity.md), every value Studio sends carries the // `unverified:` marker — and everything Studio *reads* is presented // by what it actually proves. import 'package:flutter_test/flutter_test.dart'; import 'package:chain_studio/data/reviewer_identity.dart'; void main() { setUp(() => ReviewerIdentity.debugHandle = 'stefan@studio'); tearDown(() => ReviewerIdentity.debugHandle = null); group('what Studio sends', () { test('every reviewer string leaves Studio marked as a claim', () { expect(ReviewerIdentity.wire(), 'unverified:stefan@studio'); expect(ReviewerIdentity.wire('anna@ops'), 'unverified:anna@ops'); }); test('marking is idempotent — a value that already carries the ' 'marker is not wrapped twice', () { final once = ReviewerIdentity.wire('anna@ops'); expect(ReviewerIdentity.wire(once), once); // The page computes the wire value for display AND HubService // normalises again as a backstop; that must stay harmless. expect(ReviewerIdentity.wire(ReviewerIdentity.wire(once)), once); }); test('a blank handle falls back to the local one instead of ' 'sending an empty attribution', () { expect(ReviewerIdentity.wire(' '), 'unverified:stefan@studio'); expect(ReviewerIdentity.wire(''), 'unverified:stefan@studio'); }); test('surrounding whitespace never reaches the record', () { expect(ReviewerIdentity.wire(' anna@ops '), 'unverified:anna@ops'); }); test('the local handle is a label, not an identity — but always ' 'present', () { ReviewerIdentity.debugHandle = null; expect(ReviewerIdentity.localHandle, isNotEmpty); expect( ReviewerIdentity.wire(), startsWith(kUnverifiedReviewerPrefix), reason: 'no code path may send a bare handle', ); }); }); group('what Studio reads back', () { test('a marked value is shown by its name and flagged as a claim', () { final parsed = ReviewerIdentity.parse('unverified:anna@ops'); expect(parsed.handle, 'anna@ops'); expect(parsed.trust, RecordedReviewerTrust.selfDeclared); expect(parsed.isSelfDeclared, isTrue); }); test('an unmarked value (legacy row, CLI decision) is never ' 'presented as checked', () { final parsed = ReviewerIdentity.parse('stefan@studio'); expect(parsed.handle, 'stefan@studio'); expect(parsed.trust, RecordedReviewerTrust.unknown); expect( parsed.isSelfDeclared, isFalse, reason: 'unknown provenance must not be labelled either way', ); }); test('a marker with nothing behind it keeps the marker visible ' 'rather than rendering an empty reviewer', () { final parsed = ReviewerIdentity.parse('unverified:'); expect(parsed.handle, 'unverified:'); expect(parsed.isSelfDeclared, isTrue); }); test('round-trips what Studio wrote', () { final parsed = ReviewerIdentity.parse(ReviewerIdentity.wire()); expect(parsed.handle, 'stefan@studio'); expect(parsed.isSelfDeclared, isTrue); }); }); group('assurance level per hub policy', () { test('anonymous hub — nobody can be tied to a decision', () { expect(reviewerAssuranceFor(true), ReviewerAssurance.anonymousHub); }); test('auth-enabled hub — the access is checked, the name is not', () { expect(reviewerAssuranceFor(false), ReviewerAssurance.accessControlled); }); test('unreadable policy stays unknown — never optimistic', () { expect(reviewerAssuranceFor(null), ReviewerAssurance.unknown); }); }); }