Skip to content

Commit ab59bbb

Browse files
author
TNFR AI Agent
committed
Make release validation portable across clean test environments
1 parent 023f5c3 commit ab59bbb

11 files changed

Lines changed: 202 additions & 64 deletions

‎.github/WORKFLOWS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ percentage, prove physical theorems or certify every optional backend. Audit
2525
coverage excludes optional compute, documentation and deployment environments
2626
unless their dependencies happen to be installed through the declared groups.
2727

28+
Each Python test job attempts to retain its JUnit report for 14 days, including
29+
when tests fail. A report from an interrupted run covers only the tests reached;
30+
its presence does not establish completion or success.
31+
2832
## Documentation publication
2933

3034
The documentation workflow validates repository sources, stages them through

‎.github/workflows/ci.yml‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,19 @@ jobs:
119119

120120
- name: Run unit tests
121121
if: matrix.python-version != '3.11'
122-
run: python -m pytest
122+
run: python -m pytest --junitxml=test-results/pytest.xml
123123

124124
- name: Run unit tests with coverage
125125
if: matrix.python-version == '3.11'
126126
run: |
127-
python -m coverage run --source=src -m pytest
127+
python -m coverage run --source=src -m pytest --junitxml=test-results/pytest.xml
128128
python -m coverage report -m
129+
130+
- name: Retain test results
131+
if: always()
132+
uses: actions/upload-artifact@v4
133+
with:
134+
name: pytest-python-${{ matrix.python-version }}
135+
path: test-results/pytest.xml
136+
if-no-files-found: warn
137+
retention-days: 14

