Skip to content

Commit 86aabc5

Browse files
committed
test(api): add python parity contract checks
1 parent 37b9d9c commit 86aabc5

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

apps/api/tests/helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def seed_index_db(db_path: Path) -> None:
155155
"INSERT INTO note_tags(path, tag) VALUES (?, ?)",
156156
[
157157
("docs/03-plan.md", "architecture"),
158+
("docs/03-plan.md", "project"),
158159
("notes/random.md", "misc"),
159160
],
160161
)
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
from typing import get_args
6+
7+
from fastapi.testclient import TestClient
8+
from pytest import MonkeyPatch
9+
10+
from ailss_api.main import create_app
11+
from ailss_api.models import FailureCode
12+
from tests.helpers import (
13+
DEFAULT_AGENT_INPUT,
14+
DEFAULT_RETRIEVE_QUERY,
15+
build_settings_with_seed_data,
16+
set_fake_embedding,
17+
)
18+
19+
PARITY_DOC_PATH = (
20+
Path(__file__).resolve().parents[3] / "docs" / "architecture" / "python-mcp-parity.md"
21+
)
22+
23+
24+
def _extract_bulleted_code_items(markdown: str, heading: str) -> list[str]:
25+
lines = markdown.splitlines()
26+
heading_index = next(
27+
(index for index, line in enumerate(lines) if line.strip() == heading),
28+
-1,
29+
)
30+
if heading_index < 0:
31+
return []
32+
33+
values: list[str] = []
34+
for line in lines[heading_index + 1 :]:
35+
stripped = line.strip()
36+
if not stripped:
37+
if values:
38+
break
39+
continue
40+
if stripped.startswith("#"):
41+
break
42+
43+
match = re.match(r"^- `([^`]+)`$", stripped)
44+
if match is None:
45+
continue
46+
values.append(match.group(1))
47+
return values
48+
49+
50+
def test_parity_doc_lists_current_agent_failure_codes() -> None:
51+
doc = PARITY_DOC_PATH.read_text(encoding="utf-8")
52+
53+
documented = sorted(
54+
_extract_bulleted_code_items(
55+
doc,
56+
"## Required failure codes for Python-covered agent flows",
57+
)
58+
)
59+
actual = sorted(get_args(FailureCode))
60+
61+
assert documented == actual
62+
63+
64+
def test_retrieve_parity_respects_combined_scope_filters(
65+
tmp_path: Path, monkeypatch: MonkeyPatch
66+
) -> None:
67+
settings = build_settings_with_seed_data(tmp_path)
68+
set_fake_embedding(monkeypatch)
69+
client = TestClient(create_app(settings))
70+
71+
response = client.post(
72+
"/retrieve",
73+
json={
74+
"query": DEFAULT_RETRIEVE_QUERY,
75+
"top_k": 3,
76+
"path_prefix": "docs/",
77+
"tags_any": ["project"],
78+
"tags_all": ["architecture", "project"],
79+
},
80+
)
81+
82+
assert response.status_code == 200
83+
payload = response.json()
84+
assert [result["path"] for result in payload["results"]] == ["docs/03-plan.md"]
85+
assert set(payload["results"][0]["tags"]) == {"architecture", "project"}
86+
87+
88+
def test_agent_run_returns_missing_context_when_scoped_filters_remove_candidates(
89+
tmp_path: Path, monkeypatch: MonkeyPatch
90+
) -> None:
91+
settings = build_settings_with_seed_data(tmp_path)
92+
set_fake_embedding(monkeypatch)
93+
client = TestClient(create_app(settings))
94+
95+
response = client.post(
96+
"/agent/run",
97+
json={
98+
"input": DEFAULT_AGENT_INPUT,
99+
"context": {
100+
"path_prefix": "notes/",
101+
"tags_all": ["architecture"],
102+
"top_k": 1,
103+
},
104+
},
105+
)
106+
107+
assert response.status_code == 200
108+
payload = response.json()
109+
assert payload["outcome"] == "failed"
110+
assert payload["failure"]["code"] == "missing_context"
111+
112+
113+
def test_agent_run_rejects_write_request_with_apply_true(tmp_path: Path) -> None:
114+
settings = build_settings_with_seed_data(tmp_path)
115+
client = TestClient(create_app(settings))
116+
117+
response = client.post(
118+
"/agent/run",
119+
json={
120+
"input": "Write the summary into a new note.",
121+
"requested_write_action": "capture_note",
122+
"apply": True,
123+
},
124+
)
125+
126+
assert response.status_code == 200
127+
payload = response.json()
128+
assert payload["outcome"] == "failed"
129+
assert payload["failure"]["code"] == "write_not_allowed"
130+
assert payload["write_actions"] == [
131+
{
132+
"action": "capture_note",
133+
"allowed": False,
134+
"reason": "write_not_allowed",
135+
}
136+
]

0 commit comments

Comments
 (0)