Complete WP-0015: forgejo_hubs migration + closeout (T05/T07)
migrations/0007_forgejo_hubs.sql: table forgejo_hubs, auto-populated via a BEFORE INSERT trigger on phase_manifests that reads repo_hub/ repo_hub_uri straight out of the manifest JSONB (no top-level columns needed). ON CONFLICT DO NOTHING -- a hub already seen is left alone; correcting a URI is a SECURITY DEFINER governance action (correct_forgejo_hub_uri), not a plain UPDATE, matching every other governance-action pattern in this project. Thin Python wrappers added to registry.py. tests/test_forgejo_hubs.py (6 Docker-gated tests) and tests/test_reference_docs.py (13 tests, no Docker needed -- smoke-tests every real specs/policies/specs/profiles/ file, not just the two exercised incidentally by T03's Control Plane tests). All seven WP-0015 tasks done; workplan marked finished. Final suite: 94 passing offline, 183 passing under the service extras venv. No stray Docker containers left running. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
eaea89e125
commit
de308f8947
6 changed files with 419 additions and 5 deletions
|
|
@ -91,7 +91,7 @@ The concept's §13 now defines a **Global Contingency Share Determination Rule**
|
||||||
| [TREV-WP-0012](workplans/TREV-WP-0012-phase-provenance-and-policy-modeling.md) | Phase provenance, ledger reference, and degeneration-policy modeling — **finished**, all 5 tasks done. Decisions (T02–T04) synthesized into [`specs/PhaseProvenanceSpecAddendum.md`](specs/PhaseProvenanceSpecAddendum.md) (T05) — **not yet accepted for implementation**; that's the document to discuss before any schema/UI work is filed as its own workplan |
|
| [TREV-WP-0012](workplans/TREV-WP-0012-phase-provenance-and-policy-modeling.md) | Phase provenance, ledger reference, and degeneration-policy modeling — **finished**, all 5 tasks done. Decisions (T02–T04) synthesized into [`specs/PhaseProvenanceSpecAddendum.md`](specs/PhaseProvenanceSpecAddendum.md) (T05) — **not yet accepted for implementation**; that's the document to discuss before any schema/UI work is filed as its own workplan |
|
||||||
| [TREV-WP-0013](workplans/TREV-WP-0013-remission-credit-automation.md) | Remission Credit automation (degeneration policy execution) — active; T01–T03 `wait` on WP-0012-T03's policy-spec-file decision. Nothing currently computes or writes `remission-credit` ledger entries |
|
| [TREV-WP-0013](workplans/TREV-WP-0013-remission-credit-automation.md) | Remission Credit automation (degeneration policy execution) — active; T01–T03 `wait` on WP-0012-T03's policy-spec-file decision. Nothing currently computes or writes `remission-credit` ledger entries |
|
||||||
| [TREV-WP-0014](workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md) | Control Plane UI: Extension Registry, Breach Records, Conversion Attestation — active; T01 next. Backend for all three already exists (WP-0006); UI-only work, not blocked on WP-0012 |
|
| [TREV-WP-0014](workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md) | Control Plane UI: Extension Registry, Breach Records, Conversion Attestation — active; T01 next. Backend for all three already exists (WP-0006); UI-only work, not blocked on WP-0012 |
|
||||||
| [TREV-WP-0015](workplans/TREV-WP-0015-phase-provenance-implementation.md) | Implement `specs/PhaseProvenanceSpecAddendum.md` — active; T01–T04, T06 done. T05 (`forgejo_hubs` migration) next |
|
| [TREV-WP-0015](workplans/TREV-WP-0015-phase-provenance-implementation.md) | Implement `specs/PhaseProvenanceSpecAddendum.md` — **finished**, all 7 tasks done. Phase provenance fields, `specs/policies/`/`specs/profiles/` extraction, reference-rendering routes, ledger UI change, and the `forgejo_hubs` registry are all in place |
|
||||||
|
|
||||||
Hub index: [`WORK-RECORDS.md`](WORK-RECORDS.md) · brief: [`.custodian-brief.md`](.custodian-brief.md)
|
Hub index: [`WORK-RECORDS.md`](WORK-RECORDS.md) · brief: [`.custodian-brief.md`](.custodian-brief.md)
|
||||||
|
|
||||||
|
|
|
||||||
84
migrations/0007_forgejo_hubs.sql
Normal file
84
migrations/0007_forgejo_hubs.sql
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
-- WP-0015-T05 (specs/PhaseProvenanceSpecAddendum.md §4): the Forgejo
|
||||||
|
-- hub -> service URI registry.
|
||||||
|
-- Depends on migrations/0001_registries.sql (phase_manifests).
|
||||||
|
--
|
||||||
|
-- Per WP-0012-T02's accepted decision: target-revenue is the generic
|
||||||
|
-- framework, not a specific deployment's list of repos it monetizes, so
|
||||||
|
-- this mapping is hosted Trust Service data (parallel to
|
||||||
|
-- licensor_identities, migrations/0005_licensor_credentials.sql), never
|
||||||
|
-- a file in this git repo. A Phase Manifest stays fully self-describing
|
||||||
|
-- and offline-verifiable regardless -- it already carries
|
||||||
|
-- repo_hub_uri/repo_name directly at registration time
|
||||||
|
-- (specs/PhaseLifecycleUseCases.md use case 9). This table exists purely
|
||||||
|
-- as an admin/repair convenience: if a hub's URI ever changes, there is
|
||||||
|
-- one row to correct rather than every affected Phase Manifest.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS forgejo_hubs (
|
||||||
|
hub_slug text PRIMARY KEY,
|
||||||
|
service_uri text NOT NULL,
|
||||||
|
first_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT ON forgejo_hubs TO trf_app;
|
||||||
|
-- No UPDATE/DELETE grant for trf_app -- correcting a hub's URI after the
|
||||||
|
-- fact is a governance action (correct_forgejo_hub_uri() below), same
|
||||||
|
-- pattern as set_extension_status()/revoke_credential(), not a route
|
||||||
|
-- the application's ordinary write path can take.
|
||||||
|
|
||||||
|
-- Auto-populate on first sight of a repo_hub during Phase registration --
|
||||||
|
-- registry.register_phase_manifest() never needs to know this table
|
||||||
|
-- exists; the trigger reads repo_hub/repo_hub_uri straight out of the
|
||||||
|
-- manifest JSONB it was already given, mirroring
|
||||||
|
-- ensure_licensor_identity()'s auto-create-on-first-INSERT pattern.
|
||||||
|
-- ON CONFLICT DO NOTHING: a hub already seen is left alone here -- URI
|
||||||
|
-- corrections go through correct_forgejo_hub_uri() below, not a silent
|
||||||
|
-- overwrite on the next unrelated Phase registration.
|
||||||
|
CREATE OR REPLACE FUNCTION ensure_forgejo_hub() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_hub_slug text := NEW.manifest #>> '{phase,milestone_release,repo_hub}';
|
||||||
|
v_hub_uri text := NEW.manifest #>> '{phase,milestone_release,repo_hub_uri}';
|
||||||
|
BEGIN
|
||||||
|
IF v_hub_slug IS NOT NULL AND v_hub_uri IS NOT NULL THEN
|
||||||
|
INSERT INTO forgejo_hubs (hub_slug, service_uri)
|
||||||
|
VALUES (v_hub_slug, v_hub_uri)
|
||||||
|
ON CONFLICT (hub_slug) DO NOTHING;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS phase_manifests_ensure_forgejo_hub ON phase_manifests;
|
||||||
|
CREATE TRIGGER phase_manifests_ensure_forgejo_hub
|
||||||
|
BEFORE INSERT ON phase_manifests
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION ensure_forgejo_hub();
|
||||||
|
|
||||||
|
-- Correcting a hub's URI after the fact (a domain move, etc.) is a
|
||||||
|
-- recorded governance action, not an ordinary UPDATE -- trf_app has no
|
||||||
|
-- UPDATE grant on forgejo_hubs at all.
|
||||||
|
CREATE OR REPLACE FUNCTION correct_forgejo_hub_uri(
|
||||||
|
p_hub_slug text,
|
||||||
|
p_new_uri text
|
||||||
|
) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE forgejo_hubs
|
||||||
|
SET service_uri = p_new_uri,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE hub_slug = p_hub_slug;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'unknown forgejo hub: %', p_hub_slug;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION correct_forgejo_hub_uri(text, text) TO trf_app;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -29,7 +29,7 @@ from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from psycopg import Connection
|
from psycopg import Connection
|
||||||
from psycopg.errors import UniqueViolation
|
from psycopg.errors import RaiseException, UniqueViolation
|
||||||
from psycopg.types.json import Jsonb
|
from psycopg.types.json import Jsonb
|
||||||
|
|
||||||
from . import validation
|
from . import validation
|
||||||
|
|
@ -296,3 +296,45 @@ def promote_extension_canonical(
|
||||||
"SELECT set_extension_status(%s, %s, %s, %s)",
|
"SELECT set_extension_status(%s, %s, %s, %s)",
|
||||||
(extension_id, version, "canonical", approved_by),
|
(extension_id, version, "canonical", approved_by),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_forgejo_hub(conn: Connection, hub_slug: str) -> dict[str, Any] | None:
|
||||||
|
"""Look up a hub's currently recorded service URI (WP-0015-T05).
|
||||||
|
|
||||||
|
Purely an admin/repair convenience — a Phase Manifest never needs
|
||||||
|
this to be reachable to be independently verified, since it already
|
||||||
|
carries `repo_hub_uri`/`repo_name` directly at registration time
|
||||||
|
(`specs/PhaseLifecycleUseCases.md` use case 9). Rows are created
|
||||||
|
automatically by `phase_manifests_ensure_forgejo_hub`
|
||||||
|
(`migrations/0007_forgejo_hubs.sql`) the first time a hub is seen —
|
||||||
|
there is no explicit "register a hub" function to pair with this one.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT hub_slug, service_uri, first_seen_at, updated_at "
|
||||||
|
"FROM forgejo_hubs WHERE hub_slug = %s",
|
||||||
|
(hub_slug,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
hub_slug_, service_uri, first_seen_at, updated_at = row
|
||||||
|
return {
|
||||||
|
"hub_slug": hub_slug_,
|
||||||
|
"service_uri": service_uri,
|
||||||
|
"first_seen_at": first_seen_at,
|
||||||
|
"updated_at": updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def correct_forgejo_hub_uri(conn: Connection, hub_slug: str, new_uri: str) -> None:
|
||||||
|
"""Correct a hub's recorded service URI (e.g. after a domain move).
|
||||||
|
|
||||||
|
Calls the database's `correct_forgejo_hub_uri` function rather than
|
||||||
|
an UPDATE — the application role has no UPDATE grant on
|
||||||
|
`forgejo_hubs` at all, matching every other governance-action pattern
|
||||||
|
in this project (`revoke_sub_credential`,
|
||||||
|
`promote_extension_canonical`).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn.execute("SELECT correct_forgejo_hub_uri(%s, %s)", (hub_slug, new_uri))
|
||||||
|
except RaiseException as exc:
|
||||||
|
raise RegistrationError(f"unknown forgejo hub: {hub_slug!r}") from exc
|
||||||
|
|
|
||||||
177
tests/test_forgejo_hubs.py
Normal file
177
tests/test_forgejo_hubs.py
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
"""Integration tests for WP-0015-T05 (`forgejo_hubs` registry).
|
||||||
|
|
||||||
|
Same ephemeral, disposable Postgres-via-Docker pattern as the other
|
||||||
|
hosted test modules (never the shared state-hub instance).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
psycopg = pytest.importorskip("psycopg")
|
||||||
|
|
||||||
|
from conftest import golden_manifest # noqa: E402
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
MIGRATIONS = [
|
||||||
|
REPO_ROOT / "migrations" / "0001_registries.sql",
|
||||||
|
REPO_ROOT / "migrations" / "0002_ledger.sql",
|
||||||
|
REPO_ROOT / "migrations" / "0003_attestations.sql",
|
||||||
|
REPO_ROOT / "migrations" / "0004_breach_records.sql",
|
||||||
|
REPO_ROOT / "migrations" / "0005_licensor_credentials.sql",
|
||||||
|
REPO_ROOT / "migrations" / "0006_control_plane.sql",
|
||||||
|
REPO_ROOT / "migrations" / "0007_forgejo_hubs.sql",
|
||||||
|
]
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
shutil.which("docker") is None, reason="docker not available"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def pg_container():
|
||||||
|
name = f"trf-test-pg-hubs-{uuid.uuid4().hex[:8]}"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"docker", "run", "--rm", "-d",
|
||||||
|
"--name", name,
|
||||||
|
"-e", "POSTGRES_PASSWORD=postgres",
|
||||||
|
"-e", "POSTGRES_DB=target_revenue_test",
|
||||||
|
"-p", "127.0.0.1::5432",
|
||||||
|
"postgres:16-alpine",
|
||||||
|
],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
port_out = subprocess.run(
|
||||||
|
["docker", "port", name, "5432/tcp"], check=True, capture_output=True, text=True
|
||||||
|
).stdout.strip()
|
||||||
|
host_port = port_out.split(":")[-1]
|
||||||
|
dsn = f"host=127.0.0.1 port={host_port} dbname=target_revenue_test user=postgres password=postgres"
|
||||||
|
|
||||||
|
for _ in range(60):
|
||||||
|
try:
|
||||||
|
with psycopg.connect(dsn, connect_timeout=1):
|
||||||
|
break
|
||||||
|
except psycopg.OperationalError:
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("postgres container did not become ready in time")
|
||||||
|
|
||||||
|
with psycopg.connect(dsn) as conn:
|
||||||
|
for migration in MIGRATIONS:
|
||||||
|
conn.execute(migration.read_text(encoding="utf-8"))
|
||||||
|
conn.commit()
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO licensors (token, licensor_id, credential_label, rights, issued_by) "
|
||||||
|
"VALUES (%s, %s, %s, %s, %s)",
|
||||||
|
("founding-token", "binky", "founder", "operator", "bootstrap"),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
app_dsn = (
|
||||||
|
f"host=127.0.0.1 port={host_port} dbname=target_revenue_test "
|
||||||
|
f"user=trf_app password=changeme-in-deployment"
|
||||||
|
)
|
||||||
|
yield {"admin_dsn": dsn, "app_dsn": app_dsn, "founding_token": "founding-token"}
|
||||||
|
finally:
|
||||||
|
subprocess.run(["docker", "stop", name], capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def conn(pg_container):
|
||||||
|
with psycopg.connect(pg_container["app_dsn"]) as connection:
|
||||||
|
yield connection
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def licensor(conn, pg_container):
|
||||||
|
from target_revenue import registry
|
||||||
|
|
||||||
|
return registry.authenticate(conn, pg_container["founding_token"])
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_with_hub(hub_slug: str, hub_uri: str, phase_suffix: str) -> dict:
|
||||||
|
manifest = copy.deepcopy(golden_manifest())
|
||||||
|
manifest["phase"]["id"] = manifest["phase"]["id"] + "-hubtest-" + phase_suffix
|
||||||
|
manifest["phase"]["milestone_release"]["repo_hub"] = hub_slug
|
||||||
|
manifest["phase"]["milestone_release"]["repo_hub_uri"] = hub_uri
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def test_registering_phase_auto_creates_hub_row(conn, licensor):
|
||||||
|
from target_revenue import registry
|
||||||
|
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
manifest = _manifest_with_hub(f"hub-{suffix}", "https://forgejo.example.invalid", suffix)
|
||||||
|
registry.register_phase_manifest(conn, licensor, manifest)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
hub = registry.get_forgejo_hub(conn, f"hub-{suffix}")
|
||||||
|
assert hub is not None
|
||||||
|
assert hub["service_uri"] == "https://forgejo.example.invalid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_hub_returns_none(conn):
|
||||||
|
from target_revenue import registry
|
||||||
|
|
||||||
|
assert registry.get_forgejo_hub(conn, "no-such-hub") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_phase_same_hub_does_not_overwrite(conn, licensor):
|
||||||
|
"""ON CONFLICT DO NOTHING: a hub already seen is left alone by the
|
||||||
|
auto-populate trigger -- only correct_forgejo_hub_uri() may change it."""
|
||||||
|
from target_revenue import registry
|
||||||
|
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
hub_slug = f"hub-{suffix}"
|
||||||
|
manifest_a = _manifest_with_hub(hub_slug, "https://first-seen.example.invalid", suffix + "a")
|
||||||
|
registry.register_phase_manifest(conn, licensor, manifest_a)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
manifest_b = _manifest_with_hub(hub_slug, "https://different-uri.example.invalid", suffix + "b")
|
||||||
|
registry.register_phase_manifest(conn, licensor, manifest_b)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
hub = registry.get_forgejo_hub(conn, hub_slug)
|
||||||
|
assert hub["service_uri"] == "https://first-seen.example.invalid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_correct_forgejo_hub_uri_updates_existing_hub(conn, licensor):
|
||||||
|
from target_revenue import registry
|
||||||
|
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
hub_slug = f"hub-{suffix}"
|
||||||
|
manifest = _manifest_with_hub(hub_slug, "https://old-domain.example.invalid", suffix)
|
||||||
|
registry.register_phase_manifest(conn, licensor, manifest)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
registry.correct_forgejo_hub_uri(conn, hub_slug, "https://new-domain.example.invalid")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
hub = registry.get_forgejo_hub(conn, hub_slug)
|
||||||
|
assert hub["service_uri"] == "https://new-domain.example.invalid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_correct_forgejo_hub_uri_unknown_hub_is_rejected(conn):
|
||||||
|
from target_revenue import registry
|
||||||
|
|
||||||
|
with pytest.raises(registry.RegistrationError, match="unknown forgejo hub"):
|
||||||
|
registry.correct_forgejo_hub_uri(conn, "never-registered", "https://x.example.invalid")
|
||||||
|
|
||||||
|
|
||||||
|
def test_application_role_cannot_update_forgejo_hubs_directly(pg_container):
|
||||||
|
with psycopg.connect(pg_container["app_dsn"]) as app_conn:
|
||||||
|
with pytest.raises(psycopg.errors.InsufficientPrivilege):
|
||||||
|
app_conn.execute(
|
||||||
|
"UPDATE forgejo_hubs SET service_uri = 'https://tampered.invalid' "
|
||||||
|
"WHERE hub_slug = 'anything'"
|
||||||
|
)
|
||||||
|
app_conn.rollback()
|
||||||
71
tests/test_reference_docs.py
Normal file
71
tests/test_reference_docs.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
"""Smoke tests for WP-0015-T07: every real `specs/policies/`/
|
||||||
|
`specs/profiles/` file must actually render without error. No database
|
||||||
|
or Docker required — this only exercises `reference_docs.py`'s pure
|
||||||
|
file-rendering logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from conftest import REPO_ROOT
|
||||||
|
|
||||||
|
pytest.importorskip("markdown")
|
||||||
|
pytest.importorskip("yaml")
|
||||||
|
|
||||||
|
from target_revenue.service import reference_docs # noqa: E402
|
||||||
|
|
||||||
|
POLICY_SLUGS = ["linear-longstop-v0"]
|
||||||
|
PROFILE_SLUGS = [
|
||||||
|
"development-license",
|
||||||
|
"cost-plus-operations",
|
||||||
|
"phase-sponsorship",
|
||||||
|
"service-with-development-allocation",
|
||||||
|
"product-ideation",
|
||||||
|
"general-consulting",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_real_policy_files_exist_on_disk():
|
||||||
|
for slug in POLICY_SLUGS:
|
||||||
|
assert (REPO_ROOT / "specs" / "policies" / f"{slug}.md").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_real_profile_files_exist_on_disk():
|
||||||
|
for slug in PROFILE_SLUGS:
|
||||||
|
assert (REPO_ROOT / "specs" / "profiles" / f"{slug}.md").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("slug", POLICY_SLUGS)
|
||||||
|
def test_policy_doc_renders_without_error(slug):
|
||||||
|
result = reference_docs.load_reference_doc("policies", slug)
|
||||||
|
assert result is not None
|
||||||
|
html, frontmatter = result
|
||||||
|
assert "<h1>" in html or "<h2>" in html
|
||||||
|
assert "policy_id" in frontmatter
|
||||||
|
assert frontmatter["policy_id"].startswith("trsl:policy:")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("slug", PROFILE_SLUGS)
|
||||||
|
def test_profile_doc_renders_without_error(slug):
|
||||||
|
result = reference_docs.load_reference_doc("profiles", slug)
|
||||||
|
assert result is not None
|
||||||
|
html, frontmatter = result
|
||||||
|
assert "<h1>" in html
|
||||||
|
assert "extension_id" in frontmatter
|
||||||
|
assert frontmatter["extension_id"].startswith("trsl:extension:")
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_kind_returns_none():
|
||||||
|
assert reference_docs.load_reference_doc("calculators", "anything") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_slug_returns_none():
|
||||||
|
assert reference_docs.load_reference_doc("policies", "does-not-exist") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_policy_slug_from_id():
|
||||||
|
assert reference_docs.policy_slug_from_id("trsl:policy:linear-longstop-v0@1.0") == "linear-longstop-v0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_extension_slug_from_id():
|
||||||
|
assert reference_docs.extension_slug_from_id("trsl:extension:development-license@1.0") == "development-license"
|
||||||
|
|
@ -4,7 +4,7 @@ type: workplan
|
||||||
title: "Implement Phase provenance, spec-file, and ledger UI changes"
|
title: "Implement Phase provenance, spec-file, and ledger UI changes"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: target-revenue
|
repo: target-revenue
|
||||||
status: active
|
status: finished
|
||||||
owner: claude
|
owner: claude
|
||||||
topic_slug: infotech
|
topic_slug: infotech
|
||||||
created: "2026-08-03"
|
created: "2026-08-03"
|
||||||
|
|
@ -173,7 +173,7 @@ reference plus a live-JSON link when it resolves to this instance
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TREV-WP-0015-T05
|
id: TREV-WP-0015-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "816f1ac2-da40-4550-8d6c-64a1a335a660"
|
state_hub_task_id: "816f1ac2-da40-4550-8d6c-64a1a335a660"
|
||||||
```
|
```
|
||||||
|
|
@ -188,6 +188,31 @@ implementation (not a T02-style human gate, per the addendum) whether
|
||||||
correcting a hub's URI after the fact is a `SECURITY DEFINER` governance
|
correcting a hub's URI after the fact is a `SECURITY DEFINER` governance
|
||||||
action or a plain `UPDATE`, and document whichever is chosen.
|
action or a plain `UPDATE`, and document whichever is chosen.
|
||||||
|
|
||||||
|
**Result:** `migrations/0007_forgejo_hubs.sql` — the `BEFORE INSERT`
|
||||||
|
trigger on `phase_manifests` reads `repo_hub`/`repo_hub_uri` straight out
|
||||||
|
of the manifest JSONB (`NEW.manifest #>> '{phase,milestone_release,
|
||||||
|
repo_hub}'`) rather than needing those as top-level table columns, since
|
||||||
|
`phase_manifests` stores the whole manifest as one `jsonb` blob.
|
||||||
|
`ON CONFLICT (hub_slug) DO NOTHING` — a hub already seen is left alone by
|
||||||
|
the trigger; only a second registration under the same slug with a
|
||||||
|
different URI would otherwise silently overwrite it, which is exactly
|
||||||
|
the failure mode this table exists to prevent.
|
||||||
|
|
||||||
|
**Decided: `SECURITY DEFINER` governance action**, not a plain `UPDATE`
|
||||||
|
— `correct_forgejo_hub_uri(hub_slug, new_uri)`, matching
|
||||||
|
`set_extension_status()`/`revoke_credential()`. `trf_app` has no UPDATE
|
||||||
|
grant on `forgejo_hubs` at all. Thin Python wrappers added to
|
||||||
|
`registry.py`: `get_forgejo_hub()`, `correct_forgejo_hub_uri()` (raises
|
||||||
|
`RegistrationError` for an unknown slug, via `psycopg.errors.
|
||||||
|
RaiseException`).
|
||||||
|
|
||||||
|
New `tests/test_forgejo_hubs.py`, 6 Docker-gated tests: auto-creation on
|
||||||
|
first registration, unknown-hub lookup, second-registration-same-hub
|
||||||
|
does *not* overwrite, explicit correction does, correction of an unknown
|
||||||
|
hub is rejected, and DB-level UPDATE rejection for the application role.
|
||||||
|
Full suite: 94 passing offline (unchanged), 170 passing with Docker (up
|
||||||
|
from 164).
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TREV-WP-0015-T06
|
id: TREV-WP-0015-T06
|
||||||
status: done
|
status: done
|
||||||
|
|
@ -215,7 +240,7 @@ regression test: `test_pilot_candidate_manifest_carries_real_repo_provenance`.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TREV-WP-0015-T07
|
id: TREV-WP-0015-T07
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "39b34438-a1b1-49da-a88a-f3bec926aa89"
|
state_hub_task_id: "39b34438-a1b1-49da-a88a-f3bec926aa89"
|
||||||
```
|
```
|
||||||
|
|
@ -228,3 +253,18 @@ without error. Update `README.md`'s WP-0015 row and this workplan's
|
||||||
Result sections; run the full offline + Docker-gated suite; fence-count
|
Result sections; run the full offline + Docker-gated suite; fence-count
|
||||||
check before committing any workplan edit, per this project's standing
|
check before committing any workplan edit, per this project's standing
|
||||||
practice.
|
practice.
|
||||||
|
|
||||||
|
**Result:** New `tests/test_reference_docs.py` — 13 tests, no Docker
|
||||||
|
required (only needs the `service` extras venv for `markdown`/`PyYAML`),
|
||||||
|
covering every real policy/profile file on disk, not just the two
|
||||||
|
exercised incidentally by T03's Control Plane tests. Skips cleanly under
|
||||||
|
plain `python3` (offline suite) when those extras aren't installed,
|
||||||
|
matching this project's existing `pytest.importorskip` convention.
|
||||||
|
|
||||||
|
All seven WP-0015 tasks are now done. Final suite counts: **94 passing
|
||||||
|
offline** (up from 84 at the start of this workplan — T01's schema tests
|
||||||
|
and T06's backfill-regression test, unchanged since; nothing in T02–T07
|
||||||
|
touches an offline-only code path), **183 passing under `.venv` with the
|
||||||
|
`service` extras** (the Docker-gated total moved from 146 → 170 across
|
||||||
|
T01–T05, plus 13 more non-Docker service-extras tests from this task). No
|
||||||
|
stray Docker containers left running.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue