from __future__ import annotations
|
|
from collections import OrderedDict
|
from pathlib import Path
|
import hashlib
|
import os
|
import tempfile
|
import unittest
|
from unittest.mock import patch
|
|
from hibor_fast_collection.archive import create_exclusive_bytes, publish_no_replace, validate_pdf
|
from hibor_fast_collection.manifests import (
|
MANIFEST_COLUMNS, write_bytes_create_new, write_manifest_create_new,
|
)
|
from hibor_fast_collection.models import ContractError
|
|
|
PDF = b"%PDF-1.4\n% synthetic metadata-only fixture\n%%EOF\n"
|
|
|
class ArchiveManifestTests(unittest.TestCase):
|
def test_create_publish_and_duplicate(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
root = Path(tmp)
|
staging = root / "stage.part"
|
final = root / "out" / "report.pdf"
|
create_exclusive_bytes(staging, PDF)
|
digest = hashlib.sha256(PDF).hexdigest()
|
result = publish_no_replace(staging, final, expected_sha256=digest)
|
self.assertFalse(result.duplicate)
|
self.assertEqual(result.evidence.sha256, digest)
|
staging2 = root / "stage2.part"
|
create_exclusive_bytes(staging2, PDF)
|
duplicate = publish_no_replace(staging2, final, expected_sha256=digest)
|
self.assertTrue(duplicate.duplicate)
|
|
def test_pdf_magic_rejected(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
path = Path(tmp) / "bad.pdf"
|
create_exclusive_bytes(path, b"not pdf")
|
with self.assertRaises(ContractError): validate_pdf(path)
|
|
def test_publish_uses_final_directory_staging_before_no_replace_link(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
root = Path(tmp)
|
staging = root / "source-volume" / "stage.part"
|
staging.parent.mkdir()
|
create_exclusive_bytes(staging, PDF)
|
final = root / "final-volume" / "report.pdf"
|
digest = hashlib.sha256(PDF).hexdigest()
|
with patch("hibor_fast_collection.archive.os.link", wraps=os.link) as linked:
|
publish_no_replace(staging, final, expected_sha256=digest)
|
source_arg, final_arg = map(Path, linked.call_args.args)
|
self.assertEqual(source_arg.parent, final_arg.parent)
|
self.assertNotEqual(source_arg, staging)
|
|
def test_recovered_publish_staging_is_not_unlinked_by_nonowner(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
root = Path(tmp)
|
staging = root / "source" / "stage.part"
|
staging.parent.mkdir()
|
create_exclusive_bytes(staging, PDF)
|
final = root / "final" / "report.pdf"
|
final.parent.mkdir()
|
digest = hashlib.sha256(PDF).hexdigest()
|
recovered = final.parent / f".{final.name}.{digest[:16]}.hibor-staging"
|
create_exclusive_bytes(recovered, PDF)
|
result = publish_no_replace(staging, final, expected_sha256=digest)
|
self.assertFalse(result.duplicate)
|
self.assertTrue(recovered.is_file(), "nonowner must preserve recovery staging")
|
self.assertEqual((recovered.read_bytes(), final.read_bytes()), (PDF, PDF))
|
|
def test_delivery_bytes_exact_replay_and_drift(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
target = Path(tmp) / "delivery.md"
|
first = write_bytes_create_new(target, b"delivery-v1\n")
|
second = write_bytes_create_new(target, b"delivery-v1\n")
|
self.assertEqual(first, second)
|
with self.assertRaises(ContractError):
|
write_bytes_create_new(target, b"delivery-v2\n")
|
|
def test_manifest_exact_48_columns(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
row = OrderedDict((k, "") for k in MANIFEST_COLUMNS)
|
row.update({
|
"task_id": "T", "requested_by": "R", "review_owner": "R", "source_url": "UNKNOWN",
|
"source_site": "慧博 APP(安卓模拟器本地缓存)", "title": "Title", "publisher": "Pub",
|
"report_date": "2026-07-29", "downloaded_at": "2026-07-29T00:00:00Z",
|
"http_status": "NOT_APPLICABLE_APP_CACHE", "content_type": "application/pdf",
|
"file_name": "x.pdf", "relative_path": "x.pdf", "bytes": "10", "sha256": "a"*64,
|
"download_status": "SUCCESS", "error_or_note": "NONE", "source_cache_path": "/cache/x",
|
"source_file_name": "x", "android_package": "cn.com.hibor", "extension_added": "false",
|
"pdf_magic_valid": "true", "remote_sha256": "a"*64, "local_sha256": "a"*64,
|
"openability": "true", "page_count": "1", "encryption_status": "false",
|
"schema_version": "V003", "row_id": "ROW", "handoff_id": "H", "run_id": "RUN",
|
"item_id": "I", "slot_id": "S", "query": "Q", "candidate_id": "C",
|
"report_identity": "RID", "analysts": "", "selection_reason": "TEST",
|
"remote_bytes": "10", "local_bytes": "10", "status": "SUCCESS", "stop_code": "",
|
"reused_without_new_trigger": "false", "external_evidence_hash": "a"*64,
|
"manifested_at_utc": "2026-07-29T00:00:00Z",
|
})
|
size, digest = write_manifest_create_new(Path(tmp) / "manifest.csv", [row])
|
self.assertGreater(size, 0)
|
self.assertEqual(len(digest), 64)
|
replay = write_manifest_create_new(Path(tmp) / "manifest.csv", [row])
|
self.assertEqual(replay, (size, digest))
|
changed = row.copy()
|
changed["title"] = "Changed"
|
with self.assertRaises(ContractError):
|
write_manifest_create_new(Path(tmp) / "manifest.csv", [changed])
|
|
|
if __name__ == "__main__": unittest.main()
|