"""The T9 library bundle (goal 41 task 9): the project detail, rules,
discoveries and chats pages on the Spry conversion — the project page's
Details/Edit twin cards collapsed into ONE in-place editor
(data-long-form) with the freed structure carrying the Goals reverse
link and a create-task deep-link into the /tasks dialog, the rules
page's Edit expander turned in-place with Delete demoted behind a
ghost button and the new blast-radius attachments list, the discoveries
rows' edit expander turned in-place, and breadcrumbs on the project and
chat detail pages. Markup-level via http_get_page; the new slot members
(goals vector, rule attachments, crumbs vectors) cross-checked against
seeded database rows."""
from harness import * # noqa: F401,F403 — the shared wire helpers
def run(ctx):
auth, fake = ctx.auth, ctx.fake
now = "strftime('%s','now')"
print("== project detail: the in-place Details editor ==")
# A dedicated project so the bundle owns every row it asserts on.
state = slot_public(auth, "/projects", "projects")
act(auth, state["create"], {"name": "t9-library",
"git_url": "https://example.com/t9.git",
"default_branch": "main",
"summary": "the T9 probe project"})
rows = slot_public(auth, "/projects", "projects")["projects"]
project_id = int([p["id"] for p in rows if p["name"] == "t9-library"][0])
detail = slot_public(auth, "/projects/%d" % project_id, "project")
check(detail["update"] is not None,
"the project slot authors the update action")
page = http_get_page(auth, "/projects/%d" % project_id)
check('data-long-form data-field="project"' in page,
"the Details card is one data-long-form card")
check(page.count('data-lf-edit') >= 1 and page.count('data-lf-view') >= 1,
"the card carries the read/edit halves")
check('data-lf-src="project.git_url"' in page
and 'data-lf-src="project.summary"' in page,
"the edit half prefills from the slot's current values")
check("
Edit
" not in page,
"the side-by-side Edit card is gone")
check('stm-action="project.update"' in page,
"the editor form posts the project update action")
print("== project detail: breadcrumbs replace the back anchor ==")
check("All projects
" not in page,
"the hand-drawn All-projects anchor is gone")
check('stm-def-crumbs-as="project.crumbs"' in page,
"the breadcrumb partial rides the project page")
crumbs = detail["crumbs"]
check([c["label"] for c in crumbs]
== ["Dashboard", "Projects", "t9-library"]
and crumbs[0]["href"] == "/" and crumbs[1]["href"] == "/projects"
and crumbs[2].get("href") is None,
"the crumbs vector trails Dashboard / Projects / (%s)"
% [c["label"] for c in crumbs])
print("== project detail: the create-task deep-link ==")
check("'#new?project=' + project.id" in page
or "'/tasks#new?project=' + project.id" in page,
"the create affordance deep-links the /tasks dialog prefill")
check("New task in this project" in page,
"the affordance names the project flow")
tasks_page = http_get_page(auth, "/tasks")
check("applyNewTaskPrefill" in tasks_page,
"the tasks page ships the prefill pass")
check("task-create-dialog" in tasks_page
and "openAppDialog" in tasks_page,
"the prefill opens T4's create dialog through the helper")
print("== project detail: the goals reverse link ==")
# Seed one primary goal and one co-developed link; a reference link
# must NOT appear (context, not work happening here).
conn = db()
now = "strftime('%s','now')"
conn.execute(
"insert into goals (project_id, title, brief, branch_name, status,"
" priority, creator, created_at) values (?, 'T9 primary goal',"
" 'built here', 'goal/t9-primary', 'active', 3, 'human', %s)"
% now, (project_id,))
primary_goal = int(conn.execute("select max(id) from goals").fetchone()[0])
conn.execute(
"insert into projects (name, git_url, default_branch, created_at)"
" values ('t9-codev', 'https://example.com/t9c.git', 'main', %s)"
% now)
codev_project = int(conn.execute("select max(id) from projects")
.fetchone()[0])
conn.execute(
"insert into goals (project_id, title, brief, branch_name, status,"
" priority, creator, created_at) values (?, 'T9 co-dev goal',"
" 'built alongside', 'goal/t9-codedev', 'planning', 3, 'human', %s)"
% now, (codev_project,))
codev_goal = int(conn.execute("select max(id) from goals").fetchone()[0])
conn.execute(
"insert into goal_links (goal_id, project_id, relationship)"
" values (?, ?, 'co-developed')", (codev_goal, project_id))
conn.execute(
"insert into goals (project_id, title, brief, branch_name, status,"
" priority, creator, created_at) values (?, 'T9 ref goal',"
" 'read only', 'goal/t9-ref', 'draft', 3, 'human', %s)"
% now, (codev_project,))
ref_goal = int(conn.execute("select max(id) from goals").fetchone()[0])
conn.execute(
"insert into goal_links (goal_id, project_id, relationship)"
" values (?, ?, 'reference')", (ref_goal, project_id))
conn.commit()
conn.close()
detail = slot_public(auth, "/projects/%d" % project_id, "project")
goals = {str(g["id"]): g for g in detail["goals"]}
check(set(goals) == {str(primary_goal), str(codev_goal)},
"the goals vector carries the primary and co-developed goals only"
" (got %s)" % sorted(goals))
check(goals[str(primary_goal)]["relationship"] == "primary"
and goals[str(codev_goal)]["relationship"] == "co-developed",
"each goal row names its relationship")
check(goals[str(primary_goal)]["display"] == "active"
and goals[str(codev_goal)]["display"] == "planning",
"the rows carry the human display status")
page = http_get_page(auth, "/projects/%d" % project_id)
check("Goals in this project" in page
and "'/goals/' + goal.id" in page,
"the page renders the goals reverse-link table")
check("'badge badge-' + goal.status" in page,
"goal rows badge their status like the goals list")
print("== rules: the in-place editor + the demoted delete ==")
rules_state = slot_public(auth, "/rules", "rules")
act(auth, rules_state["create"], {
"name": "T9 blast rule", "body": "The blast radius probe rule."})
conn = db()
rule_id = int(conn.execute(
"select id from rules where name = 'T9 blast rule'").fetchone()[0])
# Attach it to the probe project and to one task.
conn.execute(
"insert into rule_projects (rule_id, project_id, created_at)"
" values (?, ?, %s)" % now, (rule_id, project_id))
conn.execute(
"insert into tasks (project_id, title, status, priority, created_at)"
" values (?, 'T9 blast task', 'queued', 5, %s)" % now, (project_id,))
blast_task = int(conn.execute("select max(id) from tasks").fetchone()[0])
conn.execute(
"insert into task_rules (rule_id, task_id, created_at)"
" values (?, ?, %s)" % now, (rule_id, blast_task))
conn.commit()
conn.close()
rules_state = slot_public(auth, "/rules", "rules")
row = [r for r in rules_state["rules"]
if r["name"] == "T9 blast rule"][0]
check([(a["kind"], a["label"]) for a in row["attachments"]]
== [("project", "t9-library"), ("task", "T9 blast task")],
"the rule row lists its blast radius in attachment order (%s)"
% [(a["kind"], a["label"]) for a in row["attachments"]])
check(all(int(a["id"]) > 0 for a in row["attachments"]),
"each attachment carries the linked row's id")
unattached = [r for r in rules_state["rules"]
if len(r.get("attachments") or []) == 0]
check(len(unattached) >= 1
or len(rules_state["rules"]) == 1,
"rules attached nowhere list no attachments (no crash)")
page = http_get_page(auth, "/rules")
check("New rule" not in page
and "rule-create-dialog" in page
and "openAppDialog('rule-create-dialog')" in page,
"the create expander became a dialog behind a button")
check("Edit" not in page,
"the per-rule Edit expander is gone")
check('data-long-form data-field="rule-row"' in page
and 'data-lf-toggle' in page,
"each rule edits through the in-place engine")
# The compiled page entity-encodes the body text ("…" -> …);
# assert on the escaped form and on the ghost class itself.
check("Delete rule…" in page
and 'class="btn-ghost btn-sm"' in page,
"delete is demoted to a ghost button out of the row's face")
check('stm-text="attachment.label"' in page,
"the blast radius renders as links on the rule card")
print("== discoveries: the in-place editor ==")
page = http_get_page(auth, "/discoveries")
check("Edit" not in page,
"the discoveries Edit expander is gone")
check('data-long-form data-field="discovery-row"' in page,
"each discovery edits through the in-place engine")
check('stm-text="discovery.body"' in page
and 'name="body"' in page,
"the edit textarea prefills from the rendered row value")
check('discoveryRowVisible' in page and 'setDiscoveryQuery' in page,
"the client-side search semantics are unchanged")
state = slot_public(auth, "/discoveries", "discoveries")
check("query" in state and "more" in state,
"the slot still seeds the search box and the more count")
print("== chats: breadcrumbs on the detail page ==")
# Seed the chat row directly (the slot builder reads any owned row;
# this bundle does not restart the app with a scripted LLM - the
# chatui bundle owns that surface).
conn = db()
conn.execute(
"insert into advisor_chats (user_id, title, status, created_at,"
" updated_at) values ((select id from spry_users where username ="
" 'admin'), 'T9 crumbs probe', 'idle', %s, %s)" % (now, now))
chat_id = int(conn.execute("select max(id) from advisor_chats")
.fetchone()[0])
conn.commit()
conn.close()
chat_uri = "/chats/%d" % chat_id
chat_state = slot_public(auth, chat_uri, "chat")
check(chat_state["id"] == chat_id, "the probe chat exists (%s)" % chat_uri)
crumbs = chat_state["crumbs"]
check([c["label"] for c in crumbs]
== ["Dashboard", "Chats", "T9 crumbs probe"]
and crumbs[0]["href"] == "/" and crumbs[1]["href"] == "/chats"
and crumbs[2].get("href") is None,
"the chat slot trails Dashboard / Chats / (%s)"
% [c["label"] for c in crumbs])
page = http_get_page(auth, chat_uri)
check('stm-def-crumbs-as="chat.crumbs"' in page,
"the breadcrumb partial rides the chat detail page")
check("chat-thread" in page and "chat-composer" in page,
"the conversation still leads with the composer below")
detail = slot_public(auth, "/chats/%d" % chat_id, "chat")
check(detail["delete"] is not None,
"the seeded chat still authors its delete action")
print("== chats list: the demoted delete ==")
page = http_get_page(auth, "/chats")
check('class="btn-ghost btn-sm" stm-action="chat.delete"' in page,
"the list's delete is a quiet ghost button")
check("alert alert-warning" in page,
"the unconfigured-advisor notice uses the Spry alert")