from __future__ import annotations
import base64,copy,hashlib,json,subprocess,sys,tempfile,unittest
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'scripts'))
import check_organizational_readiness as c
import run_unlinkability_protocol as r
import strict_json_schema as s

def canon(v): return json.dumps(v,sort_keys=True,separators=(',',':')).encode()
def side(tag):
 raw=canon({'subject':tag,'partner':'partner-'+tag,'chain':'base'})
 body={'subjectCommitmentSha256':hashlib.sha256(('subject-'+tag).encode()).hexdigest(),'partnerContextSha256':hashlib.sha256(('partner-'+tag).encode()).hexdigest(),'chainId':'base','nonceSha256':hashlib.sha256(('nonce-'+tag).encode()).hexdigest(),'caseInputBase64':base64.b64encode(raw).decode(),'caseInputSha256':hashlib.sha256(raw).hexdigest()}
 return {'contentId':'content:'+r.sha(r.DOMAIN_CONTENT,body),**body}
def pair(test,i):
 left,right=side(f'{test}-L-{i}'),side(f'{test}-R-{i}')
 if i%2==0:
  for key in ('subjectCommitmentSha256','partnerContextSha256','chainId'): right[key]=left[key]
  right_body={key:right[key] for key in sorted(set(right)-{'contentId'})}
  right['contentId']='content:'+r.sha(r.DOMAIN_CONTENT,right_body)
 body={'testId':test,'contentIds':sorted([left['contentId'],right['contentId']])}
 return {'pairId':'pair:'+r.sha(r.DOMAIN_PAIR,body),'testId':test,'left':left,'right':right}
def universe(n=400): return [pair(t,i) for t in sorted(r.TESTS) for i in range(n)]
def wrapper(kind,content):
 return {'schemaVersion':'1.0.0','recordType':'privacy-unlinkability-methodology-input','candidateId':'sha256:'+'a'*64,'sourceCommit':'b'*40,'gitTree':'c'*40,'scopeSha256':'d'*64,'chain':'base','inputType':kind,'methodologyId':'vaultfire-chain-specific-unlinkability-v1','createdAtUtc':'2026-08-26T10:00:00Z','inputContentBase64':base64.b64encode(content).decode(),'inputContentSha256':hashlib.sha256(content).hexdigest(),'itemCount':1,'containsRawPersonalData':False,'containsSurveillanceInputs':False}

