Index workplan flavor and omit residuals from default views.
STATE-WP-0092: persist flavor from files, treat depends_on as the C-20 canonical edge, exclude flavor=residual from summary/next_steps/deps unless include_residuals is set. Live primary still needs the alembic revision applied. Assistant: grok Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
This commit is contained in:
parent
ea11451e9c
commit
ddc3338541
18 changed files with 852 additions and 25 deletions
|
|
@ -1328,6 +1328,173 @@ class TestC20DependencyDetection:
|
|||
|
||||
assert "C-20" not in [issue.check_id for issue in report.issues]
|
||||
|
||||
def test_depends_on_alias_satisfies_workplan_dependency(self, tmp_path, monkeypatch):
|
||||
repo = tmp_path / "repo"
|
||||
workplans = repo / "workplans"
|
||||
workplans.mkdir(parents=True)
|
||||
(workplans / "STATE-WP-0001-base.md").write_text(
|
||||
"---\n"
|
||||
"id: STATE-WP-0001\n"
|
||||
"title: Base\n"
|
||||
"domain: financials\n"
|
||||
"repo: demo-repo\n"
|
||||
"status: active\n"
|
||||
"state_hub_workstream_id: \"base-ws\"\n"
|
||||
"---\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(workplans / "STATE-WP-0002-dependent.md").write_text(
|
||||
"---\n"
|
||||
"id: STATE-WP-0002\n"
|
||||
"title: Dependent\n"
|
||||
"domain: financials\n"
|
||||
"repo: demo-repo\n"
|
||||
"status: active\n"
|
||||
"state_hub_workstream_id: \"dependent-ws\"\n"
|
||||
"depends_on:\n"
|
||||
" - STATE-WP-0001\n"
|
||||
"---\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def fake_get(_api_base, path, params=None, **_kwargs):
|
||||
if path == "/repos/demo-repo":
|
||||
import socket
|
||||
|
||||
return {
|
||||
"id": "repo-1",
|
||||
"slug": "demo-repo",
|
||||
"local_path": str(repo),
|
||||
"host_paths": {socket.gethostname(): str(repo)},
|
||||
"domain_slug": "financials",
|
||||
}
|
||||
if path == "/workplans/base-ws":
|
||||
return {"id": "base-ws", "repo_id": "repo-1", "slug": "state-wp-0001", "title": "Base", "status": "active"}
|
||||
if path == "/workplans/dependent-ws":
|
||||
return {"id": "dependent-ws", "repo_id": "repo-1", "slug": "state-wp-0002", "title": "Dependent", "status": "active"}
|
||||
if path == "/tasks" and params and params.get("workstream_id") in {"base-ws", "dependent-ws"}:
|
||||
return []
|
||||
if path == "/workplans/base-ws/dependencies":
|
||||
return []
|
||||
if path == "/workplans/dependent-ws/dependencies":
|
||||
return [
|
||||
{
|
||||
"id": "dep-1",
|
||||
"from_workplan_id": "dependent-ws",
|
||||
"to_workplan_id": "base-ws",
|
||||
"to_task_id": None,
|
||||
"relationship_type": "blocks",
|
||||
}
|
||||
]
|
||||
if path == "/workplans" and params == {"repo_id": "repo-1"}:
|
||||
return []
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("consistency_check._api_get", fake_get)
|
||||
|
||||
report = check_repo("http://unused", "demo-repo")
|
||||
|
||||
assert "C-20" not in [issue.check_id for issue in report.issues]
|
||||
|
||||
|
||||
class TestFlavorChecks:
|
||||
def test_unknown_flavor_warns_c36(self, tmp_path, monkeypatch):
|
||||
repo = tmp_path / "repo"
|
||||
workplans = repo / "workplans"
|
||||
workplans.mkdir(parents=True)
|
||||
(workplans / "STATE-WP-0001-odd.md").write_text(
|
||||
"---\n"
|
||||
"id: STATE-WP-0001\n"
|
||||
"title: Odd\n"
|
||||
"domain: financials\n"
|
||||
"repo: demo-repo\n"
|
||||
"status: active\n"
|
||||
"flavor: sidetrack\n"
|
||||
"state_hub_workstream_id: \"odd-ws\"\n"
|
||||
"---\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def fake_get(_api_base, path, params=None, **_kwargs):
|
||||
if path == "/repos/demo-repo":
|
||||
import socket
|
||||
|
||||
return {
|
||||
"id": "repo-1",
|
||||
"slug": "demo-repo",
|
||||
"local_path": str(repo),
|
||||
"host_paths": {socket.gethostname(): str(repo)},
|
||||
"domain_slug": "financials",
|
||||
}
|
||||
if path == "/workplans/odd-ws":
|
||||
return {
|
||||
"id": "odd-ws",
|
||||
"repo_id": "repo-1",
|
||||
"slug": "state-wp-0001",
|
||||
"title": "Odd",
|
||||
"status": "active",
|
||||
}
|
||||
if path == "/tasks":
|
||||
return []
|
||||
if path.endswith("/dependencies"):
|
||||
return []
|
||||
if path == "/workplans":
|
||||
return []
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("consistency_check._api_get", fake_get)
|
||||
report = check_repo("http://unused", "demo-repo")
|
||||
assert "C-36" in [issue.check_id for issue in report.issues]
|
||||
|
||||
def test_residual_without_origin_warns_c37(self, tmp_path, monkeypatch):
|
||||
repo = tmp_path / "repo"
|
||||
workplans = repo / "workplans"
|
||||
workplans.mkdir(parents=True)
|
||||
(workplans / "STATE-WP-0001-left.md").write_text(
|
||||
"---\n"
|
||||
"id: STATE-WP-0001\n"
|
||||
"title: Left\n"
|
||||
"domain: financials\n"
|
||||
"repo: demo-repo\n"
|
||||
"status: active\n"
|
||||
"flavor: residual\n"
|
||||
"state_hub_workstream_id: \"left-ws\"\n"
|
||||
"---\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def fake_get(_api_base, path, params=None, **_kwargs):
|
||||
if path == "/repos/demo-repo":
|
||||
import socket
|
||||
|
||||
return {
|
||||
"id": "repo-1",
|
||||
"slug": "demo-repo",
|
||||
"local_path": str(repo),
|
||||
"host_paths": {socket.gethostname(): str(repo)},
|
||||
"domain_slug": "financials",
|
||||
}
|
||||
if path == "/workplans/left-ws":
|
||||
return {
|
||||
"id": "left-ws",
|
||||
"repo_id": "repo-1",
|
||||
"slug": "state-wp-0001",
|
||||
"title": "Left",
|
||||
"status": "active",
|
||||
"flavor": "residual",
|
||||
}
|
||||
if path == "/tasks":
|
||||
return []
|
||||
if path.endswith("/dependencies"):
|
||||
return []
|
||||
if path == "/workplans":
|
||||
return []
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("consistency_check._api_get", fake_get)
|
||||
report = check_repo("http://unused", "demo-repo")
|
||||
assert "C-37" in [issue.check_id for issue in report.issues]
|
||||
|
||||
|
||||
class TestC06WorkstreamCreation:
|
||||
def test_fix_repo_bootstraps_legacy_ids_only_into_empty_projection(
|
||||
|
|
|
|||
136
tests/test_work_record_flavor.py
Normal file
136
tests/test_work_record_flavor.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"""STATE-WP-0092: flavor indexing, residual default-exclusion, depends_on alias."""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.test_routers_core import (
|
||||
_create_domain,
|
||||
_create_repo,
|
||||
_create_task,
|
||||
_create_topic,
|
||||
_create_workstream,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_flavor_rejected_on_create(client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
repo = await _create_repo(client)
|
||||
r = await client.post(
|
||||
"/workplans/",
|
||||
json={
|
||||
"repo_id": repo["id"],
|
||||
"topic_id": topic["id"],
|
||||
"slug": "bad-flavor",
|
||||
"title": "Bad flavor",
|
||||
"flavor": "sidetrack",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flavor_round_trip_and_list_filters(client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
relevant = await _create_workstream(
|
||||
client, topic["id"], slug="rel-wp", title="Relevant", flavor="implementation"
|
||||
)
|
||||
residual = await _create_workstream(
|
||||
client, topic["id"], slug="res-wp", title="Leftover", flavor="residual"
|
||||
)
|
||||
unspecified = await _create_workstream(
|
||||
client, topic["id"], slug="unset-wp", title="Historic"
|
||||
)
|
||||
|
||||
listed = await client.get("/workplans/")
|
||||
ids = {row["id"] for row in listed.json()}
|
||||
assert relevant["id"] in ids
|
||||
assert residual["id"] in ids
|
||||
assert unspecified["id"] in ids
|
||||
|
||||
hidden = await client.get("/workplans/", params={"include_residuals": "false"})
|
||||
hidden_ids = {row["id"] for row in hidden.json()}
|
||||
assert relevant["id"] in hidden_ids
|
||||
assert unspecified["id"] in hidden_ids
|
||||
assert residual["id"] not in hidden_ids
|
||||
|
||||
only_residual = await client.get("/workplans/", params={"flavor": "residual"})
|
||||
only_ids = {row["id"] for row in only_residual.json()}
|
||||
assert only_ids == {residual["id"]}
|
||||
|
||||
got = await client.get(f"/workplans/{residual['id']}")
|
||||
assert got.json()["flavor"] == "residual"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_summary_omits_residuals_unless_included(client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
relevant = await _create_workstream(
|
||||
client, topic["id"], slug="sum-rel", title="Do this", flavor="planning"
|
||||
)
|
||||
residual = await _create_workstream(
|
||||
client, topic["id"], slug="sum-res", title="Later maybe", flavor="residual"
|
||||
)
|
||||
|
||||
summary = (await client.get("/state/summary")).json()
|
||||
open_ids = {row["id"] for row in summary["open_workplans"]}
|
||||
assert relevant["id"] in open_ids
|
||||
assert residual["id"] not in open_ids
|
||||
assert summary["totals"]["workstreams"]["residual_open"] == 1
|
||||
assert summary["residual_open_workplans"] == []
|
||||
|
||||
included = (await client.get("/state/summary", params={"include_residuals": "true"})).json()
|
||||
residual_ids = {row["id"] for row in included["residual_open_workplans"]}
|
||||
assert residual["id"] in residual_ids
|
||||
assert relevant["id"] not in residual_ids
|
||||
open_ids_still = {row["id"] for row in included["open_workplans"]}
|
||||
assert residual["id"] not in open_ids_still
|
||||
|
||||
deps = (await client.get("/state/deps")).json()
|
||||
dep_ids = {row["id"] for row in deps}
|
||||
assert relevant["id"] in dep_ids
|
||||
assert residual["id"] not in dep_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_next_steps_skips_residual_workplan(client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
blocker = await _create_workstream(
|
||||
client, topic["id"], slug="done-dep", title="Finished dep", status="finished"
|
||||
)
|
||||
residual = await _create_workstream(
|
||||
client, topic["id"], slug="res-ready", title="Residual leftover", flavor="residual"
|
||||
)
|
||||
r = await client.post(
|
||||
f"/workplans/{residual['id']}/dependencies/",
|
||||
json={"to_workstream_id": blocker["id"]},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
await _create_task(client, residual["id"], title="Do not start this")
|
||||
|
||||
steps = (await client.get("/state/next_steps")).json()
|
||||
assert all(step.get("workplan_id") != residual["id"] for step in steps)
|
||||
assert all("Residual leftover" not in (step.get("message") or "") for step in steps)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_flavor_round_trip(client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"], slug="task-flavor-wp")
|
||||
r = await client.post(
|
||||
"/tasks/",
|
||||
json={"workplan_id": ws["id"], "title": "Leftover task", "flavor": "residual"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
assert r.json()["flavor"] == "residual"
|
||||
patched = await client.patch(
|
||||
f"/tasks/{r.json()['id']}",
|
||||
json={"flavor": "implementation", "suppress_token_event": True},
|
||||
)
|
||||
assert patched.status_code == 200
|
||||
assert patched.json()["flavor"] == "implementation"
|
||||
Loading…
Add table
Add a link
Reference in a new issue