33
44import base64
55import shutil
6+ import time
67from pathlib import Path
78
89from fastapi import APIRouter , HTTPException
1516 parse_frontmatter ,
1617 serialize_frontmatter ,
1718)
18- from ralphify .checks import discover_checks
19+ from ralphify .checks import discover_checks , run_check
1920from ralphify .contexts import discover_contexts
2021from ralphify .instructions import discover_instructions
2122from ralphify .prompts import discover_prompts
22- from ralphify .ui .models import PrimitiveResponse , PrimitiveUpdate
23+ from ralphify .ui .models import CheckTestResponse , PrimitiveResponse , PrimitiveUpdate
2324
2425router = APIRouter ()
2526
@@ -51,24 +52,35 @@ def _resolve_kind(kind: str) -> tuple:
5152 raise HTTPException (status_code = 400 , detail = f"Unknown kind: { kind } " )
5253
5354
54- def _primitive_to_response (prim , kind : str ) -> PrimitiveResponse :
55- """Convert a discovered primitive to a response model."""
56- marker = _KIND_MAP [kind ][1 ]
57- marker_file = prim .path / marker
58- if marker_file .exists ():
59- text = marker_file .read_text ()
60- fm , body = parse_frontmatter (text )
61- else :
62- fm , body = {}, ""
55+ def _response_from_marker (marker_file : Path , kind : str , name : str ) -> PrimitiveResponse :
56+ """Read a marker file, parse its frontmatter, and build a response.
57+
58+ Centralises the read-parse-respond pattern used by list/get (via
59+ ``_primitive_to_response``), update, and create endpoints so the
60+ response construction logic lives in one place.
61+ """
62+ fm , body = parse_frontmatter (marker_file .read_text ())
6363 return PrimitiveResponse (
6464 kind = kind ,
65- name = prim . name ,
66- enabled = prim . enabled ,
65+ name = name ,
66+ enabled = fm . get ( " enabled" , True ) ,
6767 content = body ,
6868 frontmatter = fm ,
6969 )
7070
7171
72+ def _primitive_to_response (prim , kind : str ) -> PrimitiveResponse :
73+ """Convert a discovered primitive to a response model."""
74+ marker = _KIND_MAP [kind ][1 ]
75+ marker_file = prim .path / marker
76+ if not marker_file .exists ():
77+ return PrimitiveResponse (
78+ kind = kind , name = prim .name , enabled = prim .enabled ,
79+ content = "" , frontmatter = {},
80+ )
81+ return _response_from_marker (marker_file , kind , prim .name )
82+
83+
7284@router .get (
7385 "/projects/{project_dir}/primitives" ,
7486 response_model = list [PrimitiveResponse ],
@@ -112,17 +124,7 @@ async def update_primitive(
112124 raise HTTPException (status_code = 404 , detail = "Primitive not found" )
113125
114126 marker_file .write_text (serialize_frontmatter (body .frontmatter or {}, body .content ))
115-
116- # Re-read to return updated state
117- text = marker_file .read_text ()
118- fm , content = parse_frontmatter (text )
119- return PrimitiveResponse (
120- kind = kind ,
121- name = name ,
122- enabled = fm .get ("enabled" , True ),
123- content = content ,
124- frontmatter = fm ,
125- )
127+ return _response_from_marker (marker_file , kind , name )
126128
127129
128130@router .post (
@@ -154,15 +156,7 @@ async def create_primitive(
154156 marker_file = prim_dir / marker
155157
156158 marker_file .write_text (serialize_frontmatter (body .frontmatter or {}, body .content ))
157-
158- fm , content = parse_frontmatter (marker_file .read_text ())
159- return PrimitiveResponse (
160- kind = kind ,
161- name = name ,
162- enabled = fm .get ("enabled" , True ),
163- content = content ,
164- frontmatter = fm ,
165- )
159+ return _response_from_marker (marker_file , kind , name )
166160
167161
168162@router .delete ("/projects/{project_dir}/primitives/{kind}/{name}" , status_code = 204 )
@@ -174,3 +168,25 @@ async def delete_primitive(project_dir: str, kind: str, name: str) -> None:
174168 if not prim_dir .exists ():
175169 raise HTTPException (status_code = 404 , detail = "Primitive not found" )
176170 shutil .rmtree (prim_dir )
171+
172+
173+ @router .post (
174+ "/projects/{project_dir}/primitives/checks/{name}/test" ,
175+ response_model = CheckTestResponse ,
176+ )
177+ async def test_check (project_dir : str , name : str ) -> CheckTestResponse :
178+ """Run a single check and return the result."""
179+ root = _decode_project_dir (project_dir )
180+ for check in discover_checks (root ):
181+ if check .name == name :
182+ start = time .monotonic ()
183+ result = run_check (check , root )
184+ duration = time .monotonic () - start
185+ return CheckTestResponse (
186+ passed = result .passed ,
187+ exit_code = result .exit_code ,
188+ output = result .output ,
189+ timed_out = result .timed_out ,
190+ duration = round (duration , 2 ),
191+ )
192+ raise HTTPException (status_code = 404 , detail = "Check not found" )
0 commit comments