t8fleet.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. """The T8 fleet bundle (goal 41 task 8): the fleet + admin pages on the
  2. Spry conversion — the saddle cards' "now" head / "Keys & admin" tail
  3. split with the ONE consolidated health verdict line (saddle.health_verdict),
  4. the fleet health summary line above the grid when any saddle is
  5. offline/stale (saddles.fleet_alert), the capability catalogue as its own
  6. h2 "Routing tags" section, the register/capability create forms as
  7. native <dialog> modals behind buttons (the shared openAppDialog helper),
  8. the users page's create dialog + dense row actions, the model page's
  9. Spry form cards and the stack detail's in-place Details editor with the
  10. demoted delete. Markup-level via http_get_page; the slot-level verdict
  11. and alert cross-checked against the saddles slot data."""
  12. from harness import * # noqa: F401,F403 — the shared wire helpers
  13. def run(ctx):
  14. auth = ctx.auth
  15. print("== /saddles: the head/tail split markup ==")
  16. page = http_get_page(auth, "/saddles")
  17. # The admin tail folds under ONE expander after the verdict/work
  18. # head (ux-notes quick win 8 A); the fold carries the hairline.
  19. check("Keys &amp; admin</summary>" in page,
  20. "the card tail folds under one Keys & admin expander")
  21. check(page.index("Keys &amp; admin</summary>")
  22. > page.index("stm-text=\"saddle.health_verdict\""),
  23. "the admin tail sits after the verdict line (head first)")
  24. check("saddle-fold" in page,
  25. "the head/tail boundary carries the saddle-fold hairline")
  26. check("saddle.health_verdict" in page,
  27. "the card head renders the composed health verdict line")
  28. # The three old fragments no longer lead the head: the badge keeps
  29. # the colour, the verdict carries the words.
  30. check("'daemon ' + saddle.daemon_state" not in page
  31. and "'contacted ' + saddle.last_contact_display" not in page,
  32. "the fragmented health line is gone (one verdict instead)")
  33. check("stm-text=\"saddle.current_goal_title\"" in page
  34. and "stm-text=\"saddle.current_task_title\"" in page,
  35. "current work stays in the head")
  36. print("== /saddles: the fleet summary line + Routing tags section ==")
  37. # fleet_alert is server-composed; the page renders it when
  38. # non-empty. Mark a saddle offline to shape it, then restore.
  39. conn = db()
  40. conn.execute("update saddles set status = 'offline' where id = ?",
  41. (ctx.fake_id,))
  42. conn.commit()
  43. conn.close()
  44. state = slot_public(auth, "/saddles", "saddles")
  45. check(state["fleet_alert"].startswith("1 of ")
  46. and "not healthy" in state["fleet_alert"]
  47. and "fake-saddle" in state["fleet_alert"],
  48. "an offline saddle shapes the fleet alert line: %s"
  49. % state["fleet_alert"])
  50. check("dispatcher may be starved" in state["fleet_alert"],
  51. "the alert names the dispatcher risk")
  52. card = saddle_card(auth, "fake-saddle")
  53. check(card["health"] == "offline"
  54. and "offline" in card["health_verdict"],
  55. "the offline row's verdict agrees with its health")
  56. conn = db()
  57. conn.execute("update saddles set status = 'idle' where id = ?",
  58. (ctx.fake_id,))
  59. conn.commit()
  60. conn.close()
  61. state = slot_public(auth, "/saddles", "saddles")
  62. check(state["fleet_alert"] == "",
  63. "a healthy fleet renders no alert (empty string)")
  64. # The catalogue is its own h2 section (recommendation C), not an
  65. # h3 tail after the grid.
  66. check("<h2 id=\"routing-tags\">Routing tags</h2>" in page,
  67. "the capability catalogue is its own h2 Routing tags section")
  68. check("<h3>Capabilities</h3>" not in page,
  69. "the old h3 Capabilities tail is gone")
  70. print("== /saddles: the register + capability dialogs ==")
  71. check('id="saddle-register-dialog"' in page
  72. and "<dialog" in page,
  73. "the register form is a native dialog")
  74. check('onclick="window.openAppDialog(\'saddle-register-dialog\')"' in page,
  75. "the + Register saddle button opens the dialog via the helper")
  76. register_html = page[page.index('id="saddle-register-dialog"'):]
  77. check('stm-action="saddles.register"' in register_html
  78. and 'name="name"' in register_html
  79. and 'name="host"' in register_html,
  80. "the dialog carries the unchanged register form")
  81. check('class="dialog-titlebar"' in register_html
  82. and 'class="dialog-footer"' in register_html,
  83. "the register dialog uses Spry's dialog classes")
  84. check('id="capability-create-dialog"' in page
  85. and 'stm-action="saddles.create_capability"' in page,
  86. "the capability create form lives in a dialog too")
  87. check("<details>" in page and "<summary><strong>Register saddle</strong></summary>" not in page,
  88. "no create expander remains on the saddles page (details are the admin tail)")
  89. # Semantics: registering through the dialog form's action works,
  90. # then clean up (the fake saddle stays the fleet's only daemon).
  91. before = len(slot_public(auth, "/saddles", "saddles")["saddles"])
  92. act(auth, state["register"], {"name": "t8-probe", "host": "localhost:1"})
  93. rows = slot_public(auth, "/saddles", "saddles")["saddles"]
  94. check(len(rows) == before + 1, "the register action still creates")
  95. probe = [s for s in rows if s["name"] == "t8-probe"][0]
  96. # register_saddle creates the row IDLE (the proven-derivation
  97. # decision): a never-contacted saddle honestly reads unknown, and
  98. # the verdict says so without the alarm of "offline".
  99. check(probe["health"] == "unknown"
  100. and "no health report yet" in probe["health_verdict"],
  101. "a never-contacted saddle reads unknown with the no-report verdict")
  102. act(auth, probe["unregister"], {})
  103. check(len(slot_public(auth, "/saddles", "saddles")["saddles"]) == before,
  104. "the probe saddle unregisters again")
  105. print("== /users: create dialog + dense row actions ==")
  106. users_page = http_get_page(auth, "/users")
  107. check('id="user-create-dialog"' in users_page
  108. and 'stm-action="users.create"' in users_page
  109. and 'onclick="window.openAppDialog(\'user-create-dialog\')"' in users_page,
  110. "the user create form lives in a dialog behind the button")
  111. check("<summary><strong>New user</strong></summary>" not in users_page,
  112. "no create expander remains on the users page")
  113. check('name="confirm_password"' in users_page
  114. and 'name="date_of_birth"' in users_page,
  115. "the dialog carries the unchanged create fields")
  116. check("btn-danger" in users_page and "Delete this user?" in users_page,
  117. "the destructive delete is the visually secondary danger button")
  118. print("== /models: Spry form cards ==")
  119. models_page = http_get_page(auth, "/models")
  120. check('stm-action="model.save"' in models_page
  121. and 'id="model-base-url"' in models_page
  122. and 'id="advisor-base-url"' in models_page,
  123. "the model form keeps its fields with labelled controls")
  124. check('class="card-header"' in models_page and "form-rows" in models_page,
  125. "the endpoint groups are cards with capped-width fields")
  126. check("alert-warning" in models_page or "alert-info" in models_page,
  127. "the readiness line renders as an alert")
  128. # The saved hint round-trip already covered by modelconfig; the
  129. # save action stays the same one.
  130. print("== /stacks/{id}: in-place Details editor + demoted delete ==")
  131. stacks_state = slot_public(auth, "/stacks", "stacks")
  132. result = act(auth, stacks_state["create"],
  133. {"name": "T8 probe stack", "summary": "markup probe"})
  134. stack_id = None
  135. for row in slot_public(auth, "/stacks", "stacks")["stacks"]:
  136. if row["name"] == "T8 probe stack":
  137. stack_id = row["id"]
  138. check(stack_id is not None, "the probe stack exists")
  139. stack_page = http_get_page(auth, "/stacks/%s" % stack_id)
  140. check("data-long-form" in stack_page and "data-lf-toggle" in stack_page,
  141. "the Details card toggles in place (view/edit halves)")
  142. check("initLongFormEditors" in stack_page,
  143. "the stack page initialises the shared editor engine")
  144. check('stm-action="stack.update"' in stack_page
  145. and 'stm-action="stack.add_member"' in stack_page,
  146. "the update and add-member actions are unchanged")
  147. # The old side-by-side Edit card is gone; the delete is a small
  148. # danger button inside a flat card at the bottom.
  149. check("<h3>Edit</h3>" not in stack_page,
  150. "the duplicate side-by-side Edit card is gone")
  151. check('class="card card-flat"' in stack_page
  152. and "btn-danger" in stack_page,
  153. "the delete action sits in a flat danger zone")
  154. # Update through the in-place form's action still works.
  155. detail = slot_public(auth, "/stacks/%s" % stack_id, "stack")
  156. result = act(auth, detail["update"],
  157. {"stack_id": str(stack_id), "name": "T8 probe stack 2",
  158. "summary": "renamed in place"})
  159. check("saved" in (action_notify(result) or ""),
  160. "the in-place editor's action saves the rename")
  161. act(auth, slot_public(auth, "/stacks/%s" % stack_id, "stack")["delete"], {})
  162. check(slot_public(auth, "/stacks", "stacks")["stacks"] == [],
  163. "the probe stack deletes again")