| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- """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("<h3>Edit</h3>" 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("<p><a href=\"/projects\">All projects</a></p>" 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 / <name> (%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("<summary><strong>New rule</strong></summary>" 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("<summary>Edit</summary>" 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…</button>" 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("<summary>Edit</summary>" 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 / <title> (%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")
|