generated from coulomb/repo-seed
Milestone 6 API completeness
This commit is contained in:
32
README.md
32
README.md
@@ -69,6 +69,7 @@ Inspect recorded facts:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:8000/repos/1/analysis-runs
|
curl http://127.0.0.1:8000/repos/1/analysis-runs
|
||||||
|
curl http://127.0.0.1:8000/repos/1/analysis-runs/1
|
||||||
curl http://127.0.0.1:8000/repos/1/observed-facts
|
curl http://127.0.0.1:8000/repos/1/observed-facts
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -91,3 +92,34 @@ curl -X POST http://127.0.0.1:8000/repos/1/analysis-runs/1/candidate-graph/appro
|
|||||||
```
|
```
|
||||||
|
|
||||||
Approval copies candidate abilities, capabilities, features, and evidence into the approved registry tables, marks candidates approved, and moves the repository status to `indexed`.
|
Approval copies candidate abilities, capabilities, features, and evidence into the approved registry tables, marks candidates approved, and moves the repository status to `indexed`.
|
||||||
|
|
||||||
|
## Review Workflow
|
||||||
|
|
||||||
|
Candidate graphs are meant to be corrected before publication. The API supports:
|
||||||
|
|
||||||
|
- edit candidate abilities and capabilities with `PATCH`
|
||||||
|
- reject candidate abilities, capabilities, features, and evidence
|
||||||
|
- relink capabilities under another ability
|
||||||
|
- relink features or evidence under another capability
|
||||||
|
- merge duplicate abilities, capabilities, features, or evidence
|
||||||
|
|
||||||
|
Examples are available in the generated OpenAPI docs at `/docs`.
|
||||||
|
|
||||||
|
## Agent-Facing Endpoints
|
||||||
|
|
||||||
|
The v0.1 API covers the main registration, analysis, review, search, and inspection loop:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /repos
|
||||||
|
POST /repos
|
||||||
|
GET /repos/{id}
|
||||||
|
POST /repos/{id}/analysis-runs
|
||||||
|
GET /repos/{id}/analysis-runs
|
||||||
|
GET /repos/{id}/analysis-runs/{run_id}
|
||||||
|
GET /repos/{id}/analysis-runs/{run_id}/candidate-graph
|
||||||
|
POST /repos/{id}/analysis-runs/{run_id}/candidate-graph/approve
|
||||||
|
GET /repos/{id}/ability-map
|
||||||
|
GET /abilities
|
||||||
|
GET /capabilities
|
||||||
|
GET /search?q=...
|
||||||
|
```
|
||||||
|
|||||||
@@ -168,3 +168,25 @@ class SearchResult:
|
|||||||
match_type: str
|
match_type: str
|
||||||
match_name: str
|
match_name: str
|
||||||
confidence: float
|
confidence: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AbilitySummary:
|
||||||
|
id: int
|
||||||
|
repository_id: int
|
||||||
|
repository_name: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
confidence: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CapabilitySummary:
|
||||||
|
id: int
|
||||||
|
repository_id: int
|
||||||
|
repository_name: str
|
||||||
|
ability_id: int
|
||||||
|
ability_name: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
confidence: float
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ from __future__ import annotations
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from repo_registry.core.models import (
|
from repo_registry.core.models import (
|
||||||
|
AbilitySummary,
|
||||||
AnalysisRun,
|
AnalysisRun,
|
||||||
|
CapabilitySummary,
|
||||||
CandidateGraph,
|
CandidateGraph,
|
||||||
ObservedFact,
|
ObservedFact,
|
||||||
Repository,
|
Repository,
|
||||||
@@ -101,6 +103,15 @@ class RegistryService:
|
|||||||
def list_analysis_runs(self, repository_id: int) -> list[AnalysisRun]:
|
def list_analysis_runs(self, repository_id: int) -> list[AnalysisRun]:
|
||||||
return self.store.list_analysis_runs(repository_id)
|
return self.store.list_analysis_runs(repository_id)
|
||||||
|
|
||||||
|
def get_analysis_run(self, repository_id: int, analysis_run_id: int) -> AnalysisRun:
|
||||||
|
return self.store.get_analysis_run(repository_id, analysis_run_id)
|
||||||
|
|
||||||
|
def list_abilities(self) -> list[AbilitySummary]:
|
||||||
|
return self.store.list_abilities()
|
||||||
|
|
||||||
|
def list_capabilities(self) -> list[CapabilitySummary]:
|
||||||
|
return self.store.list_capabilities()
|
||||||
|
|
||||||
def list_observed_facts(
|
def list_observed_facts(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from repo_registry.core.models import (
|
from repo_registry.core.models import (
|
||||||
Ability,
|
Ability,
|
||||||
|
AbilitySummary,
|
||||||
AnalysisRun,
|
AnalysisRun,
|
||||||
CandidateAbility,
|
CandidateAbility,
|
||||||
CandidateCapability,
|
CandidateCapability,
|
||||||
@@ -13,6 +14,7 @@ from repo_registry.core.models import (
|
|||||||
CandidateFeature,
|
CandidateFeature,
|
||||||
CandidateGraph,
|
CandidateGraph,
|
||||||
Capability,
|
Capability,
|
||||||
|
CapabilitySummary,
|
||||||
Evidence,
|
Evidence,
|
||||||
Feature,
|
Feature,
|
||||||
ObservedFact,
|
ObservedFact,
|
||||||
@@ -975,6 +977,56 @@ class RegistryStore:
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
return [self._analysis_run_from_row(row) for row in rows]
|
return [self._analysis_run_from_row(row) for row in rows]
|
||||||
|
|
||||||
|
def list_abilities(self) -> list[AbilitySummary]:
|
||||||
|
with self.connect() as connection:
|
||||||
|
rows = connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT a.id, a.repository_id, r.name AS repository_name,
|
||||||
|
a.name, a.description, a.confidence
|
||||||
|
FROM approved_abilities a
|
||||||
|
JOIN repositories r ON r.id = a.repository_id
|
||||||
|
ORDER BY r.name ASC, a.name ASC, a.id ASC
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
return [
|
||||||
|
AbilitySummary(
|
||||||
|
id=row["id"],
|
||||||
|
repository_id=row["repository_id"],
|
||||||
|
repository_name=row["repository_name"],
|
||||||
|
name=row["name"],
|
||||||
|
description=row["description"],
|
||||||
|
confidence=row["confidence"],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
def list_capabilities(self) -> list[CapabilitySummary]:
|
||||||
|
with self.connect() as connection:
|
||||||
|
rows = connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT c.id, c.repository_id, r.name AS repository_name,
|
||||||
|
c.ability_id, a.name AS ability_name,
|
||||||
|
c.name, c.description, c.confidence
|
||||||
|
FROM approved_capabilities c
|
||||||
|
JOIN approved_abilities a ON a.id = c.ability_id
|
||||||
|
JOIN repositories r ON r.id = c.repository_id
|
||||||
|
ORDER BY r.name ASC, a.name ASC, c.name ASC, c.id ASC
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
return [
|
||||||
|
CapabilitySummary(
|
||||||
|
id=row["id"],
|
||||||
|
repository_id=row["repository_id"],
|
||||||
|
repository_name=row["repository_name"],
|
||||||
|
ability_id=row["ability_id"],
|
||||||
|
ability_name=row["ability_name"],
|
||||||
|
name=row["name"],
|
||||||
|
description=row["description"],
|
||||||
|
confidence=row["confidence"],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
def get_snapshot(self, snapshot_id: int) -> RepositorySnapshot:
|
def get_snapshot(self, snapshot_id: int) -> RepositorySnapshot:
|
||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
row = connection.execute(
|
row = connection.execute(
|
||||||
|
|||||||
@@ -34,12 +34,37 @@ class RepositoryCreate(BaseModel):
|
|||||||
description: str | None = None
|
description: str | None = None
|
||||||
branch: str = "main"
|
branch: str = "main"
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/example/repository.git",
|
||||||
|
"name": "Example Repository",
|
||||||
|
"description": "Optional human-readable repository summary.",
|
||||||
|
"branch": "main",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AbilityCreate(BaseModel):
|
class AbilityCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Business Email Routing",
|
||||||
|
"description": "Route inbound messages to the right team.",
|
||||||
|
"confidence": 0.92,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CapabilityCreate(BaseModel):
|
class CapabilityCreate(BaseModel):
|
||||||
ability_id: int
|
ability_id: int
|
||||||
@@ -49,6 +74,21 @@ class CapabilityCreate(BaseModel):
|
|||||||
outputs: list[str] = Field(default_factory=list)
|
outputs: list[str] = Field(default_factory=list)
|
||||||
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"ability_id": 1,
|
||||||
|
"name": "Classify Incoming Email",
|
||||||
|
"description": "Classify messages by intent.",
|
||||||
|
"inputs": ["subject", "body"],
|
||||||
|
"outputs": ["intent", "confidence"],
|
||||||
|
"confidence": 0.88,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FeatureCreate(BaseModel):
|
class FeatureCreate(BaseModel):
|
||||||
capability_id: int
|
capability_id: int
|
||||||
@@ -57,6 +97,20 @@ class FeatureCreate(BaseModel):
|
|||||||
location: str = ""
|
location: str = ""
|
||||||
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"capability_id": 1,
|
||||||
|
"name": "POST /api/classify-email",
|
||||||
|
"type": "REST endpoint",
|
||||||
|
"location": "src/routes/classify_email.py",
|
||||||
|
"confidence": 0.84,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EvidenceCreate(BaseModel):
|
class EvidenceCreate(BaseModel):
|
||||||
capability_id: int
|
capability_id: int
|
||||||
@@ -64,18 +118,52 @@ class EvidenceCreate(BaseModel):
|
|||||||
reference: str
|
reference: str
|
||||||
strength: str = "medium"
|
strength: str = "medium"
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"capability_id": 1,
|
||||||
|
"type": "unit_test",
|
||||||
|
"reference": "tests/test_email_classification.py",
|
||||||
|
"strength": "strong",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AnalysisRunCreate(BaseModel):
|
class AnalysisRunCreate(BaseModel):
|
||||||
source_path: str | None = None
|
source_path: str | None = None
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{},
|
||||||
|
{"source_path": "/path/to/local/repository"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateGraphApproval(BaseModel):
|
class CandidateGraphApproval(BaseModel):
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [{"notes": "Approved after curator review."}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateRejection(BaseModel):
|
class CandidateRejection(BaseModel):
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [{"notes": "Rejected because the claim is too generic."}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateEdit(BaseModel):
|
class CandidateEdit(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
@@ -83,36 +171,96 @@ class CandidateEdit(BaseModel):
|
|||||||
confidence: float = Field(default=0.5, ge=0.0, le=1.0)
|
confidence: float = Field(default=0.5, ge=0.0, le=1.0)
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Service Health Monitoring",
|
||||||
|
"description": "Expose health state for operational checks.",
|
||||||
|
"confidence": 0.9,
|
||||||
|
"notes": "Renamed from generated review seed.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateCapabilityRelink(BaseModel):
|
class CandidateCapabilityRelink(BaseModel):
|
||||||
target_ability_id: int
|
target_ability_id: int
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{"target_ability_id": 2, "notes": "Move under operational ability."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateLeafRelink(BaseModel):
|
class CandidateLeafRelink(BaseModel):
|
||||||
target_capability_id: int
|
target_capability_id: int
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"target_capability_id": 3,
|
||||||
|
"notes": "Evidence supports a different capability.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateAbilityMerge(BaseModel):
|
class CandidateAbilityMerge(BaseModel):
|
||||||
target_ability_id: int
|
target_ability_id: int
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{"target_ability_id": 2, "notes": "Duplicate ability wording."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateCapabilityMerge(BaseModel):
|
class CandidateCapabilityMerge(BaseModel):
|
||||||
target_capability_id: int
|
target_capability_id: int
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{"target_capability_id": 3, "notes": "Duplicate capability."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateFeatureMerge(BaseModel):
|
class CandidateFeatureMerge(BaseModel):
|
||||||
target_feature_id: int
|
target_feature_id: int
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [{"target_feature_id": 4, "notes": "Duplicate route."}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CandidateEvidenceMerge(BaseModel):
|
class CandidateEvidenceMerge(BaseModel):
|
||||||
target_evidence_id: int
|
target_evidence_id: int
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [{"target_evidence_id": 5, "notes": "Duplicate evidence."}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="Repository Ability Registry", version="0.1.0")
|
app = FastAPI(title="Repository Ability Registry", version="0.1.0")
|
||||||
|
|
||||||
@@ -184,6 +332,18 @@ def list_analysis_runs(
|
|||||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}")
|
||||||
|
def get_analysis_run(
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> dict[str, object]:
|
||||||
|
try:
|
||||||
|
return asdict(service.get_analysis_run(repository_id, analysis_run_id))
|
||||||
|
except NotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@app.get("/repos/{repository_id}/observed-facts")
|
@app.get("/repos/{repository_id}/observed-facts")
|
||||||
def list_observed_facts(
|
def list_observed_facts(
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -611,3 +771,17 @@ def search(
|
|||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> list[dict[str, object]]:
|
) -> list[dict[str, object]]:
|
||||||
return [asdict(result) for result in service.search(q)]
|
return [asdict(result) for result in service.search(q)]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/abilities")
|
||||||
|
def list_abilities(
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
return [asdict(ability) for ability in service.list_abilities()]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/capabilities")
|
||||||
|
def list_capabilities(
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
return [asdict(capability) for capability in service.list_capabilities()]
|
||||||
|
|||||||
@@ -87,6 +87,14 @@ def test_search_matches_approved_abilities_and_capabilities(tmp_path):
|
|||||||
assert results[0].match_type == "capability"
|
assert results[0].match_type == "capability"
|
||||||
assert results[0].match_name == "Classify Incoming Email"
|
assert results[0].match_name == "Classify Incoming Email"
|
||||||
|
|
||||||
|
abilities = service.list_abilities()
|
||||||
|
capabilities = service.list_capabilities()
|
||||||
|
|
||||||
|
assert abilities[0].repository_name == "MailRouter"
|
||||||
|
assert abilities[0].name == "Business Email Routing"
|
||||||
|
assert capabilities[0].ability_name == "Business Email Routing"
|
||||||
|
assert capabilities[0].name == "Classify Incoming Email"
|
||||||
|
|
||||||
|
|
||||||
def test_register_repository_imports_metadata_when_name_is_omitted(tmp_path):
|
def test_register_repository_imports_metadata_when_name_is_omitted(tmp_path):
|
||||||
source = tmp_path / "metadata-source"
|
source = tmp_path / "metadata-source"
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ def test_api_analysis_run_loop(tmp_path):
|
|||||||
assert run["analysis_run"]["status"] == "completed"
|
assert run["analysis_run"]["status"] == "completed"
|
||||||
assert run["snapshot"]["file_count"] == 2
|
assert run["snapshot"]["file_count"] == 2
|
||||||
|
|
||||||
|
get_run_response = client.get(
|
||||||
|
f"/repos/{repository_id}/analysis-runs/{run['analysis_run']['id']}"
|
||||||
|
)
|
||||||
|
assert get_run_response.status_code == 200
|
||||||
|
assert get_run_response.json()["id"] == run["analysis_run"]["id"]
|
||||||
|
|
||||||
candidate_response = client.get(
|
candidate_response = client.get(
|
||||||
f"/repos/{repository_id}/analysis-runs/"
|
f"/repos/{repository_id}/analysis-runs/"
|
||||||
f"{run['analysis_run']['id']}/candidate-graph"
|
f"{run['analysis_run']['id']}/candidate-graph"
|
||||||
@@ -206,6 +212,16 @@ def test_api_analysis_run_loop(tmp_path):
|
|||||||
assert search_response.status_code == 200
|
assert search_response.status_code == 200
|
||||||
assert search_response.json()
|
assert search_response.json()
|
||||||
|
|
||||||
|
abilities_response = client.get("/abilities")
|
||||||
|
assert abilities_response.status_code == 200
|
||||||
|
assert abilities_response.json()[0]["name"] == "Frontend Delivery"
|
||||||
|
assert abilities_response.json()[0]["repository_name"] == "Frontend"
|
||||||
|
|
||||||
|
capabilities_response = client.get("/capabilities")
|
||||||
|
assert capabilities_response.status_code == 200
|
||||||
|
assert capabilities_response.json()[0]["name"] == "Describe Frontend Stack"
|
||||||
|
assert capabilities_response.json()[0]["ability_name"] == "Frontend Delivery"
|
||||||
|
|
||||||
facts_response = client.get(f"/repos/{repository_id}/observed-facts")
|
facts_response = client.get(f"/repos/{repository_id}/observed-facts")
|
||||||
assert facts_response.status_code == 200
|
assert facts_response.status_code == 200
|
||||||
fact_names = {
|
fact_names = {
|
||||||
|
|||||||
Reference in New Issue
Block a user