import hashlib import json from pathlib import Path import tempfile import unittest from paper_atlas.collectors.www_archive import (ROOT, SPRING, FALL, Sources, collect, decode, early_review, spring_rows) class ArchiveTests(unittest.TestCase): def setUp(self): t=tempfile.TemporaryDirectory();self.addCleanup(t.cleanup);self.root=Path(t.name);self.cache=self.root/'cache';self.cache.mkdir() self.pages={ROOT+'www1/PrelimProcs.html':'''
Actual abstract with inline emphasis and a separator\u2028 preserved.
Actual abstract
' _,sha=self.sources();out=self.root/'batch';collect(self.cache,sha,out) manifest=json.loads((out/'www-1995-spring-poster.manifest.json').read_text()) self.assertEqual((manifest['count'],manifest['source_listing_count'],manifest['held_entry_count']),(1,2,1)) self.assertNotIn('coverage_previous_count',manifest) # Live parent must be independently preflighted. def test_malformed_href_requires_exact_id_and_two_clean_index_anchors(self): self.pages[SPRING+'papers/abstract.html']=self.pages[SPRING+'papers/abstract.html'].replace('01/paper.html','01/ paper.html') sources,_=self.sources();rows,held,_=spring_rows(sources,'papers');self.assertEqual(held,[]) self.assertEqual(rows[0]['url'],SPRING+'papers/01/paper.html');self.assertTrue(rows[0]['metadata']['url_corroborated_by_author_and_session']) self.assertIn(' ',rows[0]['metadata']['abstract_href_raw']) def test_main_index_disagreement_fails_without_publishing_batch(self): self.pages[SPRING+'papers/session.html']='Other work';_,sha=self.sources();out=self.root/'batch' with self.assertRaisesRegex(ValueError,'ID sets disagree'):collect(self.cache,sha,out) self.assertFalse(out.exists()) def test_same_entry_id_with_conflicting_clean_url_is_rejected(self): self.pages[SPRING+'papers/session.html']='Same contribution';sources,_=self.sources() with self.assertRaisesRegex(ValueError,'URLs disagree'):spring_rows(sources,'papers') def test_poster_without_linked_author_entry_is_held_even_with_abstract(self): self.pages[SPRING+'posters/author.html']='Doe, JaneSame contribution';sources,_=self.sources();rows,held,counts=spring_rows(sources,'posters') self.assertEqual(rows,[]);self.assertEqual(counts['abstract_entries'],1);self.assertEqual(held[0]['reason'],'abstract_entry_missing_linked_author_index') def test_unlinked_and_panel_entries_never_become_acceptance_or_rejection(self): sources,_=self.sources();held,counts=early_review(sources);self.assertEqual(counts,{'first_conference_fields':2,'fall_categories':2,'fall_program_entries':3}) self.assertTrue(any(r['candidate']['title']=='Unlinked talk'for r in held));self.assertTrue(all('status'not in r for r in held)) self.assertEqual(held[0]['candidate']['authors_raw'],['Jane Doe']) def test_title_disagreement_goes_to_review_with_source_claims(self): self.pages[SPRING+'papers/abstract.html']=self.pages[SPRING+'papers/abstract.html'].replace('Same contribution','Different contribution');sources,_=self.sources();rows,held,_=spring_rows(sources,'papers') self.assertEqual(rows,[]);self.assertEqual(held[0]['reason'],'abstract_and_author_index_titles_disagree') def test_cache_tampering_and_manifest_hash_mismatch_fail_closed(self): _,sha=self.sources() with self.assertRaisesRegex(ValueError,'manifest checksum'):Sources(self.cache,'0'*64) p=next(self.cache.glob('source-*.html'));p.write_bytes(p.read_bytes()+b'x') with self.assertRaisesRegex(ValueError,'checksum or size'):Sources(self.cache,sha) def test_cache_cannot_read_paths_outside_snapshot(self): _,sha=self.sources();p=self.cache/'manifest.json';d=json.loads(p.read_text());d['sources'][0]['file']='../outside.html';p.write_text(json.dumps(d)) with self.assertRaisesRegex(ValueError,'plain files'):Sources(self.cache,hashlib.sha256(p.read_bytes()).hexdigest()) def test_keywords_only_is_missing_abstract_not_invented_summary(self): self.pages[SPRING+'papers/abstract.html']='Keywords: networking, hypertext
';sources,_=self.sources();rows,held,_=spring_rows(sources,'papers') self.assertEqual(rows,[]);self.assertEqual(held[0]['reason'],'missing_abstract') def test_old_encoding_and_duplicate_abstract_identity(self): self.assertEqual(decode(b'M\xfcller')[0],'Müller') self.pages[SPRING+'papers/abstract.html']*=2;sources,_=self.sources() with self.assertRaisesRegex(ValueError,'Duplicate abstract'):spring_rows(sources,'papers') def test_existing_output_is_never_overwritten(self): _,sha=self.sources();out=self.root/'batch';out.mkdir();(out/'keep').write_text('original') with self.assertRaisesRegex(ValueError,'overwrite'):collect(self.cache,sha,out) self.assertEqual((out/'keep').read_text(),'original') def review(self,*,authors=None,author_source='canonical_byline',missing='',page=None): url=SPRING+'posters/01/paper.html' self.pages[url]=page or 'Jane Doe, John Roe
Keywords: web
Body
' _,sha=self.sources();raw=self.pages[url].encode()if isinstance(self.pages[url],str)else self.pages[url] row=dict(entry_id='01',individual_url=url,individual_sha256=hashlib.sha256(raw).hexdigest(), title='Canonical full title',authors=authors or ['Jane Doe','John Roe'],author_source=author_source, reason='Reviewed exact canonical entry and its explicit authorship; retain original title variants',missing_abstract_reason=missing) p=self.root/'review.json';p.write_text(json.dumps({'format':1,'source_cache_sha256':sha,'records':[row]}));return sha,p,hashlib.sha256(p.read_bytes()).hexdigest() def test_reviewed_variant_keeps_primary_title_and_all_old_title_claims(self): sha,p,h=self.review();out=self.root/'reviewed';r=collect(self.cache,sha,out,reviewed=p,reviewed_sha256=h) row=json.loads((out/'www-1995-spring-poster.jsonl').read_text());self.assertEqual(row['title'],'Canonical full title') self.assertEqual(row['metadata']['abstract_heading_title_raw'],'Same contribution');self.assertEqual(row['metadata']['author_index_titles_raw'],['Same contribution']) self.assertEqual(len(row['metadata']['source_evidence']),3);self.assertEqual(r['reviewed_posters_sha256'],h) self.assertEqual(row['external_id'],'www3:posters:01');self.assertEqual(row['status'],'accepted') def test_canonical_byline_can_resolve_unlinked_author_index_without_guessing(self): self.pages[SPRING+'posters/author.html']='Doe, JaneBroken index link' sha,p,h=self.review();out=self.root/'reviewed';collect(self.cache,sha,out,reviewed=p,reviewed_sha256=h) row=json.loads((out/'www-1995-spring-poster.jsonl').read_text());self.assertEqual(row['authors'],['Jane Doe','John Roe']) self.assertEqual(row['metadata']['author_index_raw'],[]);self.assertEqual(row['metadata']['author_order'],'canonical_byline') def test_reviewed_missing_abstract_is_explicit_partial_not_body_summary(self): self.pages[SPRING+'posters/abstract.html']='Keywords: web
' sha,p,h=self.review(missing='Neither listing nor canonical page supplies an explicit abstract') out=self.root/'reviewed';collect(self.cache,sha,out,reviewed=p,reviewed_sha256=h) row=json.loads((out/'www-1995-spring-poster.jsonl').read_text());m=json.loads((out/'www-1995-spring-poster.manifest.json').read_text()) self.assertIsNone(row['abstract']);self.assertEqual((m['missing_abstract_count'],m['status']),(1,'partial')) sha,p,h=self.review() with self.assertRaisesRegex(ValueError,'Missing abstract'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256=h) self.assertFalse((self.root/'bad').exists()) def test_reviewed_names_must_be_in_byline_region_not_citations_or_contact_guess(self): sha,p,h=self.review(authors=['Unrelated Person'],page='Jane Doe
Unrelated Person cited here
') with self.assertRaisesRegex(ValueError,'byline region'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256=h) sha,p,h=self.review(author_source='author_index',authors=['Contact Person']) with self.assertRaisesRegex(ValueError,'author-index'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256=h) def test_review_hash_cache_binding_and_canonical_h1_are_all_required(self): sha,p,h=self.review() with self.assertRaisesRegex(ValueError,'checksum'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256='0'*64) d=json.loads(p.read_text());d['source_cache_sha256']='0'*64;p.write_text(json.dumps(d));h=hashlib.sha256(p.read_bytes()).hexdigest() with self.assertRaisesRegex(ValueError,'binding'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256=h) d['source_cache_sha256']=sha;d['records'][0]['title']='Wrong title';p.write_text(json.dumps(d));h=hashlib.sha256(p.read_bytes()).hexdigest() with self.assertRaisesRegex(ValueError,'canonical H1'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256=h) def test_unlisted_review_entry_and_changed_primary_digest_fail_closed(self): sha,p,h=self.review();d=json.loads(p.read_text());d['records'][0]['individual_sha256']='0'*64;p.write_text(json.dumps(d));h=hashlib.sha256(p.read_bytes()).hexdigest() with self.assertRaisesRegex(ValueError,'page checksum'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256=h) self.pages[SPRING+'posters/abstract.html']=self.pages[SPRING+'posters/abstract.html'].replace('01/paper.html','02/other.html') self.pages[SPRING+'posters/author.html']=self.pages[SPRING+'posters/author.html'].replace('01/paper.html','02/other.html') sha,p,h=self.review() with self.assertRaisesRegex(ValueError,'outside abstract'):collect(self.cache,sha,self.root/'bad',reviewed=p,reviewed_sha256=h)