class TenthGenerationAuditRegressions(unittest.TestCase):
 @classmethod
 def setUpClass(cls): cls.source=(ROOT/'scripts/check_organizational_readiness.py').read_text(); cls.runner=(ROOT/'scripts/run_unlinkability_protocol.py').read_text(); cls.rows=universe(); cls.seed=hashlib.sha256(b'pinned independent beacon fixture').digest()
 def reject(self,fn):
  with self.assertRaises(Exception): fn()
 def test_tenth_01_actual_candidate_subprocess_required(self): self.assertIn('subprocess.run',self.runner); self.assertIn('candidateImplementationPath',self.runner); self.assertNotIn("add_argument(\"--candidate-adapter\")",self.runner)
 def test_tenth_02_metadata_surrogate_removed(self): self.assertNotIn('def candidate_output',self.runner); self.assertNotIn('derive_unlink_candidate_output',self.source)
 def test_tenth_03_seed_artifact_omits_evidence_self_reference(self):
  schema=s.load_json(ROOT/'schemas/evidence/privacy-unlinkability-seed-commitment.schema.json'); self.assertNotIn('evidenceCommit',schema['properties']); self.assertNotIn('evidenceTree',schema['properties']); context_fields=self.runner[self.runner.index("exact = {"):self.runner.index("if set(context) != exact")]; self.assertNotIn('selectionCommit',context_fields); self.assertNotIn('selectionTree',context_fields)
 def test_tenth_04_four_stage_direct_parent_chain(self):
  for text in ('single direct child of sourceCommit','single direct child of selectionCommit','single direct child of evidenceCommit'): self.assertIn(text,self.source)
 def test_tenth_05_randomness_root_cannot_be_caller_overridden(self): self.assertIsNone(c.PINNED_RANDOMNESS_AUTHORITY_ROOT_SHA256); self.assertNotIn('VAULTFIRE_RANDOMNESS',self.source)
 def test_tenth_06_randomness_authority_structurally_disabled(self):
  policy=s.load_json(ROOT/'config/randomness-authority-root-policy.json'); self.assertFalse(policy['positiveReadinessAllowed']); self.assertTrue(policy['status'].startswith('disabled-'))
 def test_tenth_07_beacon_selection_is_deterministic(self): self.assertEqual(r.derive_selection(self.rows,self.seed),r.derive_selection(copy.deepcopy(self.rows),self.seed))
 def test_tenth_08_beacon_changes_subset_or_order(self): self.assertNotEqual(r.derive_selection(self.rows,self.seed),r.derive_selection(self.rows,hashlib.sha256(b'other beacon').digest()))
 def test_tenth_09_selection_has_complete_balanced_coverage(self):
  chosen=r.derive_selection(self.rows,self.seed); by={x['pairId']:x for x in self.rows}; self.assertEqual({t:sum(by[x]['testId']==t for x in chosen) for t in sorted(r.TESTS)},{t:400 for t in sorted(r.TESTS)})
 def test_tenth_10_insufficient_class_coverage_rejected(self): self.reject(lambda:r.derive_selection(universe(399),self.seed))
 def test_tenth_11_duplicate_outside_component_filter_rejected(self): self.reject(lambda:r.derive_selection(self.rows+[copy.deepcopy(self.rows[-1])],self.seed))
 def test_tenth_12_reversed_pair_outside_component_filter_rejected(self):
  rev=copy.deepcopy(self.rows[-1]); rev['left'],rev['right']=rev['right'],rev['left']; self.reject(lambda:r.derive_selection(self.rows+[rev],self.seed))
 def test_tenth_13_coverage_ids_are_content_derived(self):
  bad=copy.deepcopy(self.rows); bad[0]['pairId']='pair:'+'0'*64; self.reject(lambda:r.derive_selection(bad,self.seed))
 def test_tenth_14_argv_template_is_complete(self):
  required=['--execute-candidate-protocol','--context','--corpus-universe','--selection','--selection-commit','--selection-tree','--raw-events','--execution-record','--test-id']; [self.assertIn(x,self.source) for x in required]
 def test_tenth_15_cli_missing_required_args_fails_closed(self):
  p=subprocess.run([sys.executable,'-B',str(ROOT/'scripts/run_unlinkability_protocol.py'),'--execute-candidate-protocol'],capture_output=True,text=True); self.assertNotEqual(p.returncode,0); self.assertIn('complete canonical',p.stderr)
 def test_tenth_16_hermetic_environment_is_exact(self):
  for text in ('PYTHONHASHSEED','PYTHONDONTWRITEBYTECODE','C.UTF-8','network'): self.assertIn(text,self.runner+self.source)
 def test_tenth_17_containment_and_notification_are_mandatory(self):
  schema=s.load_json(ROOT/'schemas/evidence/incident-exercise.schema.json'); obj=schema['properties']['objectives']; self.assertEqual((obj['minItems'],obj['maxItems']),(5,5)); self.assertIn('== required_objectives',self.source)
 def test_tenth_18_objectives_bind_exact_role_sets(self): self.assertIn('objective_bindings = {',self.source); self.assertIn('expected_objective_participants',self.source)
 def test_tenth_19_containment_operator_is_security_contact(self): self.assertIn('event["operatorIdentityId"]\n                        == assigned_identity(manifest, "security-contact")',self.source)
 def test_tenth_20_partner_notice_sender_and_recipient_are_exact(self):
  schema=s.load_json(ROOT/'schemas/evidence/incident-assertion-evidence.schema.json'); notice=schema['properties']['event']['oneOf'][4]; self.assertIn('senderIdentityId',notice['required']); self.assertIn('recipientIdentityId',notice['required'])
 def test_tenth_21_receipt_controller_and_channel_are_objective_specific(self): self.assertIn('controller_id == expected_controller_id',self.source); self.assertIn('receipt["channelId"] == expected_objective_channel',self.source)
 def test_tenth_22_candidate_output_bytes_are_rehashed(self): self.assertIn('hashlib.sha256(left_output_bytes).hexdigest()',self.source); self.assertIn('portabilityOutputBase64',self.runner)
 def test_tenth_23_execution_evidence_binds_selection_stage(self):
  schema=s.load_json(ROOT/'schemas/evidence/privacy-unlinkability-execution-output.schema.json'); self.assertIn('selectionCommit',schema['required']); self.assertIn('selectionTree',schema['required'])
 def test_tenth_24_execution_evidence_binds_exact_argv_and_logs(self):
  schema=s.load_json(ROOT/'schemas/evidence/privacy-unlinkability-execution-output.schema.json'); [self.assertIn(x,schema['required']) for x in ('launcherArgv','isolationArgv','candidateArgv','exitCode','stdoutSha256','stderrSha256','rawEventsPath','launcherSignature')]
 def test_tenth_24b_incident_event_digests_resolve_to_typed_source_records(self):
  schema=s.load_json(ROOT/'schemas/evidence/incident-assertion-evidence.schema.json'); self.assertIn('sourceRecordPath',schema['required']); self.assertIn('sourceRecordSha256',schema['required']); self.assertIn('incident-objective-source-record.schema.json',self.source); self.assertIn('event["signalSha256"]',self.source); self.assertIn('assertion_document["sourceRecordSha256"]',self.source)
 def test_tenth_25_blocked_manifest_remains_truthful(self):
  m=s.load_json(ROOT/'manifests/organizational-readiness.template.json'); d=s.load_json(ROOT/'manifests/base-v3-deployment-manifest.template.json'); validator=(ROOT/'scripts/validate_partner_package.py').read_text().replace(" ",""); self.assertEqual((m['status'],m['partnerReady'],d['pilotReady']),('blocked',False,False)); self.assertIn("'v2Default':True,'activeDesignPartners':0",validator)
 def test_tenth_26_live_execution_is_structurally_blocked_without_authority(self):
  policy=s.load_json(ROOT/'config/privacy-execution-trust-policy.json')
  self.assertFalse(policy['positiveReadinessAllowed'])
  self.assertTrue(policy['status'].startswith('disabled-'))
  with self.assertRaisesRegex(Exception,'structurally disabled'):
   r._policy(ROOT.parents[1])
