diff --git a/tests/test_io.py b/tests/test_io.py index c150d24..9dcdf93 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -59,6 +59,18 @@ def test_npz_mmap_returns_memmap(tmp_path): assert isinstance(out["values"], np.memmap) +def test_npz_mmap_is_read_only(tmp_path): + # The mmap load opens with mmap_mode="r": the buffer must be non-writeable and + # an in-place write must raise — previously asserted only as isinstance(memmap) + # (2026-07 audit, F2 / register C-70; the read-only-ness is the safety property). + st = _state_2d() + npz.save(tmp_path, **st) + out = npz.load(tmp_path, mmap=True) + assert out["values"].flags.writeable is False + with pytest.raises(ValueError, match="read-only"): + out["values"][0, 0] = 99.0 + + def test_npz_builds_index_from_state(tmp_path): st = _state_2d() npz.save(tmp_path, **st) diff --git a/tests/test_reconciliation_parity.py b/tests/test_reconciliation_parity.py index fdf48e9..e5d83cb 100644 --- a/tests/test_reconciliation_parity.py +++ b/tests/test_reconciliation_parity.py @@ -78,3 +78,18 @@ def test_sample_count_mismatch_raises(self): country = np.zeros(200, dtype=np.float32) with pytest.raises(ValueError, match="Mismatch in sample count"): reconcile_proportional(grid, country) + + def test_shares_are_preserved_within_each_draw(self): + # The method's DEFINING property — top-down *forecast-proportion* + # disaggregation: within a draw, every cell keeps its relative share of + # the nonzero mass. Until now this was pinned only by the frozen-oracle + # fixtures (an equal-split-among-nonzero mutation would have passed every + # law test); the 2026-07 audit (F5) proved the law holds — commit it as a + # law so the essence is oracle-independent (register C-70). + rng = np.random.default_rng(3) + grid = rng.gamma(2.0, 5.0, size=(50, 8)).astype(np.float32) + country = grid.sum(axis=1) * np.float32(1.7) + adjusted = reconcile_proportional(grid, country) + in_shares = grid / grid.sum(axis=1, keepdims=True) + out_shares = adjusted / adjusted.sum(axis=1, keepdims=True) + np.testing.assert_allclose(out_shares, in_shares, atol=1e-6) diff --git a/tests/test_reconciliation_validation.py b/tests/test_reconciliation_validation.py index 46ea2a5..66a8982 100644 --- a/tests/test_reconciliation_validation.py +++ b/tests/test_reconciliation_validation.py @@ -85,3 +85,12 @@ def test_missing_country_raises(self, fix): vals=fix["cm__pred_ged_sb"][keep]) with pytest.raises(ValueError, match="no country forecast"): validate_reconciliation_inputs(cm, _pgm(fix), mk, mv) + + def test_missing_mapping_entry_raises(self, fix): + # Reconcile.md §6: a grid row whose (time, priogrid_gid) is absent from the + # injected mapping fails loud (via cross_level_align). Every other "missing" + # test drops a cm row; this one drops a MAP KEY — previously pinned only at + # the leaf, never from the reconcile suite (2026-07 audit / register C-70). + mk, mv = _mk(fix) + with pytest.raises(ValueError, match="no entry in the injected mapping"): + validate_reconciliation_inputs(_cm(fix), _pgm(fix), mk[:-1], mv[:-1])