"""Door B inbox provenance rendering + filename slugging (Phase 2 step 2). Pure functions, zero runtime dependency, no guard call: the slug reduces a dropped file's name to the Phase 1 id grammar, the `inbox-` namespace keeps generated concepts disjoint from `index.md`/`ingest-*`/`promoted-verdict-*`, and the provenance layer carries the §7-analogous honesty marker. The verdict reservation applies here unchanged — the promotion gate stays the only path into that layer. """ from __future__ import annotations import hashlib import re import pytest from llm_ingestion_okf import MaterializationError from llm_ingestion_okf.inbox import inbox_filename, inbox_slug, render_inbox_concept INGESTED_AT = "2026-07-25T12:00:00Z" # Phase 1's id grammar (manifest.py `_ID_PATTERN`), restated independently so a # change to that constant cannot silently widen what the inbox namespace admits. ID_GRAMMAR = re.compile(r"[a-z0-9][a-z0-9-]*\Z") # Names chosen to attack the grammar from every side: spaces, punctuation runs, # case, unicode, path-ish characters, leading/trailing separators, digits first. NASTY_NAMES = [ "My Notes 2026.md", " padded .txt", "UPPER.CSV", "a__b c.txt", "--weird--.md", "2026-report.md", "caf\u00e9.md", "re:port(final)[v2].md", "dots.in.name.md", "tab\tseparated.txt", ] # --- slug: reduction to the Phase 1 id grammar --- def test_slug_reduces_filename_to_the_phase_1_id_grammar() -> None: assert inbox_slug("My Notes 2026.md") == "my-notes-2026" def test_slug_drops_the_extension() -> None: assert inbox_slug("report.csv") == "report" def test_slug_keeps_inner_dots_as_separators() -> None: # A dot is not in the grammar; only the final extension is special. assert inbox_slug("dots.in.name.md") == "dots-in-name" def test_slug_collapses_separator_runs_to_a_single_hyphen() -> None: assert inbox_slug("a__b c.txt") == "a-b-c" def test_slug_strips_leading_and_trailing_separators() -> None: assert inbox_slug("--weird--.md") == "weird" def test_slug_lowercases() -> None: assert inbox_slug("UPPER.CSV") == "upper" def test_slug_allows_a_leading_digit() -> None: # The Phase 1 grammar admits `[a-z0-9]` as the first character. assert inbox_slug("2026-report.md") == "2026-report" def test_slug_replaces_non_ascii_rather_than_transliterating() -> None: # No guessing at a romanisation: every character outside the grammar is a # separator. Transliterating would be a semantic claim the slugger cannot # make - Norwegian "m\u00f8te" (meeting) would become "mote" (fashion). # The readable name survives verbatim in `title:`; the slug is an id. assert inbox_slug("caf\u00e9.md") == "caf" def test_slug_is_stable_across_unicode_normalisation_forms() -> None: """macOS (APFS) hands filenames over DECOMPOSED. Without NFC normalisation the combining mark alone becomes a separator and the base letter survives, so one visual filename would slug two different ways depending on whether it arrived from a directory listing or from an operator-typed string. Escapes, not literals: the test must not depend on this file's own form. """ composed = "caf\u00e9.md" # NFC: e-acute as one code point decomposed = "cafe\u0301.md" # NFD: plain e + combining acute assert composed != decomposed assert inbox_slug(composed) == inbox_slug(decomposed) == "caf" def test_slug_does_not_transliterate_nordic_letters() -> None: assert inbox_slug("m\u00f8te-notat.md") == "m-te-notat" @pytest.mark.parametrize("name", NASTY_NAMES) def test_slug_output_always_matches_the_id_grammar(name: str) -> None: assert ID_GRAMMAR.match(inbox_slug(name)) def test_slug_is_deterministic() -> None: assert inbox_slug("My Notes 2026.md") == inbox_slug("My Notes 2026.md") def test_slug_of_a_hidden_file_uses_its_leading_dot_name() -> None: # A dotfile has no extension to drop: `.hidden.md` stems to `.hidden`. assert inbox_slug(".hidden.md") == "hidden" @pytest.mark.parametrize("name", ["---.md", "!!!.txt", "\u65e5\u672c.md", ""]) def test_slug_rejects_a_name_that_reduces_to_nothing(name: str) -> None: # Never invent a fallback name: an unusable filename is a typed per-file # failure the caller reports, not a silent `untitled`. with pytest.raises(MaterializationError) as excinfo: inbox_slug(name) assert excinfo.value.code == "inbox_slug_empty" # --- filename: the `inbox-` namespace --- def test_inbox_filename_namespaces_the_slug() -> None: assert inbox_filename("my-notes") == "inbox-my-notes.md" @pytest.mark.parametrize("name", NASTY_NAMES) def test_inbox_namespace_is_disjoint_from_the_reserved_names(name: str) -> None: """Namespace safety: no admissible slug can collide with `index.md`, a Door A concept (`ingest-*`), or a promoted verdict (`promoted-verdict-*`). """ generated = inbox_filename(inbox_slug(name)) assert generated != "index.md" assert not generated.startswith("ingest-") assert not generated.startswith("promoted-verdict-") assert generated.startswith("inbox-") # --- filename length: the one gate the filesystem enforces for us otherwise --- # NAME_MAX restated independently of the module constant, for the same reason # ID_GRAMMAR is: a change to the library's value must not silently move the # boundary this suite pins. Verified empirically on APFS 2026-07-25 (a 255-byte # component writes, a 258-byte one raises errno 63 ENAMETOOLONG); ext4 and NTFS # use the same 255 limit, and the slug is ASCII-only so bytes == characters. NAME_MAX = 255 # `inbox-` (6) + `.md` (3) is the namespace's fixed overhead. LONGEST_ADMISSIBLE_SLUG = NAME_MAX - len("inbox-") - len(".md") def test_longest_admissible_slug_is_246_characters() -> None: # Pins the arithmetic itself: if the namespace prefix or suffix ever # changes, this fails before the length tests below start lying. assert LONGEST_ADMISSIBLE_SLUG == 246 def test_inbox_filename_at_the_length_limit_is_accepted() -> None: slug = "a" * LONGEST_ADMISSIBLE_SLUG generated = inbox_filename(slug) assert len(generated.encode("utf-8")) == NAME_MAX def test_inbox_filename_over_the_length_limit_is_rejected() -> None: """A name the filesystem cannot hold is a typed per-file failure, not an untyped OSError from the write and not a truncation. Truncating would be lossy AND collision-prone: two long names sharing a prefix would reduce to one filename, and the second write would silently claim the first's file. Refusing is the same posture as `inbox_slug_empty` — the library never invents a filename the operator did not give it. """ slug = "a" * (LONGEST_ADMISSIBLE_SLUG + 1) with pytest.raises(MaterializationError) as excinfo: inbox_filename(slug) assert excinfo.value.code == "inbox_slug_too_long" def test_length_rejection_names_the_limit_and_the_actual_size() -> None: # The operator's fix is to rename the dropped file, so the message has to # say by how much — a bare "too long" leaves them guessing. slug = "a" * 300 with pytest.raises(MaterializationError) as excinfo: inbox_filename(slug) message = str(excinfo.value) assert str(NAME_MAX) in message # 6 + 300 + 3 — the size the name WOULD have had. assert "309" in message def test_a_long_dropped_filename_fails_at_the_filename_not_at_the_write() -> None: """End-to-end on the pure functions: a 300-character dropped name reduces to a 300-character slug, and the gate catches it before any I/O is attempted — errno differs across platforms (63 on macOS, 36 on Linux), so catching OSError by number was never a portable option. """ with pytest.raises(MaterializationError) as excinfo: inbox_filename(inbox_slug("x" * 300 + ".md")) assert excinfo.value.code == "inbox_slug_too_long" # --- provenance rendering --- def test_frontmatter_keys_in_exact_order() -> None: out = render_inbox_concept( "Body text", okf_type="note", title="My Notes", source_file="My Notes 2026.md", source_bytes=b"original", ingested_at=INGESTED_AT, ) digest = hashlib.sha256(b"original").hexdigest() assert out == ( "---\n" "type: note\n" "title: My Notes\n" "source_file: My Notes 2026.md\n" f"source_sha256: {digest}\n" f"ingested_at: {INGESTED_AT}\n" "generated: true\n" "---\n" "\n" "Body text\n" ) def test_source_sha256_is_over_the_original_bytes_not_the_extracted_text() -> None: """The honesty marker must point at what was DROPPED, not at what the extractor produced — otherwise provenance cannot be re-verified against the operator's file. """ raw = b"name\nAda\n" text = "| name |\n| --- |\n| Ada |\n" out = render_inbox_concept( text, okf_type="note", title="Data", source_file="data.csv", source_bytes=raw, ingested_at=INGESTED_AT, ) assert f"source_sha256: {hashlib.sha256(raw).hexdigest()}\n" in out assert hashlib.sha256(text.encode("utf-8")).hexdigest() not in out def test_body_is_lf_only_with_exactly_one_trailing_newline() -> None: out = render_inbox_concept( "line one\r\nline two\r\n\n\n", okf_type="note", title="T", source_file="f.md", source_bytes=b"x", ingested_at=INGESTED_AT, ) assert "\r" not in out assert out.endswith("line one\nline two\n") def test_rendering_is_deterministic() -> None: kwargs = { "okf_type": "note", "title": "T", "source_file": "f.md", "source_bytes": b"x", "ingested_at": INGESTED_AT, } assert render_inbox_concept("body", **kwargs) == render_inbox_concept("body", **kwargs) # --- fail-fast gates --- @pytest.mark.parametrize("moment", ["2026-07-25", "2026-07-25T12:00:00", "not-a-time", ""]) def test_ingested_at_must_be_iso_8601_utc_with_a_z_suffix(moment: str) -> None: with pytest.raises(MaterializationError) as excinfo: render_inbox_concept( "body", okf_type="note", title="T", source_file="f.md", source_bytes=b"x", ingested_at=moment, ) assert excinfo.value.code == "ingested_at_invalid" @pytest.mark.parametrize("okf_type", ["verdict", "Verdict", "VERDICT"]) def test_verdict_okf_type_rejected(okf_type: str) -> None: """Verdict reservation, second door: the promotion gate remains the only path into that layer — the inbox can never write one. """ with pytest.raises(MaterializationError) as excinfo: render_inbox_concept( "body", okf_type=okf_type, title="T", source_file="f.md", source_bytes=b"x", ingested_at=INGESTED_AT, ) assert excinfo.value.code == "okf_type_reserved" @pytest.mark.parametrize("title", ["a[b", "a]b", "two\nlines", "carriage\rreturn"]) def test_title_that_would_break_an_index_link_or_frontmatter_rejected(title: str) -> None: # Same invariant as Door A's manifest validation: the title is rendered # verbatim into `- [title](target)` and into line-oriented frontmatter. with pytest.raises(MaterializationError) as excinfo: render_inbox_concept( "body", okf_type="note", title=title, source_file="f.md", source_bytes=b"x", ingested_at=INGESTED_AT, ) assert excinfo.value.code == "inbox_title_invalid" @pytest.mark.parametrize("source_file", ["two\nlines.md", "carriage\rreturn.md"]) def test_source_file_that_would_inject_frontmatter_lines_rejected(source_file: str) -> None: with pytest.raises(MaterializationError) as excinfo: render_inbox_concept( "body", okf_type="note", title="T", source_file=source_file, source_bytes=b"x", ingested_at=INGESTED_AT, ) assert excinfo.value.code == "inbox_source_file_invalid"