‎tests/c6_lineage_fixtures.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Portable byte-lineage inputs, deliberately not scientific C6 evidence."""
2+
3+
import hashlib
4+
import json
5+
from copy import deepcopy
6+
7+
8+
def write_synthetic_c6_lineage(directory, count):
9+
"""Write a minimal B26-style input DAG for CLI prevalidation only.
10+
11+
Records contain matching producer metadata and hashes, but no nodal states
12+
or admitted experiment manifest. Tests must stop at the analysis boundary;
13+
accepting these byte links does not admit a scientific report.
14+
"""
15+
if not 2 <= count <= 6:
16+
raise ValueError("the byte-lineage fixture supports two through six inputs")
17+
key_rows = (
18+
(),
19+
("input_evidence",),
20+
("input_evidence", "source_input_evidence"),
21+
("input_evidence", "parent_input_evidence", "source_input_evidence"),
22+
(
23+
"input_evidence",
24+
"parent_input_evidence",
25+
"previous_input_evidence",
26+
"source_input_evidence",
27+
),
28+
(
29+
"input_evidence",
30+
"parent_input_evidence",
31+
"previous_input_evidence",
32+
"earlier_input_evidence",
33+
"source_input_evidence",
34+
),
35+
)
36+
paths = tuple(directory / f"source_{index}.json" for index in range(count))
37+
records = [None] * count
38+
raw = [None] * count
39+
for index in reversed(range(count)):
40+
record = {
41+
"manifest": {"claim_id": f"synthetic-byte-source-{index}"},
42+
"source_scope": ["synthetic/byte-prevalidation-only"],
43+
}
44+
for key, target in zip(
45+
key_rows[count - index - 1], range(index + 1, count), strict=True
46+
):
47+
record[key] = {
48+
"sha256": hashlib.sha256(raw[target]).hexdigest(),
49+
"producer_manifest": deepcopy(records[target]["manifest"]),
50+
"producer_source_scope": list(records[target]["source_scope"]),
51+
}
52+
records[index] = record
53+
raw[index] = (json.dumps(record, sort_keys=True) + "\n").encode("utf-8")
54+
paths[index].write_bytes(raw[index])
55+
return paths

‎tests/physics/test_c6_winding_coupled_budget_report.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111

1212
from benchmarks import c6_winding_coupled_budget as campaign
13+
from tests.c6_lineage_fixtures import write_synthetic_c6_lineage
1314

1415
DIRECTORY = Path(__file__).resolve().parents[2] / "artifacts/research"
1516
INPUTS = tuple(DIRECTORY / name for name in campaign.INPUT_NAMES)
@@ -521,16 +522,18 @@ def test_cli_binds_six_input_hashes_and_output_source_identity(
521522
def test_cli_validates_all_historical_byte_links_before_analysis(
522523
tmp_path, monkeypatch, changed_index
523524
):
524-
paths = tuple(tmp_path / f"source_{i}.json" for i in range(6))
525-
for i, (path, source) in enumerate(zip(paths, INPUTS, strict=True)):
526-
path.write_bytes(source.read_bytes() + (b"\n" if i == changed_index else b""))
525+
paths = write_synthetic_c6_lineage(tmp_path, 6)
527526
output = tmp_path / "report.json"
528527
_cli(monkeypatch, paths, output)
529528

530529
def forbidden(*args):
531530
raise AssertionError("invalid historical hashes must fail before analysis")
532531

533532
monkeypatch.setattr(campaign, "analyze_c6_winding_coupled_budget", forbidden)
533+
monkeypatch.setattr(campaign, "current_git_source_provenance", lambda *args: None)
534+
with pytest.raises(AssertionError, match="invalid historical hashes"):
535+
campaign.main()
536+
paths[changed_index].write_bytes(paths[changed_index].read_bytes() + b"\n")
534537
with pytest.raises(ValueError, match="lineage hashes"):
535538
campaign.main()
536539
assert not output.exists()

‎tests/physics/test_c6_winding_pressure_levels_report.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import pytest
1313

1414
from benchmarks import c6_winding_pressure_levels as campaign
15+
from tests.c6_lineage_fixtures import write_synthetic_c6_lineage
1516

1617
DIRECTORY = Path(__file__).resolve().parents[2] / "artifacts/research"
1718
INPUTS = tuple(
@@ -508,16 +509,18 @@ def test_cli_retains_four_input_hashes_and_distinct_output_source_provenance(
508509
def test_cli_rejects_wrong_historical_bytes_before_analysis(
509510
tmp_path, monkeypatch, changed_index
510511
):
511-
paths = tuple(tmp_path / f"source_{i}.json" for i in range(4))
512-
for i, (path, source) in enumerate(zip(paths, INPUTS, strict=True)):
513-
path.write_bytes(source.read_bytes() + (b"\n" if i == changed_index else b""))
512+
paths = write_synthetic_c6_lineage(tmp_path, 4)
514513
output = tmp_path / "report.json"
515514
_cli(monkeypatch, paths, output)
516515

517516
def forbidden(*args):
518517
raise AssertionError("wrong historical bytes must fail before replay")
519518

520519
monkeypatch.setattr(campaign, "analyze_c6_winding_pressure_levels", forbidden)
520+
monkeypatch.setattr(campaign, "current_git_source_provenance", lambda *args: None)
521+
with pytest.raises(AssertionError, match="wrong historical bytes"):
522+
campaign.main()
523+
paths[changed_index].write_bytes(paths[changed_index].read_bytes() + b"\n")
521524
with pytest.raises(ValueError, match="lineage hashes"):
522525
campaign.main()
523526
assert not output.exists()

‎tests/physics/test_c6_winding_pressure_repayment_report.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import pytest
1212

1313
from benchmarks import c6_winding_pressure_repayment as campaign
14+
from tests.c6_lineage_fixtures import write_synthetic_c6_lineage
1415

1516
DIRECTORY = Path(__file__).resolve().parents[2] / "artifacts/research"
1617
INPUTS = tuple(
@@ -452,9 +453,7 @@ def test_cli_retains_three_source_hashes_and_separate_producer_manifest(
452453
def test_cli_checks_parent_and_source_byte_lineage_before_analysis(
453454
tmp_path, monkeypatch, wrong_index
454455
):
455-
paths = tuple(tmp_path / f"input_{i}.json" for i in range(3))
456-
for i, (path, source) in enumerate(zip(paths, INPUTS, strict=True)):
457-
path.write_bytes(source.read_bytes() + (b"\n" if i == wrong_index else b""))
456+
paths = write_synthetic_c6_lineage(tmp_path, 3)
458457
output = tmp_path / "result.json"
459458
monkeypatch.setattr(
460459
sys,
@@ -476,6 +475,10 @@ def forbidden(*args):
476475
raise AssertionError("lineage failure must precede analysis")
477476

478477
monkeypatch.setattr(campaign, "analyze_c6_winding_pressure_repayment", forbidden)
478+
monkeypatch.setattr(campaign, "current_git_source_provenance", lambda *args: None)
479+
with pytest.raises(AssertionError, match="lineage failure must precede analysis"):
480+
campaign.main()
481+
paths[wrong_index].write_bytes(paths[wrong_index].read_bytes() + b"\n")
479482
with pytest.raises(ValueError, match="lineage hashes"):
480483
campaign.main()
481484
assert not output.exists()

‎tests/physics/test_c6_winding_pressure_sign_report.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111

1212
from benchmarks import c6_winding_pressure_sign as campaign
13+
from tests.c6_lineage_fixtures import write_synthetic_c6_lineage
1314

1415
ARTIFACTS = Path(__file__).resolve().parents[2] / "artifacts/research"
1516
PARENT = ARTIFACTS / "c6_winding_carry_itinerary.json"
@@ -461,11 +462,8 @@ def test_cli_binds_both_distinct_input_hashes_and_retains_historical_sources(
461462

462463

463464
def test_cli_rejects_wrong_source_bytes_before_replay(tmp_path, monkeypatch):
464-
parent_path, source_path, output = (
465-
tmp_path / name for name in ("parent.json", "source.json", "output.json")
466-
)
467-
parent_path.write_bytes(PARENT.read_bytes())
468-
source_path.write_bytes(SOURCE.read_bytes() + b"\n")
465+
parent_path, source_path = write_synthetic_c6_lineage(tmp_path, 2)
466+
output = tmp_path / "output.json"
469467
monkeypatch.setattr(
470468
sys,
471469
"argv",
@@ -484,6 +482,10 @@ def forbidden(*args):
484482
raise AssertionError("the wrong source bytes must fail before replay")
485483

486484
monkeypatch.setattr(campaign, "analyze_c6_winding_pressure_sign", forbidden)
485+
monkeypatch.setattr(campaign, "current_git_source_provenance", lambda *args: None)
486+
with pytest.raises(AssertionError, match="the wrong source bytes"):
487+
campaign.main()
488+
source_path.write_bytes(source_path.read_bytes() + b"\n")
487489
with pytest.raises(ValueError, match="lineage hash"):
488490
campaign.main()
489491
assert not output.exists()

‎tests/physics/test_c6_winding_pressure_stencil_report.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111

1212
from benchmarks import c6_winding_pressure_stencil as campaign
13+
from tests.c6_lineage_fixtures import write_synthetic_c6_lineage
1314

1415
DIRECTORY = Path(__file__).resolve().parents[2] / "artifacts/research"
1516
INPUTS = tuple(
@@ -497,16 +498,18 @@ def test_cli_retains_all_five_input_hashes_and_distinct_output_source(
497498
def test_cli_rejects_wrong_historical_bytes_before_replay(
498499
tmp_path, monkeypatch, changed_index
499500
):
500-
paths = tuple(tmp_path / f"source_{i}.json" for i in range(5))
501-
for i, (path, source) in enumerate(zip(paths, INPUTS, strict=True)):
502-
path.write_bytes(source.read_bytes() + (b"\n" if i == changed_index else b""))
501+
paths = write_synthetic_c6_lineage(tmp_path, 5)
503502
output = tmp_path / "report.json"
504503
_cli(monkeypatch, paths, output)
505504

506505
def forbidden(*args):
507506
raise AssertionError("wrong historical bytes must fail before analysis")
508507

509508
monkeypatch.setattr(campaign, "analyze_c6_winding_pressure_stencil", forbidden)
509+
monkeypatch.setattr(campaign, "current_git_source_provenance", lambda *args: None)
510+
with pytest.raises(AssertionError, match="wrong historical bytes"):
511+
campaign.main()
512+
paths[changed_index].write_bytes(paths[changed_index].read_bytes() + b"\n")
510513
with pytest.raises(ValueError, match="lineage hashes"):
511514
campaign.main()
512515
assert not output.exists()

‎tests/physics/test_constitutive_support_scope.py‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
from copy import deepcopy
88
from fractions import Fraction
9+
from math import ulp
910

1011
import networkx as nx
12+
import numpy as np
1113
import pytest
1214

1315
from tnfr.dynamics.dnfr import default_compute_delta_nfr
@@ -126,7 +128,10 @@ def test_same_initial_support_has_distinct_declared_tangents_and_energy_work():
126128

127129

128130
@pytest.mark.parametrize("explicit_lengths", (False, True))
129-
def test_potential_distinguishes_metric_length_from_conductance(explicit_lengths):
131+
@pytest.mark.parametrize("triangle", ("L", "U"))
132+
def test_potential_distinguishes_metric_length_from_conductance(
133+
explicit_lengths, triangle, monkeypatch
134+
):
130135
first = _graph(explicit_lengths=explicit_lengths)
131136
second = _graph(2, explicit_lengths=explicit_lengths)
132137
before = compute_structural_potential(first)
@@ -139,10 +144,20 @@ def test_potential_distinguishes_metric_length_from_conductance(explicit_lengths
139144
assert compute_phase_curvature(first) == compute_phase_curvature(second)
140145
# Four nodes cannot supply the correlation fit's ten distinct pairs.
141146
# This explicitly tests the normalized-spectrum fallback, not every xi fit.
147+
eigvalsh = np.linalg.eigvalsh
148+
monkeypatch.setattr(
149+
np.linalg, "eigvalsh", lambda matrix: eigvalsh(matrix, UPLO=triangle)
150+
)
142151
xi_first = estimate_coherence_length_with_provenance(first)
143152
xi_second = estimate_coherence_length_with_provenance(second)
144153
assert xi_first.method == xi_second.method == "spectral_gap"
145-
assert xi_first.value == xi_second.value
154+
# A positive weighted three-leaf star has exact normalized spectrum
155+
# (0, 1, 1, 2), hence xi=1. Square-root normalization and either equivalent
156+
# LAPACK triangle route can differ in their last bits under scaling.
157+
# This is a numerical test tolerance, not a universal solver error bound.
158+
assert (xi_first.value, xi_second.value) == pytest.approx(
159+
(1.0, 1.0), rel=0.0, abs=16 * ulp(1.0)
160+
)
146161

147162

148163
def test_zero_conductance_support_still_changes_non_epi_channels():

0 commit comments

Comments
 (0)