Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,940 changes: 1,940 additions & 0 deletions ledger/bench-ledger.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions ledger/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from typing import Any

_DEFAULT_LEDGER_PATH: str = "ledger/bench-ledger.json"
_META_FILENAME: str = "ledger-meta.json"
META_FILENAME: str = "ledger-meta.json"
_GENESIS_MARKER: str = "GENESIS"
_MAX_FIELD_CHARS: int = 10_000
_MAX_STAGE_CHARS: int = 50_000
Expand Down Expand Up @@ -174,7 +174,7 @@ def append_entry(
existing.append(entry)
_atomic_write_json(file_path, existing)

meta_path: Path = directory / _META_FILENAME
meta_path: Path = directory / META_FILENAME
_update_meta(meta_path, entry, len(existing))

return entry
Expand Down
6 changes: 3 additions & 3 deletions ledger/ledger-meta.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"entry_count": 130,
"latest_hash": "ac0df6710fa2d5a0e8373ae6c04934dffb337c877aae29c14d09d54c6c6acf64",
"entry_count": 149,
"latest_hash": "111e118ea91ff3a369abbd390dabe36995af6f2a1270b2e529a9598733e9c114",
"created": "2026-04-22T04:00:32.627154+00:00",
"last_updated": "2026-07-12T19:44:20.700973+00:00"
"last_updated": "2026-07-12T21:26:15.915181+00:00"
}
5 changes: 3 additions & 2 deletions ledger/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from the GENESIS entry through to the latest. This module deliberately
does not call ``load_ledger`` from ``chain.py`` — independence from the
write path is the whole point of an auditor. Only ``compute_entry_hash``
is shared, because the hashing algorithm must match by construction.
and ``META_FILENAME`` are shared, because the hashing algorithm and the
meta-anchor filename must match the writer by construction.

The validator reports the first failure it encounters (one bad entry is
enough to invalidate the chain) along with enough context to pinpoint
Expand All @@ -17,11 +18,11 @@
from pathlib import Path
from typing import Any

from ledger.chain import META_FILENAME as _META_FILENAME
from ledger.chain import compute_entry_hash

_DEFAULT_LEDGER_PATH: str = "ledger/bench-ledger.json"
_GENESIS_MARKER: str = "GENESIS"
_META_FILENAME: str = "ledger-meta.json"


def verify_chain(path: str = _DEFAULT_LEDGER_PATH) -> dict:
Expand Down
8 changes: 8 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Mark the test suite as a regular package.

A regular package (rather than an implicit namespace package) keeps
``python -m unittest tests.test_x`` and ``from tests._ledger_fixtures
import ...`` resolving to this directory even when an unrelated
``tests`` package exists elsewhere on sys.path, because a regular
package anywhere on the path outranks every namespace portion.
"""
54 changes: 54 additions & 0 deletions tests/_ledger_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Shared synthetic-ledger fixtures for the test suite.

Single source of truth for building correctly-linked hash chains so the
entry shape lives in one place. Named with a leading underscore so
unittest discovery (pattern ``test*.py``) never collects it as a test
module; import it as ``from tests._ledger_fixtures import
build_valid_chain`` (the repo root is on sys.path under discovery,
module-name, and script-style runs, and ``tests/__init__.py`` makes the
package resolve here even if another ``tests`` exists on sys.path).
"""

import sys
from pathlib import Path
from typing import Any

_REPO_ROOT: Path = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))

from ledger.chain import compute_entry_hash # noqa: E402


def build_valid_chain(
n: int, verdicts: list[str] | None = None
) -> list[dict]:
"""Build a correctly-linked chain of n entries starting from GENESIS.

Each entry carries an oracle verdict ("PASS" unless overridden via
``verdicts``); VETO entries get a single C-001 VIOLATED citation so
stats and viewer assertions have something to count.
"""
entries: list[dict] = []
for i in range(n):
verdict: str = verdicts[i] if verdicts else "PASS"
entry: dict[str, Any] = {
"entry_id": f"id-{i}",
"timestamp": f"2026-01-01T00:00:{i:02d}+00:00",
"previous_hash": (
"GENESIS" if i == 0 else entries[i - 1]["entry_hash"]
),
"constitution_hash": "abc",
"change": {"file": f"file_{i}.py", "tool": "Write"},
"oracle": {
"verdict": verdict,
"constraint_citations": (
[{"constraint_id": "C-001", "disposition": "VIOLATED"}]
if verdict == "VETO"
else []
),
},
}
entry["entry_hash"] = compute_entry_hash(entry)
entries.append(entry)
return entries
1 change: 0 additions & 1 deletion tests/test_challenger.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import sys
import unittest
from typing import Any
from unittest.mock import MagicMock, patch

from pathlib import Path
Expand Down
1 change: 0 additions & 1 deletion tests/test_defender.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import sys
import unittest
from typing import Any
from unittest.mock import MagicMock, patch