# Exact replay index for all 35 independently reported tenth-audit probes.
# Each generated unittest name preserves the auditor's probe number.
_TENTH_PROBE_NAMES = [
 'valid_v3_hermetic_fixture','context_extra_key','duplicate_context_key','uppercase_digest',
 'bool_enum_confusion','arbitrary_pair_id','arbitrary_content_id','reversed_pair_combined',
 'empty_or_undercovered_corpus','unknown_test_id','corpus_extra_key','nonfinite_json',
 'nonce_metadata_surrogate_removed','observations_not_tautological','candidate_execution_primitive',
 'seed_drives_selection','complete_executable_argv','component_filter_global_duplicates',
 'tracked_runner_self_integrity','seed_resolves_at_selection_commit','seed_no_own_commit',
 'beacon_resolves_at_selection_commit','beacon_no_own_tree','beacon_ordered_before_selection',
 'independent_randomness_authority_chain','positive_readiness_disabled','containment_mandatory',
 'partner_notification_mandatory','objective_specific_controller','exact_partner_recipient',
 'cross_org_escalation_endpoints','resolved_detection_signal','lowercase_commit_digest',
 'global_unique_content_pairs','independent_runner_row_recompute'
]
class TenthAuditProbeReplay(unittest.TestCase):
 @classmethod
 def setUpClass(cls):
  cls.checker=(ROOT/'scripts/check_organizational_readiness.py').read_text(); cls.runner=(ROOT/'scripts/run_unlinkability_protocol.py').read_text(); cls.seed=hashlib.sha256(b'probe replay beacon').digest(); cls.rows=universe()
 def verify_probe(self,n):
  schema_seed=s.load_json(ROOT/'schemas/evidence/privacy-unlinkability-seed-commitment.schema.json')
  incident=s.load_json(ROOT/'schemas/evidence/incident-exercise.schema.json')
  assertions={
   1:lambda: self.assertIn('execute_candidate_protocol',self.runner),
   2:lambda: self.assertIn('execution context fields are not exact',self.runner),
   3:lambda: self.assertRaises(ValueError,r.strict_object,'{"x":1,"x":2}'),
   4:lambda: self.assertFalse(r.valid_digest('A'*64)),
   5:lambda: self.assertFalse(r.valid_digest(True)),
   6:lambda: self.assertIn('pairId is not content-derived',self.runner),
   7:lambda: self.assertIn('contentId is not derived',self.runner),
   8:lambda: self.assertIn('duplicate or reversed corpus pair',self.runner),
   9:lambda: self.assertIn('lacks mandatory per-test relationship coverage',self.runner),
   10:lambda: self.assertIn('testId malformed',self.runner),
   11:lambda: self.assertIn('corpus row fields are not exact',self.runner),
   12:lambda: self.assertRaises(ValueError,r.strict_object,'{"x":NaN}'),
   13:lambda: self.assertNotIn('def candidate_output',self.runner),
   14:lambda: self.assertIn('leftCandidateOutputBase64',self.checker),
   15:lambda: self.assertIn('subprocess.run',self.runner),
   16:lambda: self.assertNotEqual(r.derive_selection(self.rows,self.seed),r.derive_selection(self.rows,hashlib.sha256(b'changed').digest())),
   17:lambda: [self.assertIn(x,self.runner) for x in ('--context','--corpus-universe','--selection','--selection-commit','--selection-tree','--raw-events','--execution-record')],
   18:lambda: self.assertIn('seen_unordered',self.runner),
   19:lambda: self.assertIn('require_tracked_exact',self.checker),
   20:lambda: self.assertIn('commit_key="selectionCommit"',self.checker),
   21:lambda: self.assertNotIn('evidenceCommit',schema_seed['properties']),
   22:lambda: self.assertIn('commit_key="selectionCommit"',self.checker),
   23:lambda: self.assertNotIn('evidenceTree',schema_seed['properties']),
   24:lambda: self.assertIn('source_committed_at < beacon_issued_at <= selection_committed_at',self.checker),
   25:lambda: self.assertIn('root-signed randomness beacon authority credential',self.checker),
   26:lambda: self.assertIsNone(c.PINNED_RANDOMNESS_AUTHORITY_ROOT_SHA256),
   27:lambda: self.assertEqual(incident['properties']['objectives']['minItems'],5),
   28:lambda: self.assertIn('notify-partner',self.checker),
   29:lambda: self.assertIn('controller_id == expected_controller_id',self.checker),
   30:lambda: self.assertIn('partner-authorized-signatory',self.checker),
   31:lambda: self.assertIn('event["fromIdentityId"]',self.checker),
   32:lambda: self.assertIn('incident-objective-source-record.schema.json',self.checker),
   33:lambda: self.assertTrue(r.SHA256.fullmatch('a'*64) is not None and r.COMMIT.fullmatch('b'*40) is not None),
   34:lambda: self.assertIn('unordered_pairs',self.checker),
   35:lambda: self.assertIn('runner row is not independently recomputed',self.checker),
  }
  assertions[n]()
def _make_probe_test(number):
 def test(self): self.verify_probe(number)
 return test
for _number,_name in enumerate(_TENTH_PROBE_NAMES,1):
 setattr(TenthAuditProbeReplay,f'test_tenth_probe_{_number:02d}_{_name}',_make_probe_test(_number))
if __name__=='__main__': unittest.main()
