From 549d16c48afad2ed8840cc8281778a4c61ecfcd5 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 18 Aug 2026 00:16:14 +0000
Subject: [PATCH 1/2] fix: persist distinct TEPP received and recorded clocks
(v2.12.2)
ADR 0035 already named two clocks on accepted transport evidence.
The persist path bound one instant into both columns, so Measurement
evidence always showed Received and recorded. Store transport-response
receipt as received_at and row-write time as recorded_at. Show the
second clock only when those instants differ.
Co-authored-by: Seongho Bae
---
CHANGELOG.d/2.12.2-tepp-accepted-clocks.md | 7 +
CHANGELOG.md | 13 ++
CLAUDE.md | 3 +-
backend/app/analysis_run_start.py | 52 ++++++--
.../0035-tepp-accepted-transport-evidence.md | 13 ++
frontend/package.json | 2 +-
frontend/src/App.test.tsx | 48 ++++++-
frontend/src/App.tsx | 33 ++++-
lineageweave/__init__.py | 2 +-
pyproject.toml | 2 +-
tests/test_analysis_run_start.py | 121 ++++++++++++++++++
tests/test_tepp_public_content.py | 1 +
tests/test_tepp_transport_evidence.py | 36 ++++++
13 files changed, 313 insertions(+), 20 deletions(-)
create mode 100644 CHANGELOG.d/2.12.2-tepp-accepted-clocks.md
diff --git a/CHANGELOG.d/2.12.2-tepp-accepted-clocks.md b/CHANGELOG.d/2.12.2-tepp-accepted-clocks.md
new file mode 100644
index 000000000..56af89e3a
--- /dev/null
+++ b/CHANGELOG.d/2.12.2-tepp-accepted-clocks.md
@@ -0,0 +1,7 @@
+# 2.12.2 TEPP accepted evidence stores distinct receipt and row-write clocks
+
+Accepted transport evidence persists `received_at` as the
+transport-response receipt and `recorded_at` as the row-write
+instant. Measurement evidence shows the second clock only when those
+instants differ. Digest recomputation is unchanged. No invented
+theta (ADR 0035 follow-up).
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9bb3e2230..1f0862b8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,19 @@ All notable changes to this project are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [2.12.2] - 2026-08-18
+
+### Fixed
+
+- Accepted TEPP transport evidence now stores **received** (transport
+ response) and **recorded** (row write) as distinct clocks when those
+ instants differ (ADR 0035 follow-up). After `make seed`, Demo Analyst
+ opens **TEPP measurement · Failed · Demo Corp** Measurement evidence
+ and sees one Received clock when seed receipt and persist share an
+ instant. A later start that persists in a later minute shows both
+ clocks. Digest recomputation is unchanged. Hidden runs stay 404.
+ Never invent a theta.
+
## [2.12.1] - 2026-08-17
### Fixed
diff --git a/CLAUDE.md b/CLAUDE.md
index bfc9fc2e8..2ffd5241a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -27,7 +27,8 @@ unpublished envelope is Failed (`tepp_not_available` /
`tepp_result_not_persisted`). A published accepted acknowledgement is
Failed (`tepp_completed_result_unsupported`) and is shown as aggregate
transport evidence. Do not stamp Succeeded from that ack. Do not invent
-a theta or a local psychometric substitute.
+a theta or a local psychometric substitute. Measurement evidence shows
+Received, and recorded only when that row-write instant differs.
The home list caption stays `kind · status · entity`; the machine
failure code is detail-only (ADR 0014). Open a Failed TEPP row, then
connect a live TEPP transport or read aggregate transport evidence.
diff --git a/backend/app/analysis_run_start.py b/backend/app/analysis_run_start.py
index 62c90bd83..6cf3b657e 100644
--- a/backend/app/analysis_run_start.py
+++ b/backend/app/analysis_run_start.py
@@ -5,8 +5,10 @@
so a crash after Running does not lose the item. ADR 0035 stores a
published TEPP accepted acknowledgement as aggregate transport
evidence and never stamps Succeeded from that ack or from a
-LineageWeave-local completed envelope. Period-report stays another
-path. Neither start invents a theta or a calibrated report score.
+LineageWeave-local completed envelope. Accepted evidence stores
+transport-response receipt and row-write time as distinct clocks
+when those instants differ. Period-report stays another path.
+Neither start invents a theta or a calibrated report score.
"""
from __future__ import annotations
@@ -130,6 +132,25 @@ def tepp_run_request(
)
+def tepp_accepted_clocks(
+ *,
+ started_at: datetime,
+ received_at: datetime,
+ recorded_at: datetime,
+) -> tuple[datetime, datetime]:
+ """Return receipt then row-write clocks, monotonic versus start.
+
+ ``received_at`` is the transport-response receipt. ``recorded_at``
+ is the later row-write instant. A clock that runs backward is
+ clamped forward so ``started_at <= received_at <= recorded_at``.
+ Equal instants stay equal; this helper does not invent a later
+ recorded clock.
+ """
+ receipt = received_at if received_at >= started_at else started_at
+ recorded = recorded_at if recorded_at >= receipt else receipt
+ return receipt, recorded
+
+
def tepp_submit_outcome(
client: TeppClient,
request: AnalysisRunRequest,
@@ -709,9 +730,16 @@ async def _persist_tepp_accepted(
conn: asyncpg.Connection,
analysis_run_id: str,
evidence: TeppAcceptedEvidence,
+ received_at: datetime,
recorded_at: datetime,
) -> bool:
- """Store published accepted evidence. Missing table is not success."""
+ """Store published accepted evidence with receipt and row-write clocks.
+
+ Missing table is not success. Callers pass transport-response
+ receipt as ``received_at`` and the row-write instant as
+ ``recorded_at``. This function binds those two values as given and
+ does not invent a later recorded clock when they are equal.
+ """
try:
await conn.execute(
"""
@@ -726,7 +754,7 @@ async def _persist_tepp_accepted(
evidence.run_state,
evidence.idempotency_key,
evidence.evidence_sha256(),
- recorded_at,
+ received_at,
recorded_at,
)
except asyncpg.UndefinedTableError:
@@ -742,7 +770,7 @@ async def _deliver_tepp_measurement(
tepp_client: TeppClient,
) -> None:
"""Submit the frozen snapshot through ``tepp_client``. Never persist a theta."""
- now = datetime.now(timezone.utc)
+ started_at = datetime.now(timezone.utc)
request = tepp_run_request(
idempotency_key=str(locked["idempotency_key"]),
snapshot_sha256=str(locked["snapshot_sha256"]),
@@ -750,12 +778,16 @@ async def _deliver_tepp_measurement(
corporate_entity_id=str(locked["corporate_entity_id"]),
)
status_code, failure_code, accepted = tepp_submit_outcome(tepp_client, request)
- finished = datetime.now(timezone.utc)
- if finished < now:
- finished = now
+ received_at = datetime.now(timezone.utc)
+ recorded_at = datetime.now(timezone.utc)
+ receipt, recorded = tepp_accepted_clocks(
+ started_at=started_at,
+ received_at=received_at,
+ recorded_at=recorded_at,
+ )
if accepted is not None:
stored = await _persist_tepp_accepted(
- conn, analysis_run_id, accepted, finished
+ conn, analysis_run_id, accepted, receipt, recorded
)
if not stored:
status_code, failure_code = _FAILED, "tepp_result_not_persisted"
@@ -764,6 +796,6 @@ async def _deliver_tepp_measurement(
analysis_run_id,
await _next_status_ordinal(conn, analysis_run_id),
status_code,
- finished,
+ recorded,
failure_code,
)
diff --git a/docs/adr/0035-tepp-accepted-transport-evidence.md b/docs/adr/0035-tepp-accepted-transport-evidence.md
index 207a87a69..ef7e706ce 100644
--- a/docs/adr/0035-tepp-accepted-transport-evidence.md
+++ b/docs/adr/0035-tepp-accepted-transport-evidence.md
@@ -98,6 +98,19 @@ Existing volumes apply `0029_analysis_run_tepp_accepted.sql` after
0028. Granted retention purge empties the accepted table when it
exists.
+## Follow-up — v2.12.2 distinct receipt and row-write clocks
+
+Decision 3 already named `received_at` (transport-response receipt)
+and `recorded_at` (row persistence). v2.12.1 bound one application
+instant into both columns, so Measurement evidence copy always showed
+two clocks. v2.12.2 passes the post-transport instant as
+`received_at` and the row-write instant as `recorded_at`, clamped so
+start ≤ receipt ≤ row-write (National Institute of Standards and
+Technology, 2015). Authorized copy shows the second clock only when
+the displayed instants differ. Digest recomputation is unchanged and
+still excludes clocks. Hidden runs stay 404 (ADR 0014). Migration
+0029 is not rewritten; the two columns already exist.
+
## References — APA 7th
American Educational Research Association, American Psychological
diff --git a/frontend/package.json b/frontend/package.json
index 1051b23c9..72e864e31 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.12.1",
+ "version": "2.12.2",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 2d1528192..c2691e341 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -74,6 +74,8 @@ describe("App, authenticated", () => {
succeededReportRun?: boolean;
succeededTeppRun?: boolean;
acceptedTeppRun?: boolean;
+ distinctTeppClocks?: boolean;
+ omitTeppRecordedAt?: boolean;
pendingTeppRun?: boolean;
hiddenAnalysisRun?: boolean;
pluralAffiliations?: boolean;
@@ -325,7 +327,13 @@ describe("App, authenticated", () => {
tepp_idempotency_key: "demo-tepp-seed-2026-w02-succeeded",
tepp_evidence_sha256: "a".repeat(64),
tepp_received_at: "2026-01-12T12:45:00Z",
- tepp_recorded_at: "2026-01-12T12:45:00Z",
+ ...(options?.omitTeppRecordedAt
+ ? {}
+ : {
+ tepp_recorded_at: options?.distinctTeppClocks
+ ? "2026-01-12T12:46:00Z"
+ : "2026-01-12T12:45:00Z",
+ }),
tepp_completed_artifact_available: false,
}
: {};
@@ -722,7 +730,13 @@ describe("App, authenticated", () => {
tepp_run_state: "accepted",
tepp_evidence_sha256: "a".repeat(64),
tepp_received_at: "2026-01-12T12:45:00Z",
- tepp_recorded_at: "2026-01-12T12:45:00Z",
+ ...(options?.omitTeppRecordedAt
+ ? {}
+ : {
+ tepp_recorded_at: options?.distinctTeppClocks
+ ? "2026-01-12T12:46:00Z"
+ : "2026-01-12T12:45:00Z",
+ }),
tepp_completed_artifact_available: false,
}
: {}),
@@ -2944,6 +2958,8 @@ describe("App, authenticated", () => {
expect(screen.getAllByText("aggregate transport evidence").length).toBeGreaterThan(0);
expect(screen.getByText("a".repeat(64))).toBeInTheDocument();
expect(screen.getByText(/accepted run demo-tepp-accepted-opaque/)).toBeInTheDocument();
+ expect(screen.getByText("Received 2026-01-12 12:45")).toBeInTheDocument();
+ expect(screen.queryByText(/recorded 2026-01-12/)).not.toBeInTheDocument();
expect(screen.getByText(/completed-artifact identity/i)).toBeInTheDocument();
expect(screen.queryByText(/validated multilevel estimate/i)).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Copy evidence SHA-256" }));
@@ -2952,6 +2968,34 @@ describe("App, authenticated", () => {
expect(screen.queryByText(/2 affiliations/)).not.toBeInTheDocument();
});
+ it("shows two TEPP clocks only when receipt and row-write differ", async () => {
+ stubBackend({ acceptedTeppRun: true, distinctTeppClocks: true });
+ render();
+
+ await userEvent.click(
+ await screen.findByRole("button", {
+ name: "Open analysis run: TEPP measurement · Failed · Demo Corp. Open this run to read aggregate transport evidence. Completed TEPP measurement identity is unavailable until TEPP publishes a versioned completed-result contract.",
+ }),
+ );
+ expect(
+ await screen.findByText("Received 2026-01-12 12:45 · recorded 2026-01-12 12:46"),
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/theta/i)).not.toBeInTheDocument();
+ });
+
+ it("shows only the receipt clock when recorded time is absent", async () => {
+ stubBackend({ acceptedTeppRun: true, omitTeppRecordedAt: true });
+ render();
+
+ await userEvent.click(
+ await screen.findByRole("button", {
+ name: "Open analysis run: TEPP measurement · Failed · Demo Corp. Open this run to read aggregate transport evidence. Completed TEPP measurement identity is unavailable until TEPP publishes a versioned completed-result contract.",
+ }),
+ );
+ expect(await screen.findByText("Received 2026-01-12 12:45")).toBeInTheDocument();
+ expect(screen.queryByText(/recorded 2026-01-12/)).not.toBeInTheDocument();
+ });
+
it("records a pending lineage run and opens the authorized detail", async () => {
const fetchMock = stubBackend();
render();
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 722cab747..0e9099931 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -2037,6 +2037,34 @@ function analysisRunCorpusHint(run: AnalysisRun): string | null {
}
}
+/**
+ * Buyer-visible TEPP receipt clock. Minute precision matches other run clocks.
+ */
+function formatTeppEvidenceClock(iso: string): string {
+ return iso.slice(0, 16).replace("T", " ");
+}
+
+/**
+ * Authorized TEPP clocks. A second clock appears only when instants differ.
+ *
+ * Equal receipt and row-write values stay one sentence so the copy does
+ * not invent a second clock. Missing recorded time is receipt only.
+ */
+function teppAcceptedClockCopy(
+ receivedAt: string,
+ recordedAt: string | undefined,
+): string {
+ const received = formatTeppEvidenceClock(receivedAt);
+ if (recordedAt === undefined) {
+ return `Received ${received}`;
+ }
+ const recorded = formatTeppEvidenceClock(recordedAt);
+ if (recorded === received) {
+ return `Received ${received}`;
+ }
+ return `Received ${received} · recorded ${recorded}`;
+}
+
/**
* Authorized TEPP transport evidence. Never a validated multilevel estimate.
*
@@ -2071,10 +2099,7 @@ function TeppMeasurementEvidence({ run }: { run: AnalysisRun }) {
{run.tepp_received_at && (
- Received {run.tepp_received_at.slice(0, 16).replace("T", " ")}
- {run.tepp_recorded_at
- ? ` · recorded ${run.tepp_recorded_at.slice(0, 16).replace("T", " ")}`
- : ""}
+ {teppAcceptedClockCopy(run.tepp_received_at, run.tepp_recorded_at)}
)}
diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py
index 687eb5d06..d1fff5078 100644
--- a/lineageweave/__init__.py
+++ b/lineageweave/__init__.py
@@ -55,4 +55,4 @@
"sentence_excerpts",
]
-__version__ = "2.12.1"
+__version__ = "2.12.2"
diff --git a/pyproject.toml b/pyproject.toml
index 7360226aa..ade603e14 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.12.1"
+version = "2.12.2"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
diff --git a/tests/test_analysis_run_start.py b/tests/test_analysis_run_start.py
index 7973c339b..e6e106bbf 100644
--- a/tests/test_analysis_run_start.py
+++ b/tests/test_analysis_run_start.py
@@ -1,17 +1,21 @@
"""Start-reconstruction contracts: digest, freeze, 422/409, designed tree."""
+import asyncio
from datetime import datetime, timezone
+import asyncpg
import pytest
from backend.app.analysis_run_ingestion import reconstructed_edge_is_visible
from backend.app.analysis_run_start import (
AnalysisRunStartError,
+ _persist_tepp_accepted,
configured_tepp_client,
reconstruction_member_ids,
reconstruction_result_digest,
start_kind_rejection,
start_write_conflict_error,
+ tepp_accepted_clocks,
tepp_run_request,
tepp_submit_outcome,
)
@@ -20,7 +24,9 @@
from lineageweave.lineage_persistence import lineage_edge_specs
from lineageweave.tepp_client import AnalysisRunRequest, TeppClient, TeppNotAvailable
from lineageweave.tepp_result import (
+ TeppAcceptedEvidence,
accepted_tepp_seed_envelope,
+ parse_tepp_accepted_evidence,
persistable_tepp_seed_envelope,
)
@@ -193,6 +199,121 @@ def test_configured_tepp_client_stays_unavailable_without_http() -> None:
client.submit_analysis_run(_tepp_request())
+def test_tepp_accepted_clocks_keep_distinct_receipt_and_row_write() -> None:
+ """Transport receipt and row write stay two values when they differ."""
+ started = datetime(2026, 1, 12, 12, 44, tzinfo=timezone.utc)
+ received = datetime(2026, 1, 12, 12, 45, tzinfo=timezone.utc)
+ recorded = datetime(2026, 1, 12, 12, 46, tzinfo=timezone.utc)
+ assert tepp_accepted_clocks(
+ started_at=started,
+ received_at=received,
+ recorded_at=recorded,
+ ) == (received, recorded)
+
+
+def test_tepp_accepted_clocks_clamp_backward_receipt_to_start() -> None:
+ """A receipt earlier than start is not stored as a later invention."""
+ started = datetime(2026, 1, 12, 12, 45, tzinfo=timezone.utc)
+ earlier = datetime(2026, 1, 12, 12, 44, tzinfo=timezone.utc)
+ assert tepp_accepted_clocks(
+ started_at=started,
+ received_at=earlier,
+ recorded_at=earlier,
+ ) == (started, started)
+
+
+def test_tepp_accepted_clocks_clamp_backward_row_write_to_receipt() -> None:
+ """A row-write earlier than receipt stays the receipt, not invented later."""
+ started = datetime(2026, 1, 12, 12, 44, tzinfo=timezone.utc)
+ received = datetime(2026, 1, 12, 12, 45, tzinfo=timezone.utc)
+ earlier = datetime(2026, 1, 12, 12, 44, 30, tzinfo=timezone.utc)
+ assert tepp_accepted_clocks(
+ started_at=started,
+ received_at=received,
+ recorded_at=earlier,
+ ) == (received, received)
+
+
+def test_tepp_accepted_clocks_do_not_invent_a_second_instant() -> None:
+ """Equal receipt and persist stay one stored instant."""
+ instant = datetime(2026, 1, 12, 12, 45, tzinfo=timezone.utc)
+ assert tepp_accepted_clocks(
+ started_at=instant,
+ received_at=instant,
+ recorded_at=instant,
+ ) == (instant, instant)
+
+
+def _accepted_evidence() -> TeppAcceptedEvidence:
+ """Published Demo Corp accepted envelope used by persist-path tests."""
+ parsed = parse_tepp_accepted_evidence(
+ accepted_tepp_seed_envelope(idempotency_key="buyer-key"),
+ expected_idempotency_key="buyer-key",
+ )
+ assert parsed is not None
+ return parsed
+
+
+def test_persist_tepp_accepted_stores_two_clock_values() -> None:
+ """The insert binds transport receipt and row-write as distinct values."""
+ received = datetime(2026, 1, 12, 12, 45, tzinfo=timezone.utc)
+ recorded = datetime(2026, 1, 12, 12, 46, tzinfo=timezone.utc)
+
+ class _Conn:
+ def __init__(self) -> None:
+ self.bound: tuple[object, ...] | None = None
+
+ async def execute(self, _sql: str, *args: object) -> str:
+ self.bound = args
+ return "INSERT 0 1"
+
+ conn = _Conn()
+ stored = asyncio.run(
+ _persist_tepp_accepted(conn, "run-id", _accepted_evidence(), received, recorded)
+ )
+ assert stored is True
+ assert conn.bound is not None
+ assert conn.bound[6] == received
+ assert conn.bound[7] == recorded
+ assert conn.bound[6] != conn.bound[7]
+
+
+def test_persist_tepp_accepted_keeps_equal_clocks_equal() -> None:
+ """Same-instant receipt and persist are stored once each, not rewritten later."""
+ instant = datetime(2026, 1, 12, 12, 45, tzinfo=timezone.utc)
+
+ class _Conn:
+ def __init__(self) -> None:
+ self.bound: tuple[object, ...] | None = None
+
+ async def execute(self, _sql: str, *args: object) -> str:
+ self.bound = args
+ return "INSERT 0 1"
+
+ conn = _Conn()
+ stored = asyncio.run(
+ _persist_tepp_accepted(conn, "run-id", _accepted_evidence(), instant, instant)
+ )
+ assert stored is True
+ assert conn.bound is not None
+ assert conn.bound[6] == instant
+ assert conn.bound[7] == instant
+
+
+def test_persist_tepp_accepted_fails_closed_without_the_table() -> None:
+ """A missing accepted-evidence table is not success."""
+ instant = datetime(2026, 1, 12, 12, 45, tzinfo=timezone.utc)
+
+ class _Conn:
+ async def execute(self, _sql: str, *_args: object) -> str:
+ raise asyncpg.UndefinedTableError()
+
+ stored = asyncio.run(
+ _persist_tepp_accepted(_Conn(), "run-id", _accepted_evidence(), instant, instant)
+ )
+ assert stored is False
+
+
def test_hidden_run_start_is_not_found() -> None:
"""Operators get a 404 next action, not an internal exception name."""
error = AnalysisRunStartError(404, "This analysis run is not visible.")
diff --git a/tests/test_tepp_public_content.py b/tests/test_tepp_public_content.py
index 7f070f0b0..546438553 100644
--- a/tests/test_tepp_public_content.py
+++ b/tests/test_tepp_public_content.py
@@ -13,6 +13,7 @@
_ROOT / "migrations" / "0029_analysis_run_tepp_accepted.sql",
_ROOT / "docs" / "adr" / "0035-tepp-accepted-transport-evidence.md",
_ROOT / "CHANGELOG.d" / "2.12.1-tepp-accepted-transport-evidence.md",
+ _ROOT / "CHANGELOG.d" / "2.12.2-tepp-accepted-clocks.md",
_ROOT / "tests" / "test_tepp_result.py",
_ROOT / "tests" / "test_analysis_run_tepp_accepted_schema.py",
)
diff --git a/tests/test_tepp_transport_evidence.py b/tests/test_tepp_transport_evidence.py
index 0e8192412..764cc16d3 100644
--- a/tests/test_tepp_transport_evidence.py
+++ b/tests/test_tepp_transport_evidence.py
@@ -12,6 +12,30 @@
)
+def test_project_tepp_transport_evidence_keeps_digest_independent_of_clocks() -> None:
+ """Clock split must not change the published accepted-field digest."""
+ expected = tepp_accepted_evidence_sha256(
+ contract_version=1,
+ accepted_run_id="demo-tepp-accepted-opaque",
+ run_state="accepted",
+ idempotency_key="buyer-key",
+ )
+ later = {
+ "contract_version": 1,
+ "accepted_run_id": "demo-tepp-accepted-opaque",
+ "run_state": "accepted",
+ "idempotency_key": "buyer-key",
+ "evidence_sha256": expected,
+ "received_at": "2026-01-12T12:45:00Z",
+ "recorded_at": "2026-01-12T12:46:00Z",
+ }
+ projected = project_tepp_transport_evidence(later)
+ assert projected is not None
+ assert projected["tepp_evidence_sha256"] == expected
+ assert projected["tepp_received_at"] == "2026-01-12T12:45:00Z"
+ assert projected["tepp_recorded_at"] == "2026-01-12T12:46:00Z"
+
+
def test_project_tepp_transport_evidence_recomputes_the_exact_digest() -> None:
"""The API digest must match an independent SHA-256 recomputation."""
parsed = parse_tepp_accepted_evidence(
@@ -57,6 +81,18 @@ def test_project_tepp_transport_evidence_fails_closed_on_digest_mismatch() -> No
assert project_tepp_transport_evidence(row) is None
+def test_persist_path_binds_received_and_recorded_as_distinct_arguments() -> None:
+ """Start must not write one timestamp into both accepted-evidence clocks."""
+ source = (
+ Path(__file__).resolve().parents[1]
+ / "backend"
+ / "app"
+ / "analysis_run_start.py"
+ ).read_text(encoding="utf-8")
+ assert "received_at,\n recorded_at," in source
+ assert source.count("recorded_at,\n recorded_at,") == 0
+
+
def test_tepp_accepted_query_binds_authorized_run_ids_only() -> None:
"""Hidden runs never enter the evidence query parameter list."""
source = (
From 44088660beca93c7222de29ab280b9894c9e2cd6 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 18 Aug 2026 00:18:34 +0000
Subject: [PATCH 2/2] test: cover TEPP deliver path for distinct accepted
clocks
Exercise persist success, missing accepted table, and missing
transport so the changed start helpers stay fully covered.
Co-authored-by: Seongho Bae
---
tests/test_analysis_run_start.py | 113 ++++++++++++++++++++++++++++++-
1 file changed, 112 insertions(+), 1 deletion(-)
diff --git a/tests/test_analysis_run_start.py b/tests/test_analysis_run_start.py
index e6e106bbf..2fcccd0b1 100644
--- a/tests/test_analysis_run_start.py
+++ b/tests/test_analysis_run_start.py
@@ -9,6 +9,7 @@
from backend.app.analysis_run_ingestion import reconstructed_edge_is_visible
from backend.app.analysis_run_start import (
AnalysisRunStartError,
+ _deliver_tepp_measurement,
_persist_tepp_accepted,
configured_tepp_client,
reconstruction_member_ids,
@@ -306,7 +307,7 @@ def test_persist_tepp_accepted_fails_closed_without_the_table() -> None:
class _Conn:
async def execute(self, _sql: str, *_args: object) -> str:
- raise asyncpg.UndefinedTableError()
+ raise asyncpg.UndefinedTableError("undefined_table")
stored = asyncio.run(
_persist_tepp_accepted(_Conn(), "run-id", _accepted_evidence(), instant, instant)
@@ -314,6 +315,116 @@ async def execute(self, _sql: str, *_args: object) -> str:
assert stored is False
+class _DeliverConn:
+ """In-memory start connection for TEPP persist and status append."""
+
+ def __init__(self, *, persist_ok: bool = True) -> None:
+ self.persist_ok = persist_ok
+ self.accepted_args: tuple[object, ...] | None = None
+ self.status_args: tuple[object, ...] | None = None
+
+ async def fetchval(self, _sql: str, *_args: object) -> int:
+ return 2
+
+ async def execute(self, sql: str, *args: object) -> str:
+ if "analysis_run_tepp_accepted" in sql:
+ if not self.persist_ok:
+ raise asyncpg.UndefinedTableError("undefined_table")
+ self.accepted_args = args
+ return "INSERT 0 1"
+ if "analysis_run_status_event" in sql:
+ self.status_args = args
+ return "INSERT 0 1"
+ raise AssertionError(sql)
+
+
+def _locked_tepp_row() -> dict[str, object]:
+ """Frozen Demo Corp TEPP start row. Never invents a theta."""
+ return {
+ "idempotency_key": "buyer-tepp-2026-w07",
+ "snapshot_sha256": "ab" * 32,
+ "knowledge_cutoff": datetime(2026, 1, 12, 12, 0, tzinfo=timezone.utc),
+ "corporate_entity_id": "11111111-1111-1111-1111-111111111111",
+ }
+
+
+def test_deliver_tepp_measurement_persists_distinct_clocks() -> None:
+ """Accepted evidence stores receipt then row-write, and stays Failed."""
+ request = _tepp_request()
+
+ class _Accepted(TeppClient):
+ def __init__(self) -> None:
+ super().__init__(
+ transport=lambda _payload: accepted_tepp_seed_envelope(
+ idempotency_key=request.idempotency_key
+ )
+ )
+
+ conn = _DeliverConn()
+ asyncio.run(
+ _deliver_tepp_measurement(
+ conn,
+ analysis_run_id="run-id",
+ locked=_locked_tepp_row(),
+ tepp_client=_Accepted(),
+ )
+ )
+ assert conn.accepted_args is not None
+ received_at = conn.accepted_args[6]
+ recorded_at = conn.accepted_args[7]
+ assert isinstance(received_at, datetime)
+ assert isinstance(recorded_at, datetime)
+ assert received_at <= recorded_at
+ assert conn.status_args is not None
+ assert conn.status_args[2] == "analysis_status_failed"
+ assert conn.status_args[4] == "tepp_completed_result_unsupported"
+ assert conn.status_args[3] == recorded_at
+
+
+def test_deliver_tepp_measurement_fails_closed_when_table_is_missing() -> None:
+ """A missing accepted table is Failed, never a fabricated measurement."""
+ request = _tepp_request()
+
+ class _Accepted(TeppClient):
+ def __init__(self) -> None:
+ super().__init__(
+ transport=lambda _payload: accepted_tepp_seed_envelope(
+ idempotency_key=request.idempotency_key
+ )
+ )
+
+ conn = _DeliverConn(persist_ok=False)
+ asyncio.run(
+ _deliver_tepp_measurement(
+ conn,
+ analysis_run_id="run-id",
+ locked=_locked_tepp_row(),
+ tepp_client=_Accepted(),
+ )
+ )
+ assert conn.accepted_args is None
+ assert conn.status_args is not None
+ assert conn.status_args[2] == "analysis_status_failed"
+ assert conn.status_args[4] == "tepp_result_not_persisted"
+
+
+def test_deliver_tepp_measurement_does_not_persist_a_missing_transport() -> None:
+ """A missing TEPP transport writes no accepted evidence row."""
+ conn = _DeliverConn()
+ asyncio.run(
+ _deliver_tepp_measurement(
+ conn,
+ analysis_run_id="run-id",
+ locked=_locked_tepp_row(),
+ tepp_client=TeppClient(),
+ )
+ )
+ assert conn.accepted_args is None
+ assert conn.status_args is not None
+ assert conn.status_args[2] == "analysis_status_failed"
+ assert conn.status_args[4] == "tepp_not_available"
+
+
def test_hidden_run_start_is_not_found() -> None:
"""Operators get a 404 next action, not an internal exception name."""
error = AnalysisRunStartError(404, "This analysis run is not visible.")