From 56fb1d963fa373b3069548e774c127f7ec67e4d2 Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 02:48:54 -0700
Subject: [PATCH 01/22] fix(epcis-export): return honest empty state instead of
sample data
Remove SAMPLE_EPCIS_EVENTS and the hardcoded FDA sample CSV. Tenants with
no rows now get an empty eventList / header-only CSV, count 0,
dataSource 'empty' and a 'No data yet' message. Tests are data-free.
Drop the now-stale detect-secrets baseline entries for the sample hashes.
Signed-off-by: Christopher Sellers <1@christophersellers.com>
---
.secrets.baseline | 32 +------
services/ingestion/app/epcis_export.py | 86 ++++---------------
.../ingestion/tests/test_epcis_export_1342.py | 38 ++++----
3 files changed, 33 insertions(+), 123 deletions(-)
diff --git a/.secrets.baseline b/.secrets.baseline
index 834b4df10..ef063de2f 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -4157,36 +4157,6 @@
"line_number": 205
}
],
- "services/ingestion/app/epcis_export.py": [
- {
- "type": "Hex High Entropy String",
- "filename": "services/ingestion/app/epcis_export.py",
- "hashed_secret": "d9628db26a0fe230ac85c6e1f4d8e30ded6fec22",
- "is_verified": false,
- "line_number": 190
- },
- {
- "type": "Hex High Entropy String",
- "filename": "services/ingestion/app/epcis_export.py",
- "hashed_secret": "64f49f14a963a261feb8cd12e0197e5867ae75a9",
- "is_verified": false,
- "line_number": 191
- },
- {
- "type": "Hex High Entropy String",
- "filename": "services/ingestion/app/epcis_export.py",
- "hashed_secret": "6145bcb9eb0209bf23fc14f480724821e0a104b7",
- "is_verified": false,
- "line_number": 209
- },
- {
- "type": "Hex High Entropy String",
- "filename": "services/ingestion/app/epcis_export.py",
- "hashed_secret": "0d6622b8003561598c82b121b6f4e2c59571c216",
- "is_verified": false,
- "line_number": 210
- }
- ],
"services/ingestion/tests/test_boundaries.py": [
{
"type": "Basic Auth Credentials",
@@ -5187,5 +5157,5 @@
}
]
},
- "generated_at": "2026-10-20T13:07:06Z"
+ "generated_at": "2026-10-23T09:48:32Z"
}
diff --git a/services/ingestion/app/epcis_export.py b/services/ingestion/app/epcis_export.py
index 2460b9012..9414afd31 100644
--- a/services/ingestion/app/epcis_export.py
+++ b/services/ingestion/app/epcis_export.py
@@ -20,12 +20,17 @@
from shared.tenant_context import TenantContext
from sqlalchemy import text
-from .disclaimers import SAMPLE_EXPORT_DISCLAIMER
from .shared.csv_safety import sanitize_cell
from .webhook_compat import require_tenant_context
logger = logging.getLogger("epcis-export")
+FDA_CSV_HEADER = (
+ "CTE_Type,Traceability_Lot_Code,Product_Description,Quantity,Unit_of_Measure,"
+ "Event_Date,Event_Time,Ship_From_Location,Ship_From_GLN,Ship_To_Location,"
+ "Ship_To_GLN,Carrier,Temperature_C,SHA256_Hash"
+)
+
def _query_tenant_events(
tenant_id: str, lot_code: str | None, date_from: str | None, date_to: str | None
@@ -164,54 +169,6 @@ class ExportRequest(BaseModel):
date_to: Optional[str] = Field(None, description="End date (YYYY-MM-DD)")
-# Sample EPCIS 2.0 event data (in production, pulled from database)
-SAMPLE_EPCIS_EVENTS = [
- {
- "type": "ObjectEvent",
- "eventTime": "2026-02-26T14:30:00.000Z",
- "eventTimeZoneOffset": "-08:00",
- "action": "OBSERVE",
- "bizStep": "urn:epcglobal:cbv:bizstep:shipping",
- "disposition": "urn:epcglobal:cbv:disp:in_transit",
- "epcList": ["urn:epc:id:sgtin:0614141.000001.001"],
- "readPoint": {"id": "urn:epc:id:sgln:0614141.00000.0"},
- "bizLocation": {"id": "urn:epc:id:sgln:0614141.00000.0"},
- "extension": {
- "quantityList": [
- {"epcClass": "urn:epc:class:lgtin:0614141.000001.TOM-0226-F3-001", "quantity": 200, "uom": "CS"}
- ],
- "sourceList": [
- {"type": "urn:epcglobal:cbv:sdt:possessing_party", "source": "urn:epc:id:pgln:0614141.00000"}
- ],
- "destinationList": [
- {"type": "urn:epcglobal:cbv:sdt:possessing_party", "destination": "urn:epc:id:pgln:0614141.00001"}
- ],
- },
- "regengine:sha256": "a3f8c1d2e4b5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
- "regengine:chainHash": "b4c9e2c7d5f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3",
- },
- {
- "type": "ObjectEvent",
- "eventTime": "2026-02-27T09:15:00.000Z",
- "eventTimeZoneOffset": "-08:00",
- "action": "OBSERVE",
- "bizStep": "urn:epcglobal:cbv:bizstep:receiving",
- "disposition": "urn:epcglobal:cbv:disp:in_progress",
- "epcList": ["urn:epc:id:sgtin:0614141.000001.001"],
- "readPoint": {"id": "urn:epc:id:sgln:0614141.00001.0"},
- "bizLocation": {"id": "urn:epc:id:sgln:0614141.00001.0"},
- "extension": {
- "quantityList": [
- {"epcClass": "urn:epc:class:lgtin:0614141.000001.TOM-0226-F3-001", "quantity": 200, "uom": "CS"}
- ],
- "ilmd": {"regengine:temperatureCelsius": 3.2, "regengine:inspectionResult": "PASS"},
- },
- "regengine:sha256": "c5dae3d8e6f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4",
- "regengine:chainHash": "d6ebf4e9f7a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
- },
-]
-
-
@router.post(
"/epcis",
summary="Export in GS1 EPCIS 2.0 JSON-LD format",
@@ -230,10 +187,10 @@ async def export_epcis(
tenant_id = ctx.tenant_id
rows = _query_tenant_events(tenant_id, request.lot_code, request.date_from, request.date_to)
- data_source = "tenant" if rows else "sample"
+ data_source = "tenant" if rows else "empty"
+ event_list = []
if rows:
- event_list = []
for r in rows:
ts = r["event_timestamp"]
event = {
@@ -256,9 +213,6 @@ async def export_epcis(
"regengine:sha256": r["sha256_hash"] or "",
}
event_list.append(event)
- else:
- event_list = SAMPLE_EPCIS_EVENTS
-
epcis_document = {
"@context": [
"https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld",
@@ -279,7 +233,7 @@ async def export_epcis(
"dataSource": data_source,
"integrityVerified": data_source == "tenant",
"chainHashVerified": data_source == "tenant",
- **({"regengine:disclaimer": SAMPLE_EXPORT_DISCLAIMER} if data_source == "sample" else {}),
+ **({"regengine:message": "No data yet"} if data_source == "empty" else {}),
},
}
@@ -331,13 +285,11 @@ async def export_fda(
tenant_id = ctx.tenant_id
rows = _query_tenant_events(tenant_id, request.lot_code, request.date_from, request.date_to)
- data_source = "tenant" if rows else "sample"
+ data_source = "tenant" if rows else "empty"
if rows:
# Build CSV from real data
- lines = [
- "CTE_Type,Traceability_Lot_Code,Product_Description,Quantity,Unit_of_Measure,Event_Date,Event_Time,Ship_From_Location,Ship_From_GLN,Ship_To_Location,Ship_To_GLN,Carrier,Temperature_C,SHA256_Hash"
- ]
+ lines = [FDA_CSV_HEADER]
for r in rows:
ts = r["event_timestamp"]
date_str = ts.strftime("%Y-%m-%d") if ts else ""
@@ -365,16 +317,8 @@ async def export_fda(
csv_content = "\n".join(lines) + "\n"
event_count = len(rows)
else:
- # Sample data fallback for tenants with no events yet
- csv_content = """CTE_Type,Traceability_Lot_Code,Product_Description,Quantity,Unit_of_Measure,Event_Date,Event_Time,Ship_From_Location,Ship_From_GLN,Ship_To_Location,Ship_To_GLN,Carrier,Temperature_C,SHA256_Hash
-SHIPPING,TOM-0226-F3-001,Roma Tomatoes 12ct,200,cases,2026-02-26,14:30:00Z,Valley Fresh Farms Salinas CA,0614141000005,Metro Distribution Center LA,0614141000012,Cold Express Logistics,3.2,a3f8c1d2e4b5...
-RECEIVING,TOM-0226-F3-001,Roma Tomatoes 12ct,200,cases,2026-02-27,09:15:00Z,Valley Fresh Farms Salinas CA,0614141000005,Metro Distribution Center LA,0614141000012,,3.5,c5dae3d8e6f7...
-SHIPPING,LET-0226-A2-003,Romaine Lettuce Hearts 12ct,150,cases,2026-02-26,16:00:00Z,Green Valley Farms Salinas CA,0614141000029,Metro Distribution Center LA,0614141000012,Fresh Fleet Inc,2.8,e7f0a1b2c3d4...
-HARVESTING,CUC-0226-F2-015,English Cucumbers,500,cases,2026-02-25,08:00:00Z,Sunrise Farms Field 2,,,,,,f8a1b2c3d4e5...
-COOLING,SAL-0226-B1-007,Atlantic Salmon Fillets,300,lbs,2026-02-26,06:00:00Z,Pacific Seafood Portland OR,0614141000036,,,,-1.5,a9b0c1d2e3f4...
-TRANSFORMATION,SALAD-0226-001,Garden Salad Mix 16oz,1000,bags,2026-02-28,10:00:00Z,Metro Processing Plant LA,0614141000012,,,,,b0c1d2e3f4a5...
-"""
- event_count = 6
+ csv_content = FDA_CSV_HEADER + "\n"
+ event_count = 0
# EPIC-L (#1655): route filename through the shared safe_filename
# builder so tenant_id can't carry CRLF / quotes into the header value.
@@ -394,8 +338,8 @@ async def export_fda(
"X-RegEngine-Format": "FDA_21CFR1.1455",
"X-RegEngine-Data-Source": data_source,
}
- if data_source == "sample":
- fda_headers["X-RegEngine-Disclaimer"] = SAMPLE_EXPORT_DISCLAIMER
+ if data_source == "empty":
+ fda_headers["X-RegEngine-Message"] = "No data yet"
return StreamingResponse(
iter([csv_content]),
diff --git a/services/ingestion/tests/test_epcis_export_1342.py b/services/ingestion/tests/test_epcis_export_1342.py
index 92587d1b2..8f656f3cd 100644
--- a/services/ingestion/tests/test_epcis_export_1342.py
+++ b/services/ingestion/tests/test_epcis_export_1342.py
@@ -32,7 +32,6 @@
import app.epcis_export as epcis_export
from app.epcis_export import (
- SAMPLE_EPCIS_EVENTS,
_query_tenant_events,
_validate_epcis_document,
router as epcis_export_router,
@@ -260,7 +259,7 @@ async def _tenant_ctx(request: Request) -> TenantContext:
class TestExportEpcisEndpoint:
- def test_returns_sample_events_when_no_tenant_rows(
+ def test_returns_empty_events_when_no_tenant_rows(
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
@@ -272,16 +271,17 @@ def test_returns_sample_events_when_no_tenant_rows(
body = response.json()
assert body["type"] == "EPCISDocument"
assert body["schemaVersion"] == "2.0"
- # Sample path.
- assert body["epcisBody"]["eventList"] == SAMPLE_EPCIS_EVENTS
+ assert body["epcisBody"]["eventList"] == []
metadata = body["regengine:exportMetadata"]
- assert metadata["dataSource"] == "sample"
+ assert metadata["eventsCount"] == 0
+ assert metadata["dataSource"] == "empty"
assert metadata["integrityVerified"] is False
assert metadata["chainHashVerified"] is False
- assert "regengine:disclaimer" in metadata
- assert response.headers["X-RegEngine-Data-Source"] == "sample"
+ assert metadata["regengine:message"] == "No data yet"
+ assert "regengine:disclaimer" not in metadata
+ assert response.headers["X-RegEngine-Data-Source"] == "empty"
assert response.headers["X-RegEngine-Integrity-Verified"] == "false"
- assert response.headers["X-RegEngine-Events-Count"] == str(len(SAMPLE_EPCIS_EVENTS))
+ assert response.headers["X-RegEngine-Events-Count"] == "0"
assert "X-RegEngine-Schema-Valid" not in response.headers
def test_returns_tenant_events_when_db_has_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -352,16 +352,15 @@ def test_validate_flag_emits_validation_block_and_header(
)
assert response.status_code == 200
body = response.json()
- # Sample events are valid.
validation = body["regengine:validation"]
- assert validation["valid"] is True
- assert validation["errors"] == []
+ assert validation["valid"] is False
+ assert validation["errors"] == ["epcisBody.eventList is empty or missing"]
assert "checkedAt" in validation
- assert response.headers["X-RegEngine-Schema-Valid"] == "true"
+ assert response.headers["X-RegEngine-Schema-Valid"] == "false"
class TestExportFdaEndpoint:
- def test_sample_fallback_when_no_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
response = client.post(
"/api/v1/export/fda",
@@ -369,14 +368,11 @@ def test_sample_fallback_when_no_rows(self, client: TestClient, monkeypatch: pyt
)
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/csv")
- assert response.headers["X-RegEngine-Data-Source"] == "sample"
- assert response.headers["X-RegEngine-Events-Count"] == "6"
- assert "X-RegEngine-Disclaimer" in response.headers
- body = response.text
- assert body.startswith("CTE_Type,")
- # Sample includes these fixture rows.
- assert "TOM-0226-F3-001" in body
- assert "HARVESTING" in body
+ assert response.headers["X-RegEngine-Data-Source"] == "empty"
+ assert response.headers["X-RegEngine-Events-Count"] == "0"
+ assert response.headers["X-RegEngine-Message"] == "No data yet"
+ assert "X-RegEngine-Disclaimer" not in response.headers
+ assert response.text == epcis_export.FDA_CSV_HEADER + "\n"
def test_builds_csv_from_real_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
row1 = dict(_event_row()._mapping)
From 7b58a3a649df775e8719c92376209d6d10324262 Mon Sep 17 00:00:00 2001
From: PetrefiedThunder <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 04:25:37 -0700
Subject: [PATCH 02/22] fix(frontend): restore main build after SEL-410/SEL-414
ErrorState collision
#3475 and #3479 merged back to back and both defined ErrorState. The
surviving component lacked requestId/retrying used by #3479 callers, a
test mock had a duplicate useQAHealth key, and suppliers/page.tsx used
NO_DATA_YET without importing it. Vercel production builds for 03903ee
and e7ee4a7 failed type check; prod is still on c7874cb.
- ErrorState: optional requestId line, retrying disables Retry and shows
Retrying..., support link, className kept on root
- drop duplicate useQAHealth mock key
- import NO_DATA_YET in suppliers page
---
.../__tests__/components/error-state.test.tsx | 13 +++++++++++++
.../pages/dashboard-dead-ends.test.tsx | 1 -
frontend/src/app/dashboard/suppliers/page.tsx | 1 +
frontend/src/components/ui/error-state.tsx | 18 +++++++++++++-----
4 files changed, 27 insertions(+), 6 deletions(-)
create mode 100644 frontend/src/__tests__/components/error-state.test.tsx
diff --git a/frontend/src/__tests__/components/error-state.test.tsx b/frontend/src/__tests__/components/error-state.test.tsx
new file mode 100644
index 000000000..f59841bcc
--- /dev/null
+++ b/frontend/src/__tests__/components/error-state.test.tsx
@@ -0,0 +1,13 @@
+import { describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { ErrorState } from '@/components/ui/error-state';
+
+describe('ErrorState', () => {
+ it('keeps the callout style and shows retry progress with a caller class', () => {
+ render();
+
+ expect(screen.getByRole('alert')).toHaveClass('bg-re-danger-bg', 'm-4');
+ expect(screen.getByRole('button', { name: 'Retry' })).toBeDisabled();
+ expect(screen.getByText('Retrying…')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/__tests__/pages/dashboard-dead-ends.test.tsx b/frontend/src/__tests__/pages/dashboard-dead-ends.test.tsx
index 602dd2270..daf7c6e0b 100644
--- a/frontend/src/__tests__/pages/dashboard-dead-ends.test.tsx
+++ b/frontend/src/__tests__/pages/dashboard-dead-ends.test.tsx
@@ -18,7 +18,6 @@ vi.mock('@/lib/tenant-context', () => ({
}));
vi.mock('@/hooks/use-api', () => ({
- useQAHealth: () => ({ data: null, isLoading: false, error: null }),
useSystemStatus: () => ({ data: { overall_status: 'healthy', services: [] }, isLoading: false }),
useSystemMetrics: () => ({ data: { open_alerts: 0, events_ingested: 0 }, isLoading: false }),
useQAHealth: () => ({ data: undefined, isLoading: false, error: null, refetch: vi.fn() }),
diff --git a/frontend/src/app/dashboard/suppliers/page.tsx b/frontend/src/app/dashboard/suppliers/page.tsx
index 95aee7ecc..22bf62a40 100644
--- a/frontend/src/app/dashboard/suppliers/page.tsx
+++ b/frontend/src/app/dashboard/suppliers/page.tsx
@@ -37,6 +37,7 @@ import {
} from 'lucide-react';
import { apiClient } from '@/lib/api-client';
+import { NO_DATA_YET } from '@/lib/empty-values';
import { useAuth } from '@/lib/auth-context';
import { useTenant } from '@/lib/tenant-context';
import { useDashboardRefresh } from '@/hooks/use-dashboard-refresh';
diff --git a/frontend/src/components/ui/error-state.tsx b/frontend/src/components/ui/error-state.tsx
index 970953ed9..6ab72fc62 100644
--- a/frontend/src/components/ui/error-state.tsx
+++ b/frontend/src/components/ui/error-state.tsx
@@ -12,6 +12,8 @@ export interface ErrorStateProps {
message: string
onRetry?: () => void
retryLabel?: string
+ requestId?: string
+ retrying?: boolean
className?: string
}
@@ -20,6 +22,8 @@ export function ErrorState({
message,
onRetry,
retryLabel = "Retry",
+ requestId,
+ retrying = false,
className,
}: ErrorStateProps) {
return (
@@ -29,14 +33,18 @@ export function ErrorState({
title={title}
className={className}
action={
- onRetry ? (
-
- ) : undefined
+ <>
+ {onRetry && (
+
+ )}
+ Contact support
+ >
}
>
{message}
+ {requestId &&
Request ID: {requestId}
}
)
}
From 7026cb15a1a22bd1fe103fce8f49622dc3dc8a0a Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 06:15:16 -0700
Subject: [PATCH 03/22] test(epcis): drop SAMPLE_EPCIS_EVENTS patches;
data-free empty-state and header-only FDA CSV assertions
---
services/ingestion/tests/test_epcis_export_1342.py | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/services/ingestion/tests/test_epcis_export_1342.py b/services/ingestion/tests/test_epcis_export_1342.py
index c5aed4ffa..b6f588157 100644
--- a/services/ingestion/tests/test_epcis_export_1342.py
+++ b/services/ingestion/tests/test_epcis_export_1342.py
@@ -6,6 +6,7 @@
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
+from uuid import uuid4
import pytest
from fastapi import FastAPI, Request
@@ -103,7 +104,7 @@ def test_no_rows_returns_empty_list(self, monkeypatch: pytest.MonkeyPatch) -> No
assert _query_tenant_events("t", None, None, None) == []
def test_swallow_exception_returns_empty_list(self, monkeypatch: pytest.MonkeyPatch) -> None:
- session = _FakeSession(execute_queue=[RuntimeError("db down")])
+ session = _FakeSession(execute_queue=[RuntimeError()])
monkeypatch.setattr(epcis_export, "get_db_safe", lambda: session)
result = _query_tenant_events("t", None, None, None)
assert result == []
@@ -131,7 +132,7 @@ async def _tenant_ctx(request: Request) -> TenantContext:
return TenantContext(
tenant_id=tenant_id or "t",
principal_kind="test",
- principal_id="t",
+ principal_id=uuid4().hex,
)
app.dependency_overrides[_verify_api_key] = lambda: None
@@ -143,7 +144,6 @@ class TestExportEpcisEndpoint:
def test_returns_empty_events_when_no_tenant_rows(
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
- monkeypatch.setattr(epcis_export, "SAMPLE_EPCIS_EVENTS", [])
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
response = client.post(
"/api/v1/export/epcis",
@@ -219,7 +219,6 @@ def test_validate_flag_emits_validation_block_and_header(
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
- monkeypatch.setattr(epcis_export, "SAMPLE_EPCIS_EVENTS", [])
response = client.post(
"/api/v1/export/epcis?validate=true",
json={"tenant_id": "t"},
@@ -238,7 +237,7 @@ def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest.
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
response = client.post(
"/api/v1/export/fda",
- json={"tenant_id": "tenant-x"},
+ json={"tenant_id": "t"},
)
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/csv")
@@ -246,9 +245,7 @@ def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest.
assert response.headers["X-RegEngine-Events-Count"] == "0"
assert response.headers["X-RegEngine-Message"] == "No data yet"
assert "X-RegEngine-Disclaimer" not in response.headers
- assert response.text == epcis_export.FDA_CSV_HEADER + "\n"
-
- assert response.status_code == 422
+ assert [line for line in response.text.splitlines() if line] == [epcis_export.FDA_CSV_HEADER]
class TestListExportFormats:
From 51fa80a4c21ed3a46df1aa19c6a5ff5c7a570276 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 06:18:38 -0700
Subject: [PATCH 04/22] ci: fix required gates on #3524 (pin copy-lint actions,
inventory new workflows, allowlist false-positive secret, declare
visual/capabilities knip entries)
---
.github/workflows/copy-lint.yml | 4 ++--
.github/workflows/vercel-preview-guard.yml | 2 +-
frontend/package.json | 5 ++++-
scripts/workflow_inventory.txt | 9 +++++++++
4 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/copy-lint.yml b/.github/workflows/copy-lint.yml
index 80fb0db51..606033b89 100644
--- a/.github/workflows/copy-lint.yml
+++ b/.github/workflows/copy-lint.yml
@@ -20,8 +20,8 @@ jobs:
report:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
+ - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
with:
node-version: '20'
- run: node --test scripts/copy-lint/check-copy.test.mjs
diff --git a/.github/workflows/vercel-preview-guard.yml b/.github/workflows/vercel-preview-guard.yml
index fcb62cbcd..0833e9b2c 100644
--- a/.github/workflows/vercel-preview-guard.yml
+++ b/.github/workflows/vercel-preview-guard.yml
@@ -110,7 +110,7 @@ jobs:
BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
with:
github-token: ${{ github.token }}
- script: |
+ script: | # pragma: allowlist secret
const marker = '';
const secret = process.env.BYPASS_SECRET;
if (secret) core.setSecret(secret); // Emits ::add-mask:: before using it.
diff --git a/frontend/package.json b/frontend/package.json
index 13d694fae..e54cc8252 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,7 +22,10 @@
"knip": {
"entry": [
"scripts/design-lint.mjs",
- "tests/e2e/qa-2026-10-02/fe-regressions.config.ts"
+ "tests/e2e/qa-2026-10-02/fe-regressions.config.ts",
+ "playwright.visual.config.ts",
+ "tests/visual/**/*.ts",
+ "src/lib/capabilities.ts"
]
},
"//": "Pinning policy (#569): next, eslint-config-next, @sentry/nextjs, and @supabase/supabase-js are pinned to exact versions (no ^ or ~ prefix) because breaking changes in these packages have caused production incidents. Bump them deliberately after testing. All other deps use ^ for minor/patch auto-updates.",
diff --git a/scripts/workflow_inventory.txt b/scripts/workflow_inventory.txt
index d15859563..fcc63f216 100644
--- a/scripts/workflow_inventory.txt
+++ b/scripts/workflow_inventory.txt
@@ -33,14 +33,18 @@ agent-sweep.yml
api-surface-issue.yml
backend-ci.yml
bundle-analysis.yml
+copy-lint.yml
dependabot-lock-sync.yml
deploy.yml
+engine-review-pass.yml
factory-local-ci.yml
frontend-ci.yml
+head-sha-check-guard.yml
inflow-contract-ci.yml
infra-k8s-ci.yml
legacy-code-scan.yml
openapi-public-spec.yml
+pr-blocker-checklist.yml
pr-gate.yml
pr-secrets-non-main.yml
production-uptime.yml
@@ -52,7 +56,12 @@ sdk-ci.yml
security.yml
sentinel.yml
supply-chain.yml
+sync-main-into-prs.yml
test-suite-check.yml
+vercel-preview-guard.yml
+visual-preview.yml
+visual-production.yml
+visual-update-baselines.yml
[required-contexts]
CI Status Gate
From 9b4d11acd9cb634d11049f599670c640cc3f8324 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 06:53:09 -0700
Subject: [PATCH 05/22] chore(secrets): refresh .secrets.baseline
(detect-secrets 1.5.0)
---
.secrets.baseline | 32 +++++++-------------------------
1 file changed, 7 insertions(+), 25 deletions(-)
diff --git a/.secrets.baseline b/.secrets.baseline
index dc26a198f..1c1a7110e 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -141,35 +141,35 @@
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_verified": false,
- "line_number": 136
+ "line_number": 132
},
{
"type": "Basic Auth Credentials",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_verified": false,
- "line_number": 219
+ "line_number": 215
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "b71d494da8d930401ba9b32b7cef87295f3eb6d3",
"is_verified": false,
- "line_number": 223
+ "line_number": 219
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "d86daa0361b89a2c9f4b95648b9ad17edf6f92ae",
"is_verified": false,
- "line_number": 229
+ "line_number": 225
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "eec69da96b4b2984741269925f0a8e46a1e03468",
"is_verified": false,
- "line_number": 231
+ "line_number": 227
}
],
".github/workflows/frontend-ci.yml": [
@@ -3203,22 +3203,13 @@
"line_number": 142
}
],
- "services/ingestion/tests/test_epcis_gln_check_digit.py": [
- {
- "type": "Hex High Entropy String",
- "filename": "services/ingestion/tests/test_epcis_gln_check_digit.py",
- "hashed_secret": "8e4456ef140191dfda4a6d287b36e5451d1d7d79",
- "is_verified": false,
- "line_number": 64
- }
- ],
"services/ingestion/tests/test_epcis_tenant_isolation.py": [
{
"type": "Secret Keyword",
"filename": "services/ingestion/tests/test_epcis_tenant_isolation.py",
"hashed_secret": "9762e53f3b6420f10041406e88c337725670824b",
"is_verified": false,
- "line_number": 135
+ "line_number": 124
}
],
"services/ingestion/tests/test_fsma_plugin_catalog.py": [
@@ -3275,15 +3266,6 @@
"line_number": 276
}
],
- "services/ingestion/tests/test_qa4_webhook_inbound.py": [
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_qa4_webhook_inbound.py",
- "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de",
- "is_verified": false,
- "line_number": 121
- }
- ],
"services/ingestion/tests/test_regengine_sources_rest_qa.py": [
{
"type": "Hex High Entropy String",
@@ -4176,5 +4158,5 @@
}
]
},
- "generated_at": "2026-10-23T09:48:32Z"
+ "generated_at": "2026-10-23T13:49:18Z"
}
From 66733bc8d67f23ebcc028adf9f4e24c559bd6791 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 07:05:38 -0700
Subject: [PATCH 06/22] test(ingestion): make #2868 error-logging tests
data-free to fix collection
test_ingest_error_logging_2868.py imported _FakePersistence, _FakeSession, _client and _make_shipping_event from test_webhook_router_v2_1342.py, which the real-data-only cleanup removed, so collection failed on main. Drop the ingest-batch route cases that needed a fabricated event, test _failure_diagnostics directly with no payload, and keep the chain-verify case using MagicMock and a local app builder. Remove the stale detect-secrets baseline entry.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.secrets.baseline | 18 --
.../tests/test_ingest_error_logging_2868.py | 178 +++++-------------
2 files changed, 51 insertions(+), 145 deletions(-)
diff --git a/.secrets.baseline b/.secrets.baseline
index dc26a198f..31bf27126 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3239,24 +3239,6 @@
"line_number": 15
}
],
- "services/ingestion/tests/test_ingest_error_logging_2868.py": [
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_ingest_error_logging_2868.py",
- "hashed_secret": "33777c2f7ec7f479b1d0e0907b6b55c526491e78",
- "is_verified": false,
- "line_number": 52,
- "is_secret": false
- },
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_ingest_error_logging_2868.py",
- "hashed_secret": "0d2f6e84de0f98930bbc011a4cb95d931c35a93f",
- "is_verified": false,
- "line_number": 53,
- "is_secret": false
- }
- ],
"services/ingestion/tests/test_integration_router_1342.py": [
{
"type": "Secret Keyword",
diff --git a/services/ingestion/tests/test_ingest_error_logging_2868.py b/services/ingestion/tests/test_ingest_error_logging_2868.py
index d2414ca72..b98256dad 100644
--- a/services/ingestion/tests/test_ingest_error_logging_2868.py
+++ b/services/ingestion/tests/test_ingest_error_logging_2868.py
@@ -17,17 +17,20 @@
* and **no customer supply-chain data** rides along. This is a food-safety
compliance platform: lot codes and product descriptions identify a
specific customer's shipment, so an over-eager log line is its own
- incident. The last test locks that boundary.
+ incident. ``TestFailureDiagnostics`` locks that boundary.
"""
from __future__ import annotations
import sys
import types
+import uuid
from pathlib import Path
from typing import Any, Dict, List
+from unittest.mock import MagicMock
import pytest
+from fastapi import FastAPI
from fastapi.testclient import TestClient
service_dir = Path(__file__).parent.parent
@@ -36,21 +39,23 @@
pytest.importorskip("fastapi")
import app.webhook_router_v2 as wr # noqa: E402
-
-from services.ingestion.tests.test_webhook_router_v2_1342 import ( # noqa: E402
- _FakePersistence,
- _FakeSession,
- _build_app,
- _client,
- _make_shipping_event,
-)
+from app.authz import IngestionPrincipal, get_ingestion_principal # noqa: E402
+from app.webhook_router_v2.routes import _failure_diagnostics # noqa: E402
TENANT_ID = "tenant-a"
-# Values that must never reach a log line. These are the customer's
-# supply-chain identifiers, not diagnostics.
-SECRET_TLC = "TLC-CUSTOMER-CONFIDENTIAL-0001"
-SECRET_PRODUCT = "Romaine Hearts Lot 42 for Metro DC"
+
+def _build_app() -> FastAPI:
+ app = FastAPI()
+ app.include_router(wr.router)
+ app.dependency_overrides[wr._verify_api_key] = lambda: None
+ app.dependency_overrides[get_ingestion_principal] = lambda: IngestionPrincipal(
+ key_id="test-key",
+ scopes=["*"],
+ tenant_id=TENANT_ID,
+ auth_mode="test",
+ )
+ return app
class _LogRecorder:
@@ -61,9 +66,7 @@ def __init__(self) -> None:
def _record(self, level: str):
def _emit(event: Any = None, *args: Any, **kwargs: Any) -> None:
- self.calls.append(
- {"level": level, "event": event, "args": args, "kwargs": kwargs}
- )
+ self.calls.append({"level": level, "event": event, "args": args, "kwargs": kwargs})
return _emit
@@ -74,9 +77,7 @@ def __getattr__(self, name: str):
def find(self, event: str) -> Dict[str, Any]:
matches = [c for c in self.calls if c["event"] == event]
- assert matches, (
- f"expected a {event!r} log line; got {[c['event'] for c in self.calls]}"
- )
+ assert matches, f"expected a {event!r} log line; got {[c['event'] for c in self.calls]}"
return matches[0]
def payload_text(self) -> str:
@@ -94,8 +95,8 @@ def _fields(call: Dict[str, Any]) -> Dict[str, Any]:
def _assert_traceback_captured(call: Dict[str, Any]) -> None:
- captured = call["level"] == "exception" or bool(_fields(call).get("exc_info")) or bool(
- call["kwargs"].get("exc_info")
+ captured = (
+ call["level"] == "exception" or bool(_fields(call).get("exc_info")) or bool(call["kwargs"].get("exc_info"))
)
assert captured, (
"the 500 handler tells the caller to check the logs, so the log line "
@@ -112,109 +113,38 @@ def recorder(monkeypatch) -> _LogRecorder:
# ---------------------------------------------------------------------------
-# POST /api/v1/webhooks/ingest
+# Shared 5xx diagnostics (used by POST /api/v1/webhooks/ingest)
+#
+# The ingest-batch route cases were removed: they needed a fabricated
+# shipping event and an in-memory persistence fake, both dropped in the
+# real-data-only test cleanup. The diagnostics contract the ingest handler
+# relies on is checked directly, with no request body at all.
# ---------------------------------------------------------------------------
-class TestIngestBatchFailureLogging:
- def _payload(self) -> dict:
- event = _make_shipping_event(tlc=SECRET_TLC)
- event["product_description"] = SECRET_PRODUCT
- return {"source": "erp-connector", "events": [event]}
-
- def _blow_up(self, monkeypatch, exc: Exception):
- session = _FakeSession()
+class TestFailureDiagnostics:
+ def _request(self, correlation_id=None) -> MagicMock:
+ request = MagicMock()
+ request.state.correlation_id = correlation_id
+ return request
- class _BoomPersistence(_FakePersistence):
- def store_events_batch(self, **kw):
- raise exc
+ def test_names_error_type_so_outage_differs_from_bug(self) -> None:
+ diag = _failure_diagnostics(self._request(), SystemError())
+ assert diag["error_type"] == "SystemError"
- persistence = _BoomPersistence(session)
- return _client(monkeypatch, session=session, persistence=persistence)
-
- def test_logs_traceback(self, monkeypatch, recorder: _LogRecorder) -> None:
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- resp = client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-tb"},
- )
- assert resp.status_code == 500
- _assert_traceback_captured(recorder.find("ingest_batch_failed"))
-
- def test_logs_tenant_and_correlation_id(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- resp = client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-ids"},
- )
- assert resp.status_code == 500
- fields = _fields(recorder.find("ingest_batch_failed"))
- assert fields.get("tenant_id") == TENANT_ID, (
- "without tenant_id there is no way to tell whose batch failed (#2868)"
- )
- assert fields.get("correlation_id"), (
- "the 500 body points the caller at the logs; a correlation id is "
- "what lets support join the two (#2868)"
- )
+ def test_always_carries_correlation_id(self) -> None:
+ diag = _failure_diagnostics(self._request(), RuntimeError())
+ assert diag["correlation_id"]
- def test_logs_error_type_so_outage_differs_from_bug(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- """A bare ``except Exception`` makes every failure look alike."""
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-type"},
- )
- fields = _fields(recorder.find("ingest_batch_failed"))
- assert fields.get("error_type") == "SystemError", (
- "the handler catches bare Exception, so the log line must name the "
- "exception class or a DB outage is indistinguishable from a "
- "programming error (#2868)"
- )
+ def test_reuses_request_correlation_id(self) -> None:
+ cid = str(uuid.uuid4())
+ diag = _failure_diagnostics(self._request(cid), RuntimeError())
+ assert diag["correlation_id"] == cid
- def test_logs_batch_shape_for_triage(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-shape"},
- )
- fields = _fields(recorder.find("ingest_batch_failed"))
- assert fields.get("event_count") == 1
- assert fields.get("source") == "erp-connector"
-
- def test_does_not_log_customer_supply_chain_data(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- """Diagnostics only — never the payload.
-
- Lot codes and product descriptions identify a specific customer's
- shipment. Fixing an under-logging bug by dumping the request body
- would trade a debuggability gap for a data-handling incident.
- """
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-pii"},
- )
- logged = recorder.payload_text()
- assert SECRET_TLC not in logged, (
- "traceability lot codes are customer supply-chain data and must "
- "not be logged (#2868)"
- )
- assert SECRET_PRODUCT not in logged, (
- "product descriptions are customer supply-chain data and must not "
- "be logged (#2868)"
- )
+ def test_only_diagnostic_fields(self) -> None:
+ """Diagnostics only, never the payload (#2868)."""
+ diag = _failure_diagnostics(self._request(), RuntimeError())
+ assert set(diag) == {"correlation_id", "error", "error_type"}
# ---------------------------------------------------------------------------
@@ -224,7 +154,7 @@ def test_does_not_log_customer_supply_chain_data(
class TestChainVerifyFailureLogging:
def _client(self, monkeypatch) -> TestClient:
- session = _FakeSession()
+ session = MagicMock()
class _BadPersistence:
def __init__(self, _s):
@@ -249,9 +179,7 @@ def verify_chain(self, _tid):
app = _build_app()
return TestClient(app)
- def test_logs_traceback_tenant_and_error_type(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
+ def test_logs_traceback_tenant_and_error_type(self, monkeypatch, recorder: _LogRecorder) -> None:
client = self._client(monkeypatch)
resp = client.get(
"/api/v1/webhooks/chain/verify",
@@ -262,11 +190,7 @@ def test_logs_traceback_tenant_and_error_type(
_assert_traceback_captured(call)
fields = _fields(call)
assert fields.get("tenant_id") == TENANT_ID, (
- "chain verification failures must name the tenant whose hash "
- "chain could not be walked (#2868)"
+ "chain verification failures must name the tenant whose hash chain could not be walked (#2868)"
)
assert fields.get("error_type") == "RuntimeError"
- assert fields.get("correlation_id"), (
- "a correlation id is what makes 'check server logs' actionable "
- "(#2868)"
- )
+ assert fields.get("correlation_id"), "a correlation id is what makes 'check server logs' actionable (#2868)"
From 278e85afb750c6c56a3789e794c02ee1ce769dc1 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 07:06:44 -0700
Subject: [PATCH 07/22] fix(ci): scope suppliers no-data test; match uses: as
YAML key in supply-chain checker
---
.../__tests__/pages/suppliers-metrics.test.tsx | 3 ++-
scripts/ci/check_supply_chain.py | 2 +-
tests/scripts/test_check_supply_chain.py | 18 ++++++++++++++++++
3 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/frontend/src/__tests__/pages/suppliers-metrics.test.tsx b/frontend/src/__tests__/pages/suppliers-metrics.test.tsx
index dfceb8a20..8e09da6a4 100644
--- a/frontend/src/__tests__/pages/suppliers-metrics.test.tsx
+++ b/frontend/src/__tests__/pages/suppliers-metrics.test.tsx
@@ -67,7 +67,8 @@ it('shows "No data yet" for lot codes when no facility has a count', async () =>
mocks.tlcs.mockRejectedValue(new Error('boom'));
render();
await waitFor(() => expect(screen.getAllByText('No lot code data yet')).toHaveLength(2));
- expect(screen.getByText('No data yet')).toBeInTheDocument();
+ const tile = screen.getByText('Lot Codes Tracked').parentElement as HTMLElement;
+ expect(within(tile).getByText('No data yet')).toBeInTheDocument();
expect(document.body.textContent).not.toMatch(/NaN/);
});
diff --git a/scripts/ci/check_supply_chain.py b/scripts/ci/check_supply_chain.py
index 9902f8204..4de28a459 100644
--- a/scripts/ci/check_supply_chain.py
+++ b/scripts/ci/check_supply_chain.py
@@ -35,7 +35,7 @@
from pathlib import Path
SHA40 = re.compile(r"^[0-9a-f]{40}$")
-USES_RE = re.compile(r"[\"']?uses[\"']?:\s*[\"']?([A-Za-z0-9_.\-/]+)(?:@([^\"'#\s]+))?[\"']?")
+USES_RE = re.compile(r"(?:^\s*(?:-\s*)?|[\[{,]\s*)[\"']?uses[\"']?\s*:\s*[\"']?([A-Za-z0-9_.\-/]+)(?:@([^\"'#\s]+))?[\"']?")
USES_KEY_RE = re.compile(r"^\s*(?:-\s*)?[\"']?uses[\"']?\s*:(.*)$")
PIPE_SHELL_RE = re.compile(r"\|\s*(sudo(\s+-\S+)*\s+)?(sh|bash)\b")
LATEST_URL_RE = re.compile(r"https?://\S*latest\S*|@latest\b", re.IGNORECASE)
diff --git a/tests/scripts/test_check_supply_chain.py b/tests/scripts/test_check_supply_chain.py
index f6d8f88fc..50a0a3feb 100644
--- a/tests/scripts/test_check_supply_chain.py
+++ b/tests/scripts/test_check_supply_chain.py
@@ -85,6 +85,24 @@ def test_action_pin_rejects_branch_ref(repo: Path) -> None:
assert _rules(mod.check_action_pin(path, "bad.yml")) == {"action-pin"}
+@pytest.mark.parametrize("access", ["read", "write"])
+def test_action_pin_ignores_status_permissions_but_checks_uses(repo: Path, access: str) -> None:
+ path = _write(
+ repo,
+ ".github/workflows/bad.yml",
+ f"permissions:\n statuses: {access}\nsteps:\n - uses: actions/checkout@v4\n",
+ )
+ violations = mod.check_action_pin(path, "bad.yml")
+ assert len(violations) == 1
+ assert violations[0].line == 4
+ assert violations[0].text == "- uses: actions/checkout@v4"
+
+
+def test_action_pin_checks_uses_in_flow_mapping(repo: Path) -> None:
+ path = _write(repo, ".github/workflows/bad.yml", "steps: [{ uses: actions/checkout@v4 }]\n")
+ assert _rules(mod.check_action_pin(path, "bad.yml")) == {"action-pin"}
+
+
def test_action_pin_allows_local_reusable_workflow(repo: Path) -> None:
path = _write(
repo,
From d7c150520efaf11e59212c3f534c3d2fe36def3c Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 07:22:29 -0700
Subject: [PATCH 08/22] test(ingestion): restore data-free ingest_batch_failed
route coverage for #2868
Mint TENANT_ID and KEY_ID with uuid4 at runtime. Re-add a POST /api/v1/webhooks/ingest failure test that uses caplog to check the traceback, tenant_id, event_count, source, error_type and correlation_id, and that the uuid4 canary lot code, product text and location never reach the log. A MagicMock persistence store raises to reach the handler's except block.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.../tests/test_ingest_error_logging_2868.py | 109 +++++++++++++++++-
1 file changed, 107 insertions(+), 2 deletions(-)
diff --git a/services/ingestion/tests/test_ingest_error_logging_2868.py b/services/ingestion/tests/test_ingest_error_logging_2868.py
index b98256dad..a2518690a 100644
--- a/services/ingestion/tests/test_ingest_error_logging_2868.py
+++ b/services/ingestion/tests/test_ingest_error_logging_2868.py
@@ -22,9 +22,12 @@
from __future__ import annotations
+import json
+import logging
import sys
import types
import uuid
+from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List
from unittest.mock import MagicMock
@@ -40,9 +43,13 @@
import app.webhook_router_v2 as wr # noqa: E402
from app.authz import IngestionPrincipal, get_ingestion_principal # noqa: E402
+from app.webhook_models import WebhookCTEType # noqa: E402
from app.webhook_router_v2.routes import _failure_diagnostics # noqa: E402
-TENANT_ID = "tenant-a"
+# Identifiers are minted per test run with uuid4; nothing here is a recorded
+# or invented customer value.
+TENANT_ID = str(uuid.uuid4())
+KEY_ID = str(uuid.uuid4())
def _build_app() -> FastAPI:
@@ -50,7 +57,7 @@ def _build_app() -> FastAPI:
app.include_router(wr.router)
app.dependency_overrides[wr._verify_api_key] = lambda: None
app.dependency_overrides[get_ingestion_principal] = lambda: IngestionPrincipal(
- key_id="test-key",
+ key_id=KEY_ID,
scopes=["*"],
tenant_id=TENANT_ID,
auth_mode="test",
@@ -112,6 +119,104 @@ def recorder(monkeypatch) -> _LogRecorder:
return rec
+# ---------------------------------------------------------------------------
+# POST /api/v1/webhooks/ingest
+#
+# WebhookPayload requires at least one structurally valid event, so the
+# request carries exactly one. Its lot code, product text and location are uuid4
+# canaries minted at runtime (not invented supply-chain records); they exist
+# only so the redaction assertion has something concrete to look for. The
+# persistence stub is a MagicMock whose batch store raises, which is the
+# smallest path into the handler's ``except Exception`` block.
+# ---------------------------------------------------------------------------
+
+
+class TestIngestBatchFailureLogging:
+ def _post(self, monkeypatch, caplog):
+ from app.subscription_gate import require_active_subscription
+ from shared.database import get_db_session
+
+ session = MagicMock()
+ persistence = MagicMock()
+ persistence.store_events_batch.side_effect = SystemError("store unavailable")
+ fake_cte = types.ModuleType("shared.cte_persistence")
+ fake_cte.CTEPersistence = lambda _s: persistence
+ monkeypatch.setitem(sys.modules, "shared.cte_persistence", fake_cte)
+
+ monkeypatch.setattr(wr, "consume_tenant_rate_limit", lambda **_kw: (True, 999))
+ monkeypatch.setattr("app.authz.consume_tenant_rate_limit", lambda **_kw: (True, 999))
+ # KDE completeness and alert generation are covered elsewhere; skip
+ # them so the one event reaches the batch store without extra fields.
+ monkeypatch.setattr(wr, "_validate_event_kdes", lambda _e: [])
+ monkeypatch.setattr(wr, "_generate_alerts", lambda _e: [])
+ monkeypatch.setattr(wr, "_rules_preeval_reject", lambda *_a, **_kw: (False, None, None))
+ for var in ("API_KEY", "REGENGINE_API_KEY", "WEBHOOK_HMAC_SECRET"):
+ monkeypatch.delenv(var, raising=False)
+
+ app = _build_app()
+ app.dependency_overrides[wr._verify_webhook_signature] = lambda: None
+ app.dependency_overrides[require_active_subscription] = lambda: None
+
+ def _db():
+ yield session
+
+ app.dependency_overrides[get_db_session] = _db
+
+ canary_tlc = uuid.uuid4().hex
+ canary_product = uuid.uuid4().hex
+ canary_location = uuid.uuid4().hex
+ source = uuid.uuid4().hex
+ payload = {
+ "source": source,
+ "events": [
+ {
+ "cte_type": WebhookCTEType.SHIPPING.value,
+ "traceability_lot_code": canary_tlc,
+ "product_description": canary_product,
+ "location_name": canary_location,
+ "quantity": 1,
+ "unit_of_measure": "",
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "kdes": {},
+ }
+ ],
+ }
+ with caplog.at_level(logging.DEBUG):
+ resp = TestClient(app).post(
+ "/api/v1/webhooks/ingest",
+ json=payload,
+ headers={"Idempotency-Key": uuid.uuid4().hex},
+ )
+ return resp, source, (canary_tlc, canary_product, canary_location)
+
+ def _record(self, caplog) -> logging.LogRecord:
+ matches = [r for r in caplog.records if "ingest_batch_failed" in r.getMessage()]
+ assert matches, f"expected an ingest_batch_failed log line; got {[r.getMessage() for r in caplog.records]}"
+ return matches[0]
+
+ def test_logs_traceback_tenant_count_and_source(self, monkeypatch, caplog) -> None:
+ resp, source, _canaries = self._post(monkeypatch, caplog)
+ assert resp.status_code == 500, resp.text
+ record = self._record(caplog)
+ fields = json.loads(record.getMessage())
+ assert record.levelno == logging.ERROR
+ assert "Traceback" in fields.get("exception", ""), "ingest_batch_failed must carry the traceback (#2868)"
+ assert fields["tenant_id"] == TENANT_ID, "without tenant_id there is no way to tell whose batch failed (#2868)"
+ assert fields["event_count"] == 1
+ assert fields["source"] == source
+ assert fields["error_type"] == "SystemError"
+ assert fields["correlation_id"]
+
+ def test_does_not_log_lot_code_or_product_text(self, monkeypatch, caplog) -> None:
+ """Diagnostics only, never the payload (#2868)."""
+ resp, _source, canaries = self._post(monkeypatch, caplog)
+ assert resp.status_code == 500
+ self._record(caplog)
+ logged = " ".join(r.getMessage() + (caplog.text or "") for r in caplog.records)
+ for canary in canaries:
+ assert canary not in logged, "request-body values must not reach the log (#2868)"
+
+
# ---------------------------------------------------------------------------
# Shared 5xx diagnostics (used by POST /api/v1/webhooks/ingest)
#
From 9cd13579c5554bc9dce87f84ff1b2cff62e15a41 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 07:22:31 -0700
Subject: [PATCH 09/22] fix(frontend): show QA-signal loading row on dashboard;
align qa-health test with ErrorState copy
---
frontend/src/__tests__/pages/qa-health.test.tsx | 2 +-
frontend/src/app/dashboard/page.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/__tests__/pages/qa-health.test.tsx b/frontend/src/__tests__/pages/qa-health.test.tsx
index a0ded69f5..24903b35d 100644
--- a/frontend/src/__tests__/pages/qa-health.test.tsx
+++ b/frontend/src/__tests__/pages/qa-health.test.tsx
@@ -168,7 +168,7 @@ describe('QAHealthPage', () => {
expect(screen.getByText('QA health request failed')).toBeInTheDocument();
expect(screen.getByRole('alert')).toHaveTextContent(
- 'The sysadmin QA endpoint did not return a dashboard summary.',
+ 'We could not load this page.',
);
expect(screen.queryByRole('heading', { name: 'QA Health' })).not.toBeInTheDocument();
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
index 9ccc34b9a..eb6a1286c 100644
--- a/frontend/src/app/dashboard/page.tsx
+++ b/frontend/src/app/dashboard/page.tsx
@@ -301,7 +301,7 @@ export default function DashboardPage() {
rowHref={(row) => row.href}
density="medium"
minWidth="480px"
- status={qa.status === "loading" ? "loading" : attentionRows.length === 0 ? "empty" : "idle"}
+ status={attentionRows.length === 0 ? "empty" : "idle"}
empty={{
icon: CheckCircle2,
title: "Nothing needs attention",
From 9466f30cc384c45ce80c603600e24fb1d33b05d8 Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 07:26:24 -0700
Subject: [PATCH 10/22] fix(tenant): update route manifest expectations for two
new tenant-scoped routes
---
tests/security/test_route_security_manifest.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/tests/security/test_route_security_manifest.py b/tests/security/test_route_security_manifest.py
index c043947b1..c7fea970a 100644
--- a/tests/security/test_route_security_manifest.py
+++ b/tests/security/test_route_security_manifest.py
@@ -68,7 +68,9 @@ def test_checked_in_manifest_matches_live_route_and_dependency_graph(manifest):
# +1 public: admin:POST:/api/v1/tools/check-access, the lead-capture
# tool-access token check the Next.js gate calls (#3318).
# +1 global-sysadmin: admin:PATCH:/admin/organizations/{org_id} (rename).
- "enabled_operation_count": 252,
+ # +2 tenant-scoped (SEL-421, #3492): admin:GET:/v1/supplier/facilities/{facility_id}/removal-check
+ # and admin:DELETE:/v1/supplier/facilities/{facility_id}, owner-only, tenant-filtered.
+ "enabled_operation_count": 254,
# 178/161 includes graph:GET:/api/v1/provisions/by-request: it is added
# to the graph v1_router AFTER router.include_router(v1_router), so the
# eager route copies of fastapi < 0.137 (under which the previous
@@ -92,7 +94,7 @@ def test_checked_in_manifest_matches_live_route_and_dependency_graph(manifest):
"enabled_by_classification": {
"global-sysadmin": 16,
"public": 30,
- "tenant-scoped": 199,
+ "tenant-scoped": 201,
"workload-only": 7,
},
}
@@ -185,8 +187,8 @@ def test_dual_session_routes_record_cookie_tenancy_and_csrf_adapter(manifest):
dual = [item for item in manifest["enabled_operations"] if item["test_adapter"] == "tenant_dual_session_csrf"]
mutations = [item for item in dual if item["method"] in {"POST", "PUT", "PATCH", "DELETE"}]
- assert len(dual) == 49
- assert len(mutations) == 22
+ assert len(dual) == 51
+ assert len(mutations) == 23
assert all(item["authentication"] == "bearer-token-or-opaque-session-cookie" for item in dual)
assert all(item["storage_boundary"] == "admin-postgres:jwt-tenant-or-session-membership-tenant" for item in dual)
From e0605c49ae0a4a96b2fa90568cf0381d1f1f5cae Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 08:23:00 -0700
Subject: [PATCH 11/22] ci: list 9 missing workflows in inventory; stop
action-pin matching 'statuses:' permissions
---
.github/workflows/copy-lint.yml | 4 ++--
scripts/ci/check_supply_chain.py | 2 +-
scripts/workflow_inventory.txt | 9 +++++++++
tests/scripts/test_check_supply_chain.py | 11 +++++++++++
4 files changed, 23 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/copy-lint.yml b/.github/workflows/copy-lint.yml
index 80fb0db51..606033b89 100644
--- a/.github/workflows/copy-lint.yml
+++ b/.github/workflows/copy-lint.yml
@@ -20,8 +20,8 @@ jobs:
report:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
+ - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
with:
node-version: '20'
- run: node --test scripts/copy-lint/check-copy.test.mjs
diff --git a/scripts/ci/check_supply_chain.py b/scripts/ci/check_supply_chain.py
index 9902f8204..9e0e1b779 100644
--- a/scripts/ci/check_supply_chain.py
+++ b/scripts/ci/check_supply_chain.py
@@ -35,7 +35,7 @@
from pathlib import Path
SHA40 = re.compile(r"^[0-9a-f]{40}$")
-USES_RE = re.compile(r"[\"']?uses[\"']?:\s*[\"']?([A-Za-z0-9_.\-/]+)(?:@([^\"'#\s]+))?[\"']?")
+USES_RE = re.compile(r"(? None:
assert mod.check_action_pin(path, "ok.yml") == []
+def test_action_pin_ignores_status_permissions_but_checks_uses(repo: Path) -> None:
+ path = _write(
+ repo,
+ ".github/workflows/statuses.yml",
+ "permissions:\n statuses: write\n statuses: read\nsteps:\n - uses: actions/checkout@v4\n - {uses: actions/setup-node@v4}\n",
+ )
+ violations = mod.check_action_pin(path, "statuses.yml")
+ assert [v.line for v in violations] == [5, 6]
+ assert [v.rule for v in violations] == ["action-pin", "action-pin"]
+
+
def test_action_pin_rejects_tag_ref(repo: Path) -> None:
path = _write(repo, ".github/workflows/bad.yml", "steps:\n - uses: actions/checkout@v4\n")
violations = mod.check_action_pin(path, "bad.yml")
From 547ed1630b2ba3c4f21df2ed3687de2d1c73cde6 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 13:21:22 -0700
Subject: [PATCH 12/22] ci(frontend): allowlist visual regression files in Knip
unused file guard
---
frontend/package.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frontend/package.json b/frontend/package.json
index 13d694fae..662ee8144 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,7 +22,9 @@
"knip": {
"entry": [
"scripts/design-lint.mjs",
- "tests/e2e/qa-2026-10-02/fe-regressions.config.ts"
+ "tests/e2e/qa-2026-10-02/fe-regressions.config.ts",
+ "playwright.visual.config.ts",
+ "tests/visual/*.spec.ts"
]
},
"//": "Pinning policy (#569): next, eslint-config-next, @sentry/nextjs, and @supabase/supabase-js are pinned to exact versions (no ^ or ~ prefix) because breaking changes in these packages have caused production incidents. Bump them deliberately after testing. All other deps use ^ for minor/patch auto-updates.",
From 987124f3a693be0ef9579ae16e00b958396554a2 Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 14:07:26 -0700
Subject: [PATCH 13/22] test(ingestion): fix-forward email_ingest principal
stub and sel409 admin DB env
Signed-off-by: Christopher Sellers <1@christophersellers.com>
---
.../ingestion/tests/test_email_ingest_service.py | 16 ++++++++--------
.../ingestion/tests/test_sel409_session_auth.py | 3 +++
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/services/ingestion/tests/test_email_ingest_service.py b/services/ingestion/tests/test_email_ingest_service.py
index 492ef34f1..d9146ee6e 100644
--- a/services/ingestion/tests/test_email_ingest_service.py
+++ b/services/ingestion/tests/test_email_ingest_service.py
@@ -52,9 +52,7 @@ def test_prefers_original_recipient(self):
assert _first_recipient(payload) == "preferred@tenant.co"
def test_strips_original_recipient(self):
- payload = PostmarkInboundPayload(
- MessageID="m1", From="a@example.com", OriginalRecipient=" spaced@tenant.co "
- )
+ payload = PostmarkInboundPayload(MessageID="m1", From="a@example.com", OriginalRecipient=" spaced@tenant.co ")
assert _first_recipient(payload) == "spaced@tenant.co"
def test_blank_original_recipient_falls_through_to_tofull(self):
@@ -67,9 +65,7 @@ def test_blank_original_recipient_falls_through_to_tofull(self):
assert _first_recipient(payload) == "tofull@tenant.co"
def test_falls_back_to_tofull_lowercase_key(self):
- payload = PostmarkInboundPayload(
- MessageID="m1", From="a@example.com", ToFull=[{"email": "lower@tenant.co"}]
- )
+ payload = PostmarkInboundPayload(MessageID="m1", From="a@example.com", ToFull=[{"email": "lower@tenant.co"}])
assert _first_recipient(payload) == "lower@tenant.co"
def test_tofull_skips_blank_entries_and_uses_first_valid(self):
@@ -527,7 +523,9 @@ async def test_real_ingest_csv_is_not_401d_by_the_api_key_presence_gate(self, mo
import app.csv_templates as csv_templates_mod
from app.webhook_models import IngestResponse
- async def fake_ingest_events(payload, *, principal_tenant_id, x_regengine_api_key=None, source_file=None):
+ async def fake_ingest_events(
+ payload, *, principal_tenant_id, x_regengine_api_key=None, principal=None, source_file=None
+ ):
if not x_regengine_api_key:
raise HTTPException(status_code=401, detail="Invalid or missing API key")
return IngestResponse(accepted=1, rejected=0, total=1, events=[])
@@ -780,7 +778,9 @@ async def test_unmapped_recipient_with_attachments_runs_no_ingestion_and_sets_no
# touch CSV ingestion nor set tenant context, even when it carries
# attachments. The claim row is still recorded as rejected for audit.
monkeypatch.setattr(service_mod, "get_inbound_email_audit_by_message_id", lambda db, mid: None)
- monkeypatch.setattr(service_mod.tenant_lookup, "lookup_tenant_by_inbound_address", lambda db, addr, sender: None)
+ monkeypatch.setattr(
+ service_mod.tenant_lookup, "lookup_tenant_by_inbound_address", lambda db, addr, sender: None
+ )
_stub_claim(monkeypatch, succeeds=True)
monkeypatch.setattr(service_mod, "finalize_inbound_email_audit", lambda db, **kwargs: {"id": "1"})
diff --git a/services/ingestion/tests/test_sel409_session_auth.py b/services/ingestion/tests/test_sel409_session_auth.py
index 54e3157ed..19582513e 100644
--- a/services/ingestion/tests/test_sel409_session_auth.py
+++ b/services/ingestion/tests/test_sel409_session_auth.py
@@ -15,6 +15,9 @@ def _request(path: str) -> Request:
@pytest.mark.asyncio
async def test_session_without_selected_rls_tenant_is_denied(monkeypatch):
+ monkeypatch.setenv("REGENGINE_ENV", "test")
+ monkeypatch.setenv("ADMIN_FALLBACK_SQLITE", "sqlite:///:memory:")
+ monkeypatch.delenv("ADMIN_DATABASE_URL", raising=False)
from services.admin.app import auth_routes, database, dependencies
async def verified_user(**_kwargs):
From 7818f30e9c06b516929662db692a62abbe617ddd Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 14:20:05 -0700
Subject: [PATCH 14/22] fix(deps): bump pypdf to 6.21.2 for
CVE-2026-115572/115585/115580
---
requirements.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements.lock b/requirements.lock
index bca5be258..01d0eaa5b 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -3198,9 +3198,9 @@ pyotp==2.10.0 \
--hash=sha256:1df2f6a1bcc3bb0716172a5215ddc2f8c7c7fd26a13df9927d52e1746934836c \
--hash=sha256:d01e9703443616b03c57c700b5cbffd56a1f929c1b0f8f03131bc78c1fca9d3f
# via -r requirements.in
-pypdf==6.21.1 \
- --hash=sha256:2d62c383acd6e0dff0a96322ba8b1135229343b084d73c6f6cfd947fd291afcb \
- --hash=sha256:553e2d42f5bb3bb2a785a7d3de8ef90bb7df74b5413eff5a7b4388007ea9254b
+pypdf==6.21.2 \
+ --hash=sha256:0a3e1c5c886a1be03646c16a04827cc166c2a0575c971b26a4bf89686551c2d4 \
+ --hash=sha256:1d44d1a3a200f25062adff0085b02040e4646431ac2ccac22e42679e91466084
# via -r requirements.in
pypdfium2==5.13.0 \
--hash=sha256:07f58e91b8c45ca144a1ff3008faf3c73ef8a5e9fb32988831788363288228cd \
From e1c82ea86ac079b2bf30f4e6b84bfad9879998ea Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 14:34:38 -0700
Subject: [PATCH 15/22] test(ingestion): fix suite A collection (2868
persistence import) and drop SAMPLE_EPCIS_EVENTS expectations
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.secrets.baseline | 20 +-
.../ingestion/tests/test_epcis_export_1342.py | 13 +-
.../tests/test_ingest_error_logging_2868.py | 287 ++++++++++--------
3 files changed, 164 insertions(+), 156 deletions(-)
diff --git a/.secrets.baseline b/.secrets.baseline
index 1c1a7110e..108b58015 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3230,24 +3230,6 @@
"line_number": 15
}
],
- "services/ingestion/tests/test_ingest_error_logging_2868.py": [
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_ingest_error_logging_2868.py",
- "hashed_secret": "33777c2f7ec7f479b1d0e0907b6b55c526491e78",
- "is_verified": false,
- "line_number": 52,
- "is_secret": false
- },
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_ingest_error_logging_2868.py",
- "hashed_secret": "0d2f6e84de0f98930bbc011a4cb95d931c35a93f",
- "is_verified": false,
- "line_number": 53,
- "is_secret": false
- }
- ],
"services/ingestion/tests/test_integration_router_1342.py": [
{
"type": "Secret Keyword",
@@ -4158,5 +4140,5 @@
}
]
},
- "generated_at": "2026-10-23T13:49:18Z"
+ "generated_at": "2026-10-23T21:34:25Z"
}
diff --git a/services/ingestion/tests/test_epcis_export_1342.py b/services/ingestion/tests/test_epcis_export_1342.py
index c5aed4ffa..b6f588157 100644
--- a/services/ingestion/tests/test_epcis_export_1342.py
+++ b/services/ingestion/tests/test_epcis_export_1342.py
@@ -6,6 +6,7 @@
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
+from uuid import uuid4
import pytest
from fastapi import FastAPI, Request
@@ -103,7 +104,7 @@ def test_no_rows_returns_empty_list(self, monkeypatch: pytest.MonkeyPatch) -> No
assert _query_tenant_events("t", None, None, None) == []
def test_swallow_exception_returns_empty_list(self, monkeypatch: pytest.MonkeyPatch) -> None:
- session = _FakeSession(execute_queue=[RuntimeError("db down")])
+ session = _FakeSession(execute_queue=[RuntimeError()])
monkeypatch.setattr(epcis_export, "get_db_safe", lambda: session)
result = _query_tenant_events("t", None, None, None)
assert result == []
@@ -131,7 +132,7 @@ async def _tenant_ctx(request: Request) -> TenantContext:
return TenantContext(
tenant_id=tenant_id or "t",
principal_kind="test",
- principal_id="t",
+ principal_id=uuid4().hex,
)
app.dependency_overrides[_verify_api_key] = lambda: None
@@ -143,7 +144,6 @@ class TestExportEpcisEndpoint:
def test_returns_empty_events_when_no_tenant_rows(
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
- monkeypatch.setattr(epcis_export, "SAMPLE_EPCIS_EVENTS", [])
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
response = client.post(
"/api/v1/export/epcis",
@@ -219,7 +219,6 @@ def test_validate_flag_emits_validation_block_and_header(
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
- monkeypatch.setattr(epcis_export, "SAMPLE_EPCIS_EVENTS", [])
response = client.post(
"/api/v1/export/epcis?validate=true",
json={"tenant_id": "t"},
@@ -238,7 +237,7 @@ def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest.
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [])
response = client.post(
"/api/v1/export/fda",
- json={"tenant_id": "tenant-x"},
+ json={"tenant_id": "t"},
)
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/csv")
@@ -246,9 +245,7 @@ def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest.
assert response.headers["X-RegEngine-Events-Count"] == "0"
assert response.headers["X-RegEngine-Message"] == "No data yet"
assert "X-RegEngine-Disclaimer" not in response.headers
- assert response.text == epcis_export.FDA_CSV_HEADER + "\n"
-
- assert response.status_code == 422
+ assert [line for line in response.text.splitlines() if line] == [epcis_export.FDA_CSV_HEADER]
class TestListExportFormats:
diff --git a/services/ingestion/tests/test_ingest_error_logging_2868.py b/services/ingestion/tests/test_ingest_error_logging_2868.py
index d2414ca72..a2518690a 100644
--- a/services/ingestion/tests/test_ingest_error_logging_2868.py
+++ b/services/ingestion/tests/test_ingest_error_logging_2868.py
@@ -17,17 +17,23 @@
* and **no customer supply-chain data** rides along. This is a food-safety
compliance platform: lot codes and product descriptions identify a
specific customer's shipment, so an over-eager log line is its own
- incident. The last test locks that boundary.
+ incident. ``TestFailureDiagnostics`` locks that boundary.
"""
from __future__ import annotations
+import json
+import logging
import sys
import types
+import uuid
+from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List
+from unittest.mock import MagicMock
import pytest
+from fastapi import FastAPI
from fastapi.testclient import TestClient
service_dir = Path(__file__).parent.parent
@@ -36,21 +42,27 @@
pytest.importorskip("fastapi")
import app.webhook_router_v2 as wr # noqa: E402
-
-from services.ingestion.tests.test_webhook_router_v2_1342 import ( # noqa: E402
- _FakePersistence,
- _FakeSession,
- _build_app,
- _client,
- _make_shipping_event,
-)
-
-TENANT_ID = "tenant-a"
-
-# Values that must never reach a log line. These are the customer's
-# supply-chain identifiers, not diagnostics.
-SECRET_TLC = "TLC-CUSTOMER-CONFIDENTIAL-0001"
-SECRET_PRODUCT = "Romaine Hearts Lot 42 for Metro DC"
+from app.authz import IngestionPrincipal, get_ingestion_principal # noqa: E402
+from app.webhook_models import WebhookCTEType # noqa: E402
+from app.webhook_router_v2.routes import _failure_diagnostics # noqa: E402
+
+# Identifiers are minted per test run with uuid4; nothing here is a recorded
+# or invented customer value.
+TENANT_ID = str(uuid.uuid4())
+KEY_ID = str(uuid.uuid4())
+
+
+def _build_app() -> FastAPI:
+ app = FastAPI()
+ app.include_router(wr.router)
+ app.dependency_overrides[wr._verify_api_key] = lambda: None
+ app.dependency_overrides[get_ingestion_principal] = lambda: IngestionPrincipal(
+ key_id=KEY_ID,
+ scopes=["*"],
+ tenant_id=TENANT_ID,
+ auth_mode="test",
+ )
+ return app
class _LogRecorder:
@@ -61,9 +73,7 @@ def __init__(self) -> None:
def _record(self, level: str):
def _emit(event: Any = None, *args: Any, **kwargs: Any) -> None:
- self.calls.append(
- {"level": level, "event": event, "args": args, "kwargs": kwargs}
- )
+ self.calls.append({"level": level, "event": event, "args": args, "kwargs": kwargs})
return _emit
@@ -74,9 +84,7 @@ def __getattr__(self, name: str):
def find(self, event: str) -> Dict[str, Any]:
matches = [c for c in self.calls if c["event"] == event]
- assert matches, (
- f"expected a {event!r} log line; got {[c['event'] for c in self.calls]}"
- )
+ assert matches, f"expected a {event!r} log line; got {[c['event'] for c in self.calls]}"
return matches[0]
def payload_text(self) -> str:
@@ -94,8 +102,8 @@ def _fields(call: Dict[str, Any]) -> Dict[str, Any]:
def _assert_traceback_captured(call: Dict[str, Any]) -> None:
- captured = call["level"] == "exception" or bool(_fields(call).get("exc_info")) or bool(
- call["kwargs"].get("exc_info")
+ captured = (
+ call["level"] == "exception" or bool(_fields(call).get("exc_info")) or bool(call["kwargs"].get("exc_info"))
)
assert captured, (
"the 500 handler tells the caller to check the logs, so the log line "
@@ -113,108 +121,135 @@ def recorder(monkeypatch) -> _LogRecorder:
# ---------------------------------------------------------------------------
# POST /api/v1/webhooks/ingest
+#
+# WebhookPayload requires at least one structurally valid event, so the
+# request carries exactly one. Its lot code, product text and location are uuid4
+# canaries minted at runtime (not invented supply-chain records); they exist
+# only so the redaction assertion has something concrete to look for. The
+# persistence stub is a MagicMock whose batch store raises, which is the
+# smallest path into the handler's ``except Exception`` block.
# ---------------------------------------------------------------------------
class TestIngestBatchFailureLogging:
- def _payload(self) -> dict:
- event = _make_shipping_event(tlc=SECRET_TLC)
- event["product_description"] = SECRET_PRODUCT
- return {"source": "erp-connector", "events": [event]}
-
- def _blow_up(self, monkeypatch, exc: Exception):
- session = _FakeSession()
-
- class _BoomPersistence(_FakePersistence):
- def store_events_batch(self, **kw):
- raise exc
-
- persistence = _BoomPersistence(session)
- return _client(monkeypatch, session=session, persistence=persistence)
-
- def test_logs_traceback(self, monkeypatch, recorder: _LogRecorder) -> None:
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- resp = client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-tb"},
- )
- assert resp.status_code == 500
- _assert_traceback_captured(recorder.find("ingest_batch_failed"))
-
- def test_logs_tenant_and_correlation_id(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- resp = client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-ids"},
- )
+ def _post(self, monkeypatch, caplog):
+ from app.subscription_gate import require_active_subscription
+ from shared.database import get_db_session
+
+ session = MagicMock()
+ persistence = MagicMock()
+ persistence.store_events_batch.side_effect = SystemError("store unavailable")
+ fake_cte = types.ModuleType("shared.cte_persistence")
+ fake_cte.CTEPersistence = lambda _s: persistence
+ monkeypatch.setitem(sys.modules, "shared.cte_persistence", fake_cte)
+
+ monkeypatch.setattr(wr, "consume_tenant_rate_limit", lambda **_kw: (True, 999))
+ monkeypatch.setattr("app.authz.consume_tenant_rate_limit", lambda **_kw: (True, 999))
+ # KDE completeness and alert generation are covered elsewhere; skip
+ # them so the one event reaches the batch store without extra fields.
+ monkeypatch.setattr(wr, "_validate_event_kdes", lambda _e: [])
+ monkeypatch.setattr(wr, "_generate_alerts", lambda _e: [])
+ monkeypatch.setattr(wr, "_rules_preeval_reject", lambda *_a, **_kw: (False, None, None))
+ for var in ("API_KEY", "REGENGINE_API_KEY", "WEBHOOK_HMAC_SECRET"):
+ monkeypatch.delenv(var, raising=False)
+
+ app = _build_app()
+ app.dependency_overrides[wr._verify_webhook_signature] = lambda: None
+ app.dependency_overrides[require_active_subscription] = lambda: None
+
+ def _db():
+ yield session
+
+ app.dependency_overrides[get_db_session] = _db
+
+ canary_tlc = uuid.uuid4().hex
+ canary_product = uuid.uuid4().hex
+ canary_location = uuid.uuid4().hex
+ source = uuid.uuid4().hex
+ payload = {
+ "source": source,
+ "events": [
+ {
+ "cte_type": WebhookCTEType.SHIPPING.value,
+ "traceability_lot_code": canary_tlc,
+ "product_description": canary_product,
+ "location_name": canary_location,
+ "quantity": 1,
+ "unit_of_measure": "",
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "kdes": {},
+ }
+ ],
+ }
+ with caplog.at_level(logging.DEBUG):
+ resp = TestClient(app).post(
+ "/api/v1/webhooks/ingest",
+ json=payload,
+ headers={"Idempotency-Key": uuid.uuid4().hex},
+ )
+ return resp, source, (canary_tlc, canary_product, canary_location)
+
+ def _record(self, caplog) -> logging.LogRecord:
+ matches = [r for r in caplog.records if "ingest_batch_failed" in r.getMessage()]
+ assert matches, f"expected an ingest_batch_failed log line; got {[r.getMessage() for r in caplog.records]}"
+ return matches[0]
+
+ def test_logs_traceback_tenant_count_and_source(self, monkeypatch, caplog) -> None:
+ resp, source, _canaries = self._post(monkeypatch, caplog)
+ assert resp.status_code == 500, resp.text
+ record = self._record(caplog)
+ fields = json.loads(record.getMessage())
+ assert record.levelno == logging.ERROR
+ assert "Traceback" in fields.get("exception", ""), "ingest_batch_failed must carry the traceback (#2868)"
+ assert fields["tenant_id"] == TENANT_ID, "without tenant_id there is no way to tell whose batch failed (#2868)"
+ assert fields["event_count"] == 1
+ assert fields["source"] == source
+ assert fields["error_type"] == "SystemError"
+ assert fields["correlation_id"]
+
+ def test_does_not_log_lot_code_or_product_text(self, monkeypatch, caplog) -> None:
+ """Diagnostics only, never the payload (#2868)."""
+ resp, _source, canaries = self._post(monkeypatch, caplog)
assert resp.status_code == 500
- fields = _fields(recorder.find("ingest_batch_failed"))
- assert fields.get("tenant_id") == TENANT_ID, (
- "without tenant_id there is no way to tell whose batch failed (#2868)"
- )
- assert fields.get("correlation_id"), (
- "the 500 body points the caller at the logs; a correlation id is "
- "what lets support join the two (#2868)"
- )
+ self._record(caplog)
+ logged = " ".join(r.getMessage() + (caplog.text or "") for r in caplog.records)
+ for canary in canaries:
+ assert canary not in logged, "request-body values must not reach the log (#2868)"
- def test_logs_error_type_so_outage_differs_from_bug(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- """A bare ``except Exception`` makes every failure look alike."""
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-type"},
- )
- fields = _fields(recorder.find("ingest_batch_failed"))
- assert fields.get("error_type") == "SystemError", (
- "the handler catches bare Exception, so the log line must name the "
- "exception class or a DB outage is indistinguishable from a "
- "programming error (#2868)"
- )
- def test_logs_batch_shape_for_triage(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-shape"},
- )
- fields = _fields(recorder.find("ingest_batch_failed"))
- assert fields.get("event_count") == 1
- assert fields.get("source") == "erp-connector"
-
- def test_does_not_log_customer_supply_chain_data(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
- """Diagnostics only — never the payload.
-
- Lot codes and product descriptions identify a specific customer's
- shipment. Fixing an under-logging bug by dumping the request body
- would trade a debuggability gap for a data-handling incident.
- """
- client, _s, _p = self._blow_up(monkeypatch, SystemError("wholly unexpected"))
- client.post(
- "/api/v1/webhooks/ingest",
- json=self._payload(),
- headers={"Idempotency-Key": "idem-2868-pii"},
- )
- logged = recorder.payload_text()
- assert SECRET_TLC not in logged, (
- "traceability lot codes are customer supply-chain data and must "
- "not be logged (#2868)"
- )
- assert SECRET_PRODUCT not in logged, (
- "product descriptions are customer supply-chain data and must not "
- "be logged (#2868)"
- )
+# ---------------------------------------------------------------------------
+# Shared 5xx diagnostics (used by POST /api/v1/webhooks/ingest)
+#
+# The ingest-batch route cases were removed: they needed a fabricated
+# shipping event and an in-memory persistence fake, both dropped in the
+# real-data-only test cleanup. The diagnostics contract the ingest handler
+# relies on is checked directly, with no request body at all.
+# ---------------------------------------------------------------------------
+
+
+class TestFailureDiagnostics:
+ def _request(self, correlation_id=None) -> MagicMock:
+ request = MagicMock()
+ request.state.correlation_id = correlation_id
+ return request
+
+ def test_names_error_type_so_outage_differs_from_bug(self) -> None:
+ diag = _failure_diagnostics(self._request(), SystemError())
+ assert diag["error_type"] == "SystemError"
+
+ def test_always_carries_correlation_id(self) -> None:
+ diag = _failure_diagnostics(self._request(), RuntimeError())
+ assert diag["correlation_id"]
+
+ def test_reuses_request_correlation_id(self) -> None:
+ cid = str(uuid.uuid4())
+ diag = _failure_diagnostics(self._request(cid), RuntimeError())
+ assert diag["correlation_id"] == cid
+
+ def test_only_diagnostic_fields(self) -> None:
+ """Diagnostics only, never the payload (#2868)."""
+ diag = _failure_diagnostics(self._request(), RuntimeError())
+ assert set(diag) == {"correlation_id", "error", "error_type"}
# ---------------------------------------------------------------------------
@@ -224,7 +259,7 @@ def test_does_not_log_customer_supply_chain_data(
class TestChainVerifyFailureLogging:
def _client(self, monkeypatch) -> TestClient:
- session = _FakeSession()
+ session = MagicMock()
class _BadPersistence:
def __init__(self, _s):
@@ -249,9 +284,7 @@ def verify_chain(self, _tid):
app = _build_app()
return TestClient(app)
- def test_logs_traceback_tenant_and_error_type(
- self, monkeypatch, recorder: _LogRecorder
- ) -> None:
+ def test_logs_traceback_tenant_and_error_type(self, monkeypatch, recorder: _LogRecorder) -> None:
client = self._client(monkeypatch)
resp = client.get(
"/api/v1/webhooks/chain/verify",
@@ -262,11 +295,7 @@ def test_logs_traceback_tenant_and_error_type(
_assert_traceback_captured(call)
fields = _fields(call)
assert fields.get("tenant_id") == TENANT_ID, (
- "chain verification failures must name the tenant whose hash "
- "chain could not be walked (#2868)"
+ "chain verification failures must name the tenant whose hash chain could not be walked (#2868)"
)
assert fields.get("error_type") == "RuntimeError"
- assert fields.get("correlation_id"), (
- "a correlation id is what makes 'check server logs' actionable "
- "(#2868)"
- )
+ assert fields.get("correlation_id"), "a correlation id is what makes 'check server logs' actionable (#2868)"
From 21288efe5f9cd16ae16a9de186e13407f22c7be0 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 14:35:17 -0700
Subject: [PATCH 16/22] fix(ci): stop Head SHA Check Guard failing main on
check_suite/workflow_run events
Those events run in the default-branch context, so failed PR verdicts were
attributed to main. Keep the failure commit status on the PR head SHA; only
fail the job for pull_request events.
---
.github/workflows/head-sha-check-guard.yml | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/head-sha-check-guard.yml b/.github/workflows/head-sha-check-guard.yml
index 1ec7fce4b..f9ed150d4 100644
--- a/.github/workflows/head-sha-check-guard.yml
+++ b/.github/workflows/head-sha-check-guard.yml
@@ -41,6 +41,18 @@ jobs:
const targetUrl = `${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
+ // check_suite and workflow_run events run in the default-branch context, so a
+ // failed job there is attributed to main even though it evaluated a PR head.
+ // For those events the PR verdict is carried by the commit status posted on the
+ // PR head SHA; only fail the job itself for pull_request events.
+ const failGuard = (msg) => {
+ if (context.eventName === 'pull_request') {
+ core.setFailed(msg);
+ } else {
+ core.warning(`${msg} (reported via commit status; not failing ${context.eventName} run on default branch)`);
+ }
+ };
+
// Prevent self-trigger loops
if (context.eventName === 'workflow_run' && context.payload.workflow_run?.name === 'Head SHA Check Guard') {
core.info('Skipping run triggered by own workflow_run completion.');
@@ -143,7 +155,7 @@ jobs:
const prHeadSha = matchedPr.head?.sha;
if (!prHeadSha) {
- core.setFailed(`PR #${prNumber} has no head SHA.`);
+ failGuard(`PR #${prNumber} has no head SHA.`);
return;
}
@@ -160,7 +172,7 @@ jobs:
description: 'Head moved',
target_url: targetUrl,
});
- core.setFailed(`Head moved: PR #${prNumber} head is ${prHeadSha}, evaluated commit was ${evaluatedSha}`);
+ failGuard(`Head moved: PR #${prNumber} head is ${prHeadSha}, evaluated commit was ${evaluatedSha}`);
return;
}
@@ -314,7 +326,7 @@ jobs:
description: 'Head moved',
target_url: targetUrl,
});
- core.setFailed(`Head moved: PR #${prNumber} head is ${latestPrAfterWait.head?.sha}, evaluated commit was ${evaluatedSha}`);
+ failGuard(`Head moved: PR #${prNumber} head is ${latestPrAfterWait.head?.sha}, evaluated commit was ${evaluatedSha}`);
return;
}
@@ -394,7 +406,7 @@ jobs:
description: statusDesc,
target_url: targetUrl,
});
- core.setFailed(`Head SHA Check Guard failed on head SHA ${evaluatedSha}. Failed checks: ${failedList}`);
+ failGuard(`Head SHA Check Guard failed on head SHA ${evaluatedSha}. Failed checks: ${failedList}`);
return;
}
From 30cc4833737e1fde4e29ae78441d02e849b379ac Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 14:37:35 -0700
Subject: [PATCH 17/22] ci: clear detect-secrets false positive in
vercel-preview-guard
---
.github/workflows/vercel-preview-guard.yml | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/vercel-preview-guard.yml b/.github/workflows/vercel-preview-guard.yml
index fcb62cbcd..85d46ad9d 100644
--- a/.github/workflows/vercel-preview-guard.yml
+++ b/.github/workflows/vercel-preview-guard.yml
@@ -107,22 +107,22 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
PREVIEW_URL: ${{ needs.preview.outputs.preview_url }}
- BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
+ BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} # pragma: allowlist secret
with:
github-token: ${{ github.token }}
script: |
const marker = '';
- const secret = process.env.BYPASS_SECRET;
- if (secret) core.setSecret(secret); // Emits ::add-mask:: before using it.
+ const bypassValue = process.env.BYPASS_SECRET;
+ if (bypassValue) core.setSecret(bypassValue); // Emits ::add-mask:: before using it.
const preview = process.env.PREVIEW_URL;
if (!preview) throw new Error('The preview job did not return an access URL');
const url = new URL(preview);
- if (secret) {
- url.searchParams.set('x-vercel-protection-bypass', secret);
+ if (bypassValue) {
+ url.searchParams.set('x-vercel-protection-bypass', bypassValue);
url.searchParams.set('x-vercel-set-bypass-cookie', 'true');
}
const body = `${marker}\nVercel preview: ${url.toString()}` +
- (secret ? '' : '\n\nThe bypass secret is not configured.');
+ (bypassValue ? '' : '\n\nThe bypass secret is not configured.');
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, {
From b8d901531a4a60bd96b8882199ba9d8515bae1cd Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Fri, 23 Oct 2026 14:39:29 -0700
Subject: [PATCH 18/22] test(ingestion): make 944/945 envelope test data-free
after #3500 fixture removal
---
.../tests/test_edi_envelope_integrity.py | 28 ++++++++++---------
1 file changed, 15 insertions(+), 13 deletions(-)
diff --git a/services/ingestion/tests/test_edi_envelope_integrity.py b/services/ingestion/tests/test_edi_envelope_integrity.py
index 3088a4e0f..caaca9a61 100644
--- a/services/ingestion/tests/test_edi_envelope_integrity.py
+++ b/services/ingestion/tests/test_edi_envelope_integrity.py
@@ -133,22 +133,24 @@ def test_all_four_extractors_populate_envelope():
def test_warehouse_extractors_populate_envelope():
"""944 and 945 carry the same ISA and GS envelope keys as 856."""
- from pathlib import Path
-
+ # Coverage gap: 944/945 envelope extraction from a real recorded warehouse advice is not covered
+ # because no real recorded 944/945 file exists in the repo (fixtures removed in #3500).
from app.edi_ingestion.extractors import _extract_944_fields, _extract_945_fields
- corpus = Path(__file__).parent / "fixtures" / "edi_corpus"
- for name, extractor in (
- ("receipt_944.edi", _extract_944_fields),
- ("advice_945.edi", _extract_945_fields),
- ):
- segments = _parse_x12_segments((corpus / name).read_text(encoding="utf-8"))
+ raw = _build_isa() + _GS_BASE
+ segments = _parse_x12_segments(raw)
+ expected_env = _extract_envelope_ids(segments)
+
+ for extractor in (_extract_944_fields, _extract_945_fields):
data = extractor(segments)
- assert data["isa13"]
- assert data["gs_sender_id"] == "SENDERID"
- assert data["sender_id"] == "SENDERID"
- assert data["receiver_id"] == "RECEIVERID"
- assert data["envelope_mismatch"] is False
+ for key in ("isa13", "gs_sender_id", "sender_id", "receiver_id", "envelope_mismatch"):
+ assert key in data
+ assert data["sender_id"] == data["gs_sender_id"]
+ assert data["isa13"] == expected_env["isa13"]
+ assert data["gs_sender_id"] == expected_env["gs_sender_id"]
+ assert data["sender_id"] == expected_env["sender_id"]
+ assert data["receiver_id"] == expected_env["receiver_id"]
+ assert data["envelope_mismatch"] == expected_env["envelope_mismatch"]
# ---------------------------------------------------------------------------
From 182a4e45487817e120abb82a8606d95d64cc94d9 Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 14:51:04 -0700
Subject: [PATCH 19/22] ci: refresh .secrets.baseline line numbers
(detect-secrets 1.5.0, no new findings)
---
.secrets.baseline | 32 +++++++-------------------------
1 file changed, 7 insertions(+), 25 deletions(-)
diff --git a/.secrets.baseline b/.secrets.baseline
index dc26a198f..20d3aa5c8 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -141,35 +141,35 @@
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_verified": false,
- "line_number": 136
+ "line_number": 132
},
{
"type": "Basic Auth Credentials",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_verified": false,
- "line_number": 219
+ "line_number": 215
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "b71d494da8d930401ba9b32b7cef87295f3eb6d3",
"is_verified": false,
- "line_number": 223
+ "line_number": 219
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "d86daa0361b89a2c9f4b95648b9ad17edf6f92ae",
"is_verified": false,
- "line_number": 229
+ "line_number": 225
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/backend-ci.yml",
"hashed_secret": "eec69da96b4b2984741269925f0a8e46a1e03468",
"is_verified": false,
- "line_number": 231
+ "line_number": 227
}
],
".github/workflows/frontend-ci.yml": [
@@ -3203,22 +3203,13 @@
"line_number": 142
}
],
- "services/ingestion/tests/test_epcis_gln_check_digit.py": [
- {
- "type": "Hex High Entropy String",
- "filename": "services/ingestion/tests/test_epcis_gln_check_digit.py",
- "hashed_secret": "8e4456ef140191dfda4a6d287b36e5451d1d7d79",
- "is_verified": false,
- "line_number": 64
- }
- ],
"services/ingestion/tests/test_epcis_tenant_isolation.py": [
{
"type": "Secret Keyword",
"filename": "services/ingestion/tests/test_epcis_tenant_isolation.py",
"hashed_secret": "9762e53f3b6420f10041406e88c337725670824b",
"is_verified": false,
- "line_number": 135
+ "line_number": 124
}
],
"services/ingestion/tests/test_fsma_plugin_catalog.py": [
@@ -3275,15 +3266,6 @@
"line_number": 276
}
],
- "services/ingestion/tests/test_qa4_webhook_inbound.py": [
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_qa4_webhook_inbound.py",
- "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de",
- "is_verified": false,
- "line_number": 121
- }
- ],
"services/ingestion/tests/test_regengine_sources_rest_qa.py": [
{
"type": "Hex High Entropy String",
@@ -4176,5 +4158,5 @@
}
]
},
- "generated_at": "2026-10-23T09:48:32Z"
+ "generated_at": "2026-10-23T21:50:55Z"
}
From e65bea9e72770eeecd8ff435cbf28b02db0c4a05 Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 15:09:49 -0700
Subject: [PATCH 20/22] test(ingestion): capture ingest_batch_failed via
dedicated logger; bump factory-local pypdf floor
---
packages/factory-local/requirements.txt | 2 +-
.../tests/test_ingest_error_logging_2868.py | 69 ++++++++++++++-----
2 files changed, 52 insertions(+), 19 deletions(-)
diff --git a/packages/factory-local/requirements.txt b/packages/factory-local/requirements.txt
index eb943d2bb..890ce9304 100644
--- a/packages/factory-local/requirements.txt
+++ b/packages/factory-local/requirements.txt
@@ -8,6 +8,6 @@ neo4j>=5.23,<6
streamlit>=1.38,<2
pytest>=9.0.3,<10
beautifulsoup4>=4.12,<5
-pypdf>=5,<7
+pypdf>=6.21.2,<7
openai>=1.40,<3
python-dotenv>=1.0,<2
diff --git a/services/ingestion/tests/test_ingest_error_logging_2868.py b/services/ingestion/tests/test_ingest_error_logging_2868.py
index a2518690a..102cae0b6 100644
--- a/services/ingestion/tests/test_ingest_error_logging_2868.py
+++ b/services/ingestion/tests/test_ingest_error_logging_2868.py
@@ -29,10 +29,11 @@
import uuid
from datetime import datetime, timezone
from pathlib import Path
-from typing import Any, Dict, List
+from typing import Any, Dict, Iterator, List
from unittest.mock import MagicMock
import pytest
+import structlog
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -119,6 +120,39 @@ def recorder(monkeypatch) -> _LogRecorder:
return rec
+@pytest.fixture()
+def batch_log_capture(monkeypatch) -> Iterator[List[logging.LogRecord]]:
+ records: List[logging.LogRecord] = []
+
+ class _ListHandler(logging.Handler):
+ def emit(self, record: logging.LogRecord) -> None:
+ records.append(record)
+
+ stdlib_logger = logging.getLogger(f"test.ingest_batch_failure.{uuid.uuid4().hex}")
+ stdlib_logger.setLevel(logging.DEBUG)
+ stdlib_logger.propagate = False
+ handler = _ListHandler()
+ stdlib_logger.addHandler(handler)
+ monkeypatch.setattr(
+ wr,
+ "logger",
+ structlog.wrap_logger(
+ stdlib_logger,
+ processors=[
+ structlog.stdlib.add_log_level,
+ structlog.processors.format_exc_info,
+ structlog.processors.JSONRenderer(),
+ ],
+ wrapper_class=structlog.stdlib.BoundLogger,
+ ),
+ )
+ try:
+ yield records
+ finally:
+ stdlib_logger.removeHandler(handler)
+ handler.close()
+
+
# ---------------------------------------------------------------------------
# POST /api/v1/webhooks/ingest
#
@@ -132,7 +166,7 @@ def recorder(monkeypatch) -> _LogRecorder:
class TestIngestBatchFailureLogging:
- def _post(self, monkeypatch, caplog):
+ def _post(self, monkeypatch):
from app.subscription_gate import require_active_subscription
from shared.database import get_db_session
@@ -181,23 +215,22 @@ def _db():
}
],
}
- with caplog.at_level(logging.DEBUG):
- resp = TestClient(app).post(
- "/api/v1/webhooks/ingest",
- json=payload,
- headers={"Idempotency-Key": uuid.uuid4().hex},
- )
+ resp = TestClient(app).post(
+ "/api/v1/webhooks/ingest",
+ json=payload,
+ headers={"Idempotency-Key": uuid.uuid4().hex},
+ )
return resp, source, (canary_tlc, canary_product, canary_location)
- def _record(self, caplog) -> logging.LogRecord:
- matches = [r for r in caplog.records if "ingest_batch_failed" in r.getMessage()]
- assert matches, f"expected an ingest_batch_failed log line; got {[r.getMessage() for r in caplog.records]}"
+ def _record(self, records: List[logging.LogRecord]) -> logging.LogRecord:
+ matches = [r for r in records if "ingest_batch_failed" in r.getMessage()]
+ assert matches, f"expected an ingest_batch_failed log line; got {[r.getMessage() for r in records]}"
return matches[0]
- def test_logs_traceback_tenant_count_and_source(self, monkeypatch, caplog) -> None:
- resp, source, _canaries = self._post(monkeypatch, caplog)
+ def test_logs_traceback_tenant_count_and_source(self, monkeypatch, batch_log_capture) -> None:
+ resp, source, _canaries = self._post(monkeypatch)
assert resp.status_code == 500, resp.text
- record = self._record(caplog)
+ record = self._record(batch_log_capture)
fields = json.loads(record.getMessage())
assert record.levelno == logging.ERROR
assert "Traceback" in fields.get("exception", ""), "ingest_batch_failed must carry the traceback (#2868)"
@@ -207,12 +240,12 @@ def test_logs_traceback_tenant_count_and_source(self, monkeypatch, caplog) -> No
assert fields["error_type"] == "SystemError"
assert fields["correlation_id"]
- def test_does_not_log_lot_code_or_product_text(self, monkeypatch, caplog) -> None:
+ def test_does_not_log_lot_code_or_product_text(self, monkeypatch, batch_log_capture) -> None:
"""Diagnostics only, never the payload (#2868)."""
- resp, _source, canaries = self._post(monkeypatch, caplog)
+ resp, _source, canaries = self._post(monkeypatch)
assert resp.status_code == 500
- self._record(caplog)
- logged = " ".join(r.getMessage() + (caplog.text or "") for r in caplog.records)
+ self._record(batch_log_capture)
+ logged = " ".join(r.getMessage() for r in batch_log_capture)
for canary in canaries:
assert canary not in logged, "request-body values must not reach the log (#2868)"
From 866471d9f8ce35520ccd80f351fe5e631620b133 Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 15:12:16 -0700
Subject: [PATCH 21/22] ci: refresh .secrets.baseline after test change (drop
stale entry)
---
.secrets.baseline | 20 +-------------------
1 file changed, 1 insertion(+), 19 deletions(-)
diff --git a/.secrets.baseline b/.secrets.baseline
index 20d3aa5c8..38bb04d34 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3230,24 +3230,6 @@
"line_number": 15
}
],
- "services/ingestion/tests/test_ingest_error_logging_2868.py": [
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_ingest_error_logging_2868.py",
- "hashed_secret": "33777c2f7ec7f479b1d0e0907b6b55c526491e78",
- "is_verified": false,
- "line_number": 52,
- "is_secret": false
- },
- {
- "type": "Secret Keyword",
- "filename": "services/ingestion/tests/test_ingest_error_logging_2868.py",
- "hashed_secret": "0d2f6e84de0f98930bbc011a4cb95d931c35a93f",
- "is_verified": false,
- "line_number": 53,
- "is_secret": false
- }
- ],
"services/ingestion/tests/test_integration_router_1342.py": [
{
"type": "Secret Keyword",
@@ -4158,5 +4140,5 @@
}
]
},
- "generated_at": "2026-10-23T21:50:55Z"
+ "generated_at": "2026-10-23T22:12:00Z"
}
From 8ed7c068a5e39fc45cf16e5a4dd06e3e50170c6c Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Fri, 23 Oct 2026 15:19:12 -0700
Subject: [PATCH 22/22] fix: address CodeRabbit review on #3552 (checkout
creds, retry a11y name, EDI envelope literals, duplicate test name)
---
.github/workflows/copy-lint.yml | 2 ++
frontend/src/__tests__/components/error-state.test.tsx | 2 +-
frontend/src/__tests__/pages/dashboard-load-errors.test.tsx | 2 +-
frontend/src/components/ui/error-state.tsx | 2 +-
services/ingestion/tests/test_edi_envelope_integrity.py | 2 ++
tests/scripts/test_check_supply_chain.py | 2 +-
6 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/copy-lint.yml b/.github/workflows/copy-lint.yml
index 606033b89..3a4712820 100644
--- a/.github/workflows/copy-lint.yml
+++ b/.github/workflows/copy-lint.yml
@@ -21,6 +21,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
+ with:
+ persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
with:
node-version: '20'
diff --git a/frontend/src/__tests__/components/error-state.test.tsx b/frontend/src/__tests__/components/error-state.test.tsx
index f59841bcc..2f2425f45 100644
--- a/frontend/src/__tests__/components/error-state.test.tsx
+++ b/frontend/src/__tests__/components/error-state.test.tsx
@@ -7,7 +7,7 @@ describe('ErrorState', () => {
render();
expect(screen.getByRole('alert')).toHaveClass('bg-re-danger-bg', 'm-4');
- expect(screen.getByRole('button', { name: 'Retry' })).toBeDisabled();
+ expect(screen.getByRole('button', { name: 'Retrying…' })).toBeDisabled();
expect(screen.getByText('Retrying…')).toBeInTheDocument();
});
});
diff --git a/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx b/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx
index bdfa2c6a5..f60926616 100644
--- a/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx
+++ b/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx
@@ -83,7 +83,7 @@ describe('shared dashboard load errors', () => {
expect(onRetry).toHaveBeenCalledOnce();
rerender();
- expect(screen.getByRole('button', { name: 'Retry' })).toBeDisabled();
+ expect(screen.getByRole('button', { name: 'Retrying…' })).toBeDisabled();
});
it('omits the request ID when none is available', () => {
diff --git a/frontend/src/components/ui/error-state.tsx b/frontend/src/components/ui/error-state.tsx
index 6ab72fc62..e29428191 100644
--- a/frontend/src/components/ui/error-state.tsx
+++ b/frontend/src/components/ui/error-state.tsx
@@ -35,7 +35,7 @@ export function ErrorState({
action={
<>
{onRetry && (
-