|
14 | 14 | """ |
15 | 15 |
|
16 | 16 | import base64 |
| 17 | +import html |
17 | 18 | import json |
18 | 19 | import os |
19 | 20 | import re |
|
26 | 27 | BRAIN = BRAIN_HOME / "bin" / "pyauto-brain" |
27 | 28 |
|
28 | 29 | SURFACE_KEYS = { |
29 | | - "generated", "org", "overnight", "heart", "heart_blockers", "performance", |
| 30 | + "generated", "org", "overnight", "heart", "heart_blockers", "heart_plan", |
| 31 | + "performance", |
30 | 32 | "hands", "versions", "community", "resume", "open_issues", "hygiene", |
31 | 33 | "devbox", "autonomy", "doors", "boards", "degraded", "history", |
32 | 34 | } |
|
78 | 80 | } |
79 | 81 |
|
80 | 82 | HEART_BOARD_JSON = { |
81 | | - "schema_version": 2, |
| 83 | + "schema_version": 3, |
82 | 84 | "blockers": [{ |
83 | 85 | "text": "RepoA: nightly smoke red", |
84 | 86 | "severity": "red", |
85 | 87 | "repo": "RepoA", |
86 | 88 | "repo_url": "https://example.invalid/RepoA", |
87 | 89 | "run_url": "https://example.invalid/run/9", |
88 | 90 | "prompt": "/bug Heart board: RepoA nightly smoke red — https://example.invalid/run/9", |
| 91 | + # v3: a stale row carries the command that re-runs its check; a red |
| 92 | + # one has no such remedy — the fix is code, not a re-run. |
| 93 | + "command": None, |
89 | 94 | }], |
90 | 95 | # Additive to schema v2 — an older Heart publish simply omits it. |
91 | 96 | "performance": HEART_PERFORMANCE, |
@@ -529,6 +534,63 @@ def test_heart_blockers_render_with_their_own_prompts(tmp_path): |
529 | 534 | assert "Shipped: **GREEN**" in md # the Hands headline joined the section |
530 | 535 |
|
531 | 536 |
|
| 537 | +HEART_BOARD_STALE = { |
| 538 | + "schema_version": 3, |
| 539 | + "blockers": [ |
| 540 | + {"text": "install verification not run", "severity": "stale", |
| 541 | + "repo": None, "repo_url": None, "run_url": None, |
| 542 | + "command": "pyauto-heart verify_install --report-json", |
| 543 | + "prompt": "/health run `pyauto-heart verify_install --report-json` — …"}, |
| 544 | + {"text": "no release validation for current source", "severity": "stale", |
| 545 | + "repo": None, "repo_url": None, "run_url": None, "command": None, |
| 546 | + "prompt": "/health dispatch a release rehearsal with `/release rehearse` — …"}, |
| 547 | + ], |
| 548 | + "stale_plan": { |
| 549 | + "count": 2, |
| 550 | + "command": None, |
| 551 | + "prompt": "/health clear the Heart's 2 evidence gap(s) — …\n1. …\n2. …", |
| 552 | + }, |
| 553 | +} |
| 554 | + |
| 555 | + |
| 556 | +def test_a_stale_heart_offers_the_one_plan_that_clears_every_gap(tmp_path): |
| 557 | + stub = _fabricate(tmp_path, _default_fixtures(), HEART_BOARD_STALE) |
| 558 | + page = _run(["--html"], tmp_path, stub).stdout |
| 559 | + plan = HEART_BOARD_STALE["stale_plan"] |
| 560 | + |
| 561 | + # The Heart's own plan is the chip payload — the Brain never derives one. |
| 562 | + assert html.escape(plan["prompt"], quote=True) in page |
| 563 | + assert "Clear all 2 evidence gaps" in page |
| 564 | + # Each gap still arrives with the command that closes it, forwarded whole. |
| 565 | + s = json.loads(_run(["--json"], tmp_path, stub).stdout) |
| 566 | + assert [b["command"] for b in s["heart_blockers"]] == [ |
| 567 | + "pyauto-heart verify_install --report-json", None] |
| 568 | + assert s["heart_plan"]["count"] == 2 |
| 569 | + # The digest spends one line on the tier, not one per gap. |
| 570 | + md = _run([], tmp_path, stub).stdout |
| 571 | + assert "Evidence gaps: 2 — clear them all: `pyauto-heart fix stale`" in md |
| 572 | + |
| 573 | + |
| 574 | +def test_a_plan_with_a_command_chain_offers_the_terminal_door(tmp_path): |
| 575 | + chain = "pyauto-heart verify_install --report-json && pyauto-heart tick" |
| 576 | + board = {**HEART_BOARD_STALE, |
| 577 | + "stale_plan": {**HEART_BOARD_STALE["stale_plan"], "command": chain}} |
| 578 | + stub = _fabricate(tmp_path, _default_fixtures(), board) |
| 579 | + |
| 580 | + page = _run(["--html"], tmp_path, stub).stdout |
| 581 | + assert f'data-cmd="{html.escape(chain, quote=True)}"' in page |
| 582 | + assert "copy term" in page # the ⌨ terminal chip, not the 📋 one |
| 583 | + md = _run([], tmp_path, stub).stdout |
| 584 | + assert f"clear them all: `{chain}`" in md |
| 585 | + |
| 586 | + |
| 587 | +def test_a_heart_board_without_a_plan_renders_none(tmp_path): |
| 588 | + stub = _fabricate(tmp_path, _default_fixtures()) # no stale_plan published |
| 589 | + s = json.loads(_run(["--json"], tmp_path, stub).stdout) |
| 590 | + assert s["heart_plan"] is None |
| 591 | + assert "Evidence gaps:" not in _run([], tmp_path, stub).stdout |
| 592 | + |
| 593 | + |
532 | 594 | def test_test_performance_rows_carry_their_own_prompts(tmp_path): |
533 | 595 | stub = _fabricate(tmp_path, _default_fixtures(), HEART_BOARD_WITH_EVENT) |
534 | 596 | s = json.loads(_run(["--json"], tmp_path, stub).stdout) |
|
0 commit comments