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-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-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)
|
||||
|
||||
|
|
|
|||
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 psycopg import Connection
|
||||
from psycopg.errors import UniqueViolation
|
||||
from psycopg.errors import RaiseException, UniqueViolation
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from . import validation
|
||||
|
|
@ -296,3 +296,45 @@ def promote_extension_canonical(
|
|||
"SELECT set_extension_status(%s, %s, %s, %s)",
|
||||
(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"
|
||||
domain: infotech
|
||||
repo: target-revenue
|
||||
status: active
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: infotech
|
||||
created: "2026-08-03"
|
||||
|
|
@ -173,7 +173,7 @@ reference plus a live-JSON link when it resolves to this instance
|
|||
|
||||
```task
|
||||
id: TREV-WP-0015-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
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
|
||||
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
|
||||
id: TREV-WP-0015-T06
|
||||
status: done
|
||||
|
|
@ -215,7 +240,7 @@ regression test: `test_pilot_candidate_manifest_carries_real_repo_provenance`.
|
|||
|
||||
```task
|
||||
id: TREV-WP-0015-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
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
|
||||
check before committing any workplan edit, per this project's standing
|
||||
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