from pathlib import Path
Expand Down
1 change: 0 additions & 1 deletion tests/test_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import sys
import unittest
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

_REPO_ROOT: Path = Path(__file__).resolve().parent.parent
Expand Down
1 change: 0 additions & 1 deletion tests/test_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import sys
import unittest
from typing import Any
from unittest.mock import MagicMock, patch

from pathlib import Path
Expand Down
3 changes: 1 addition & 2 deletions tests/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@

import sys
import unittest
from typing import Any
from unittest.mock import MagicMock, call, patch
from unittest.mock import MagicMock, patch

from pathlib import Path

Expand Down
24 changes: 6 additions & 18 deletions tests/test_verify.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""Tests for ledger.verify chain validation and tamper detection.
"""Tests for ledger.verify: chain validation and tamper detection.

Covers: verify_chain across all 7 failure types (READ_ERROR, PARSE_ERROR,
SCHEMA_ERROR, HASH_MISMATCH, INVALID_GENESIS, CHAIN_BREAK, META_MISMATCH),
plus valid chains of varying lengths and the ledger-meta.json anchor.

Synthetic chains come from the shared fixture module
tests/_ledger_fixtures.py (build_valid_chain), which already exists in
the repository and is the single source of truth for the entry shape.

Run: python -m unittest tests.test_verify -v
"""

Expand All @@ -14,32 +18,16 @@
import tempfile
import unittest
from pathlib import Path
from typing import Any

_REPO_ROOT: Path = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))

from tests._ledger_fixtures import build_valid_chain as _build_valid_chain # noqa: E402
from ledger.chain import compute_entry_hash # noqa: E402
from ledger.verify import verify_chain # noqa: E402


def _build_valid_chain(n: int) -> list[dict]:
"""Build a correctly-linked chain of n entries starting from GENESIS."""
entries: list[dict] = []
for i in range(n):
entry: dict[str, Any] = {
"entry_id": f"id-{i}",
"timestamp": f"2026-01-01T00:00:{i:02d}+00:00",
"previous_hash": "GENESIS" if i == 0 else entries[i - 1]["entry_hash"],
"constitution_hash": "abc",
"change": {"file": "test.py", "tool": "Write"},
}
entry["entry_hash"] = compute_entry_hash(entry)
entries.append(entry)
return entries


class VerifyChainValidTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp: str = tempfile.mkdtemp()
Expand Down
39 changes: 9 additions & 30 deletions tests/test_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
on disk: document structure, stats banner values, chain status labels,
JSON embedding safety, and the never-raises error page fallback.

Run: python -m unittest discover -s tests -p test_viewer.py -v
Synthetic chains come from the shared fixture module
tests/_ledger_fixtures.py (build_valid_chain), the single source of
truth for the entry shape.

Run: python -m unittest tests.test_viewer -v
"""

import json
Expand All @@ -14,42 +18,17 @@
import tempfile
import unittest
from pathlib import Path
from typing import Any
from unittest.mock import patch

_REPO_ROOT: Path = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))

from tests._ledger_fixtures import build_valid_chain as _build_valid_chain # noqa: E402
from ledger.chain import compute_entry_hash # noqa: E402
from utils.viewer import generate_viewer_html # noqa: E402


def _build_valid_chain(n: int, verdicts: list[str] | None = None) -> list[dict]:
"""Build a correctly-linked chain of n entries with oracle verdicts."""
entries: list[dict] = []
for i in range(n):
verdict: str = verdicts[i] if verdicts else "PASS"
entry: dict[str, Any] = {
"entry_id": f"id-{i}",
"timestamp": f"2026-01-01T00:00:{i:02d}+00:00",
"previous_hash": "GENESIS" if i == 0 else entries[i - 1]["entry_hash"],
"constitution_hash": "abc",
"change": {"file": f"file_{i}.py", "tool": "Write"},
"oracle": {
"verdict": verdict,
"constraint_citations": (
[{"constraint_id": "C-001", "disposition": "VIOLATED"}]
if verdict == "VETO"
else []
),
},
}
entry["entry_hash"] = compute_entry_hash(entry)
entries.append(entry)
return entries


class GenerateViewerHtmlTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp: str = tempfile.mkdtemp()
Expand All @@ -74,9 +53,9 @@ def test_valid_ledger_renders_stats_and_chain_status(self) -> None:
)
self._write_chain(chain)
html_out: str = generate_viewer_html(self._path())
self.assertIn(">VALID<", html_out)
self.assertIn("2 <span class=\"pct\">(66.7%)</span>", html_out)
self.assertIn("1 <span class=\"pct\">(33.3%)</span>", html_out)
self.assertIn('"status": "VALID"', html_out)
self.assertIn("(66.7%)", html_out)
self.assertIn("(33.3%)", html_out)
self.assertIn("C-001 (1 veto(es))", html_out)
self.assertIn("file_1.py", html_out)

Expand Down
Loading