feat(workbench): prepare construction evidence endpoint seam (#893)

* feat(workbench): add construction endpoint handler seam

* test(workbench): cover construction endpoint handler seam

* feat(workbench-ui): add construction evidence endpoint helpers

* test(workbench-ui): cover construction endpoint helpers
This commit is contained in:
Shay 2026-06-23 10:11:21 -07:00 committed by GitHub
parent 2c8258fe1a
commit 4c23aa4b00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 202 additions and 0 deletions

View file

@ -0,0 +1,63 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from workbench.construction_endpoint import (
construction_evidence_response,
construction_turn_id_from_path,
)
@dataclass(frozen=True, slots=True)
class _Entry:
turn_id: int
class _Journal:
def __init__(self, entries: dict[int, Any]) -> None:
self._entries = entries
def get_entry(self, turn_id: int) -> Any:
try:
return self._entries[turn_id]
except KeyError:
raise FileNotFoundError(str(turn_id)) from None
def test_construction_turn_id_from_path_matches_only_construction_route() -> None:
assert construction_turn_id_from_path("/trace/7/construction") == "7"
assert construction_turn_id_from_path("/trace/7/pipeline") is None
assert construction_turn_id_from_path("/trace/7") is None
assert construction_turn_id_from_path("/trace//construction") is None
def test_construction_evidence_response_returns_missing_evidence_for_legacy_turn() -> None:
response = construction_evidence_response(_Journal({7: _Entry(turn_id=7)}), "7")
assert response.status == 200
assert response.payload["ok"] is True
data = response.payload["data"]
assert data["schema_version"] == "construction_evidence_v1"
assert data["turn_id"] == 7
assert data["status"] == "missing_evidence"
assert data["diagnostic_only"] is True
assert data["serving_allowed"] is False
def test_construction_evidence_response_returns_404_for_bad_turn_id() -> None:
response = construction_evidence_response(_Journal({}), "not-an-int")
assert response.status == 404
assert response.payload["ok"] is False
assert response.payload["error"]["code"] == "not_found"
assert "not-an-int" in response.payload["error"]["message"]
def test_construction_evidence_response_returns_404_for_missing_turn() -> None:
response = construction_evidence_response(_Journal({}), "8")
assert response.status == 404
assert response.payload["ok"] is False
assert response.payload["error"]["code"] == "not_found"
assert "8" in response.payload["error"]["message"]

View file

@ -0,0 +1,48 @@
import { describe, expect, it, vi } from "vitest";
import type { ConstructionEvidence } from "../../types/constructionEvidence";
import {
TRACE_CONSTRUCTION_EMPTY_LABEL,
TRACE_CONSTRUCTION_ERROR_MUTATION_STATUS,
TRACE_CONSTRUCTION_LOADING_LABEL,
fetchTraceConstructionWith,
traceConstructionPath,
traceConstructionReproducer,
} from "./constructionEvidenceEndpoint";
const evidence: ConstructionEvidence = {
schema_version: "construction_evidence_v1",
turn_id: 7,
status: "missing_evidence",
missing_reason: "construction evidence was not persisted for this turn",
problem_text: null,
proposals: [],
mentions: [],
bindings: [],
bound_relations: [],
contract_assessments: [],
diagnostic_only: true,
serving_allowed: false,
};
describe("construction evidence endpoint helpers", () => {
it("builds the trace construction endpoint path", () => {
expect(traceConstructionPath(7)).toBe("/trace/7/construction");
});
it("fetches construction evidence through an injected fetcher", async () => {
const fetcher = vi.fn(async () => evidence);
await expect(fetchTraceConstructionWith(7, fetcher)).resolves.toBe(evidence);
expect(fetcher).toHaveBeenCalledWith("/trace/7/construction");
});
it("defines loading/empty/error labels for route conformance", () => {
expect(TRACE_CONSTRUCTION_LOADING_LABEL).toBe("Loading construction evidence...");
expect(TRACE_CONSTRUCTION_EMPTY_LABEL).toBe("No construction evidence recorded for this turn.");
expect(TRACE_CONSTRUCTION_ERROR_MUTATION_STATUS).toBe("No trace mutation occurred.");
});
it("builds a copyable reproducer", () => {
expect(traceConstructionReproducer(7)).toBe("curl /trace/7/construction");
});
});

View file

@ -0,0 +1,20 @@
import type { ConstructionEvidence } from "../../types/constructionEvidence";
export function traceConstructionPath(turnId: number): string {
return `/trace/${encodeURIComponent(String(turnId))}/construction`;
}
export async function fetchTraceConstructionWith(
turnId: number,
fetcher: <T>(path: string) => Promise<T>,
): Promise<ConstructionEvidence> {
return fetcher<ConstructionEvidence>(traceConstructionPath(turnId));
}
export const TRACE_CONSTRUCTION_LOADING_LABEL = "Loading construction evidence...";
export const TRACE_CONSTRUCTION_EMPTY_LABEL = "No construction evidence recorded for this turn.";
export const TRACE_CONSTRUCTION_ERROR_MUTATION_STATUS = "No trace mutation occurred.";
export function traceConstructionReproducer(turnId: number): string {
return `curl /trace/${encodeURIComponent(String(turnId))}/construction`;
}

View file

@ -0,0 +1,71 @@
"""Narrow construction-evidence endpoint seam for Workbench.
This helper keeps the endpoint behavior testable without editing the large
`workbench.api` dispatch table from environments that cannot run local syntax and
route tests. The final API wiring is intentionally one line of dispatch:
return construction_evidence_response(self._journal, raw_turn_id)
The helper itself is read-only and projects only persisted construction evidence
through `construction_evidence_from_journal_entry`.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from workbench.construction_evidence import construction_evidence_from_journal_entry
from workbench.schemas import error, ok
@dataclass(frozen=True, slots=True)
class ConstructionEndpointResponse:
status: int
payload: dict[str, Any]
def construction_evidence_response(
journal: Any,
raw_turn_id: str,
) -> ConstructionEndpointResponse:
"""Return JSON-envelope payload for `/trace/<turn_id>/construction`.
The function is deliberately free of parsing/reconstruction side effects. It
reads a journal entry and projects construction evidence when already
persisted; legacy entries receive a typed `missing_evidence` data payload.
"""
try:
turn_id = int(raw_turn_id)
except ValueError:
return ConstructionEndpointResponse(
404,
error("not_found", f"trace construction not found: {raw_turn_id}"),
)
try:
entry = journal.get_entry(turn_id)
except FileNotFoundError:
return ConstructionEndpointResponse(
404,
error("not_found", f"trace construction not found: {turn_id}"),
)
return ConstructionEndpointResponse(
200,
ok(construction_evidence_from_journal_entry(entry)),
)
def construction_turn_id_from_path(path: str) -> str | None:
"""Extract raw turn id from `/trace/<turn_id>/construction`.
Returns None for non-matching paths so the main dispatch table can remain
explicit and order-safe.
"""
if not (path.startswith("/trace/") and path.endswith("/construction")):
return None
raw = path.removeprefix("/trace/").removesuffix("/construction").strip("/")
return raw or None