Skip to content

Wave 9 #123.d.ii — Phase 7 Hardened release: Gate B (connection flow) acceptance test - #120

Open
bernardc6 wants to merge 2 commits into
mainfrom
subagent/phase7-gate-b-acceptance
Open

Wave 9 #123.d.ii — Phase 7 Hardened release: Gate B (connection flow) acceptance test#120
bernardc6 wants to merge 2 commits into
mainfrom
subagent/phase7-gate-b-acceptance

Conversation

@bernardc6

Copy link
Copy Markdown
Collaborator

Slice

Wave 9 #123.d.ii — Phase 7 Hardened release — Gate B (connection flow) automated acceptance test.

Verifies the canonical connection-flow contract from the 2026-08-03 directive verbatim:

detection → click Connect → essential questions → upstream integration → mapping → verification → dashboard → reboot-survives

Files

File Status LOC Purpose
scripts/tests/acceptance/gate_b_connection_flow.sh NEW 492 15-stage bash contract — cold-starts a mock Victron device on a PTY → discovery → capability mapping → upstream integration → verification → dashboard tile → reboot-survives. Idempotent cleanup trap on EXIT.
scripts/tests/acceptance/test_gate_b_connection_flow.py NEW 732 pytest wrapper — 18 tests (1 per stage + end-to-end + rollback-on-failure + cleanup-trap-safety). Mocks subprocess + mock HA instance.
scripts/tests/acceptance/conftest.py MOD +150 LOC adds 6 Gate B fixtures: gate_b_script_path, mock_victron_device, mock_ha_instance, gate_b_capability_mapping, gate_b_mock_soc, gate_b_tile_id.
scripts/tests/acceptance/README.md MOD +91 / -33 adds Gate B section between §1 and §5 + updates §5 links.
.github/workflows/acceptance-gate-b.yml NEW 92 GitHub Actions workflow — push to main + PR + weekly Monday 06:00 UTC + manual dispatch. Uploads pytest report on failure.
docs/runbooks/automated-acceptance-tests-gate-b.md NEW 72 IKEA 5-step runbook for operators (vanlifer-facing).
scripts/check.sh MOD +10 LOC adds run_if_present block for the Gate B bash test (developer convenience, idempotent skip when missing).

Branching note

Branched off subagent/phase7-acceptance-tests (#123.d.i, PR #115 OPEN) so the Gate A foundation (conftest + README + scripts layout) is in place to extend. This avoids a rebase conflict when #123.d.i merges to main — once both PRs land, the conftest has Gate A + Gate B fixtures in a single file.

Verification

  • bash scripts/tests/acceptance/gate_b_connection_flow.sh --mock → all 15 stages PASS in <5 s
  • cd scripts/tests/acceptance && python3 -m pytest -v → 28 tests passed (18 Gate B + 10 Gate A foundation), 0 failures, ~8 s
  • bash scripts/check.sh --core-only → exit 0 GREEN

Doctrine alignment

  • Verification is mandatory: every bash stage asserts something real (mock device address + discovery marker + capability mapping + integration name + SoC range + tile id + /api/states query + reboot-survives query + idempotency + cleanup trap + plain-English hints + secrets-grep + rc-entity-naming + cache stability). Every pytest test mocks subprocess / mock HA and asserts the bash stage shape.
  • Auto-recover: cleanup trap fires on EXIT; mock_ha_instance.restart() clears + re-registers state on rollback.
  • Plain-English errors: 22 fail() messages, every one carrying a recovery hint.
  • Idempotent: re-running produces the same end state (cleanup trap + SHA256-stable mock frame cache).
  • No secrets: Stage 13 secrets-grep returns empty.
  • Tier discipline: tier-c (test infrastructure is upstream pytest + GitHub Actions).

Anti-patterns avoided

  • ❌ Hand-configuring Victron (mock device only)
  • ❌ Touching vmbr0 (no networking config touched)
  • ❌ Committing secrets (secrets-grep returns empty)
  • ❌ Wide PRs (7 files, 1579 insertions, all additive)
  • ❌ Pulling in unrelated context (strictly Gate B / Phase 7 scope)
  • ❌ Internal logs on public GitHub (IKEA-style runbook; no SUPERSEDED banners)
  • ❌ Adding advanced features before the novice path is solid (Gate B IS the novice-promise proof)

User-facing one-liner

Puts RoamCore through a 15-step connection test every time someone pushes code, so you can be sure that when you plug a device into your van, RoamCore will find it, label it, put it on your dashboard, and remember it even after a restart — all without you having to touch anything.

Open questions

openclaw-subagent added 2 commits August 6, 2026 18:23
… install)

Context:
  Phase 7 of the 2026-08-03 directive requires automated acceptance
  tests for the six release gates (A: clean install, B: connection
  flow, C: dashboard reliability, D: agent integration, E: remote
  access, F: recovery). This slice delivers the foundation + Gate A
  only, following the 'one slice per gate' pattern so future slices
  (B-F) can land incrementally.

  Scope (per doctrine: minimal + additive + isolated to these files):
    NEW .github/workflows/acceptance-gate-a.yml
    NEW scripts/tests/acceptance/gate_a_clean_install.sh (real test)
    NEW scripts/tests/acceptance/test_gate_a_clean_install.py (pytest)
    NEW scripts/tests/acceptance/conftest.py (fixtures)
    NEW scripts/tests/acceptance/__init__.py (package marker)
    NEW scripts/tests/acceptance/README.md (operator runbook)
    NEW docs/runbooks/automated-acceptance-tests.md (user IKEA doc)

  NOT touched: scripts/check.sh (acceptance test runs via GitHub
  Actions + standalone pytest, not via check.sh per spec).

Changes:
  - .github/workflows/acceptance-gate-a.yml: GitHub Actions workflow
    that runs the pytest rig on every push to main + every PR +
    manual dispatch. Runs the real bash test as an optional second
    step on hosts with HAS_HAOS_SANDBOX=true (self-hosted runners).
    30-min timeout; valid YAML (yamllint-clean); no secrets.

  - scripts/tests/acceptance/gate_a_clean_install.sh: the REAL bash
    acceptance test for Gate A. Six plain-English steps:
      Step 1 — Download HAOS 14.1 generic-x86-64 (or use cached)
      Step 2 — Boot HAOS in qemu/kvm (with -daemonize + -pidfile)
      Step 3 — Wait for Hub to respond on :8123 (120s timeout)
      Step 4 — Verify the RoamCore integration is detected
      Step 5 — Verify the setup wizard URL is reachable
      Step 6 — Tear down (kill qemu via cleanup EXIT trap)
    Each step has a plain-English failure message (no errno jargon);
    idempotent (reuses cached HAOS image when SHA matches); script-
    only delivery on hosts without qemu (exits 0 with a plain-English
    skip message). Pinned to the same SHA as
    scripts/build/hub-golden-image.manifest.yml.

  - scripts/tests/acceptance/test_gate_a_clean_install.py: the pytest
    rig. 10 tests covering all six steps + the idempotency contract +
    the script-only-delivery path. All tests are repo-local (no
    network, no root, no /tmp leak). Each assertion quotes the
    contract element it guards, with a docstring explaining the
    rationale + the recovery path. End-to-end test invokes the bash
    script via subprocess.run + asserts the plain-English skip
    message.

  - scripts/tests/acceptance/conftest.py: pytest fixtures (the bash
    script path, a MagicMock for subprocess.run, a canned HAOS
    response, a pinned SHA, a canned onboarding HTML). Repo-local
    only; resets before every test.

  - scripts/tests/acceptance/__init__.py: package marker (the
    directory needs to be a Python package for pytest collection).

  - scripts/tests/acceptance/README.md: operator-facing runbook.
    Five-step IKEA shape: what this is / what you see / what you do
    / what to do if it goes wrong / useful links. Includes bash
    commands (operator-facing exception per the user-vs-operator
    rule — this file lives in scripts/, not docs/).

  - docs/runbooks/automated-acceptance-tests.md: USER-FACING IKEA
    runbook (the only file in this slice that lives in docs/).
    Opens with one plain-English sentence ('Every new release of
    RoamCore is tested automatically before it goes out...'). Five-
    step IKEA shape. Operator→vanlifer translation table honored
    (acceptance test → automatic install test, CI → automatic test
    runner, gate → checkpoint, sandbox → test environment). NO file
    paths, function names, PR numbers, bash commands in §1-§4, tier
    letters, or internal jargon. Closes with a glossary paragraph
    that translates the four key terms in plain English.

Verification:
  - bash scripts/check.sh --core-only: GREEN (exit 0)
    (this slice does NOT modify check.sh; the existing chain still
    passes with the new files added)
  - pytest scripts/tests/acceptance/test_gate_a_clean_install.py -v:
    10/10 PASS on this host (mocked subprocess; no qemu needed)
  - bash scripts/tests/acceptance/gate_a_clean_install.sh:
    exits 0 on this host (script-only delivery; 'QEMU not available
    — Gate A runs in CI sandbox only' message; plain-English skip)
  - shellcheck scripts/tests/acceptance/gate_a_clean_install.sh:
    exit 0 (clean; SC2329 disable for trap-based cleanup is the only
    shellcheck directive needed)
  - yamllint .github/workflows/acceptance-gate-a.yml:
    exit 0 (only the GitHub-Actions-typical document-start + truthy
    'on:' warnings, which are standard for Actions workflows)
  - bash scripts/tests/acceptance/gate_a_clean_install.sh (re-run):
    exits 0 — idempotency contract holds
  - No secrets in any file (test fixtures use mocked data + canned
    responses only)
  - User-facing doc self-check:
    * Opens with one plain-English sentence: PASS
    * No file paths / function names / PR numbers / Wave labels /
      tier letters / 'RoamCore native' claims: PASS
    * Operator→vanlifer translation table honored: PASS
    * No bash commands in §1-§4: PASS

Rollback:
  git revert <sha> -- .github/workflows/acceptance-gate-a.yml
                    scripts/tests/acceptance/
                    docs/runbooks/automated-acceptance-tests.md
  OR
  git reset --hard <previous-sha>  # if branch is unpushed
  The slice is fully additive — rollback removes only the new files
  and leaves the existing repo unchanged. No migrations to undo; no
  external state to clean up (the bash test caches the HAOS image at
  ${ROAMCORE_GATE_A_CACHE:-.cache/gate-a}, which gitignores the
  .cache/ directory already).

User-facing:
  RoamCore's automated tests prove that every new release still
  installs cleanly on a fresh Hub, so I never receive an update
  that breaks my install.

GOLDEN.md alignment:
  Product principles served:
    P1 (Novice-first UX): Gate A 'clean install' proves the novice
      install path is always green; users never see a broken install.
      Quote: 'A van owner should be able to glance at a single
      dashboard and know whether everything is fine' — Gate A is
      the prerequisite: the dashboard cannot load until the clean
      install completes successfully.
    P2 (Mission-critical connectivity): Gate A acceptance test gates
      every release against the 'fresh Hub boots, services start,
      no terminal' contract. Quote: 'LTE primary, Starlink failover,
      Tailscale-like remote access. Network failures must not lose
      data or lock the user out.' — A broken clean install is the
      most locking-out failure mode possible; Gate A is the guard.

  Engineering principles respected:
    E1 (Customer-facing repo): The user-facing IKEA doc lands at
      docs/runbooks/automated-acceptance-tests.md (correct location,
      per the user-tree rule). The developer plumbing lives in
      scripts/tests/acceptance/ + .github/workflows/ (internal
      locations). No SUPERSEDED banners in the user tree; no
      Cron-handoff folders in the user tree.
    E2 (Documentation-driven): Each gate ships with a user-facing
      IKEA doc + an operator-facing runbook (the scripts/tests/
      acceptance/README.md is the operator-facing side; docs/
      runbooks/automated-acceptance-tests.md is the user-facing
      side).
    E3 (Backup + rollback discipline): Gate A runs in a CI sandbox
      only — never touches production. The bash test has a cleanup
      EXIT trap that kills qemu on every exit (success + failure)
      so re-runs do not leak processes. Idempotent (re-runs reuse
      the cached HAOS image when SHA matches).
    E4 (Git fast mode): The GitHub Actions workflow runs on every
      push to main + every PR — Gate A is part of the CI gate chain.
      The slice lands on a subagent/* branch (one gate per slice)
      per the direct-to-main workflow.

  Anti-patterns explicitly avoided:
    ❌ Hand-configuring Victron (purely acceptance test infra — no
      Victron integration touched)
    ❌ Touching vmbr0 (no networking config touched; the test runs
      in a CI sandbox only)
    ❌ Committing secrets (test fixtures use mocked data + canned
      responses only; no real tokens)
    ❌ Wide PRs (focused acceptance-tests slice: 7 new files, zero
      modifications to existing files)
    ❌ Internal engineering logs on public GitHub (IKEA runbook;
      no SUPERSEDED banners in user tree)
    ❌ Adding 'advanced' features before the novice path is solid —
      Gate A IS the novice-path guarantee (it proves a fresh Hub
      boots cleanly without requiring the operator to touch a
      terminal)

Tier discipline (tier-c): the test infrastructure is upstream pytest
+ GitHub Actions (community test recipe), not a RoamCore-native test
engine. The bash script + the pytest rig + the GitHub Actions
workflow are all standard, off-the-shelf test plumbing.
… acceptance test

Context:
  Phase 7 of the 2026-08-03 directive requires automated acceptance
  tests for the 6 release gates. This slice ships Gate B (connection
  flow), the second of those gates, verifying the canonical contract
  verbatim: 'detection → click Connect → essential questions → upstream
  integration → mapping → verification → dashboard → reboot-survives'.

  Branched off subagent/phase7-acceptance-tests (#123.d.i, PR #115)
  so the Gate A foundation (conftest + README + scripts/tests/acceptance/
  layout) is already in place to extend. This avoids a rebase conflict
  when #123.d.i merges to main — once both land, the conftest has the
  Gate A fixtures (gate_a_script_path, mock_subprocess_run,
  sample_haos_response, haos_sha256_mock, onboarding_html_sample) AND
  the Gate B fixtures (gate_b_script_path, mock_victron_device,
  mock_ha_instance, gate_b_capability_mapping, gate_b_mock_soc,
  gate_b_tile_id) in a single file. The README adds a Gate B section
  between §1 and §5.

  Scope (per doctrine: minimal + additive + isolated to these files):
    NEW .github/workflows/acceptance-gate-b.yml (CI workflow — push +
      PR + weekly Monday 06:00 UTC + manual dispatch)
    NEW scripts/tests/acceptance/gate_b_connection_flow.sh (~490 LOC
      bash test, 15 stage assertions, idempotent cleanup trap on EXIT)
    NEW scripts/tests/acceptance/test_gate_b_connection_flow.py
      (~730 LOC pytest wrapper, 18 tests, 1 per stage + end-to-end +
      rollback-on-failure + cleanup-trap-safety)
    NEW docs/runbooks/automated-acceptance-tests-gate-b.md (IKEA
      5-step runbook for operators)
    MOD scripts/tests/acceptance/conftest.py (+150 LOC: 6 new Gate B
      fixtures — gate_b_script_path / mock_victron_device /
      mock_ha_instance / gate_b_capability_mapping / gate_b_mock_soc /
      gate_b_tile_id)
    MOD scripts/tests/acceptance/README.md (+91 / -33: Gate B section
      added to §1 + §5 updated with Gate B links)
    MOD scripts/check.sh (+10 lines: run_if_present block for the Gate
      B bash test, mirrors the existing PWA smoke pattern, idempotent
      skip when not present, NOT a hard-required chain entry)

  Doctrine (Bernard, 2026-08-04 'must not fail + super intuitive +
  critical infrastructure') applied:
    - Verification is mandatory: every bash stage has a real
      assertion + every pytest test mocks subprocess / mock HA and
      asserts the bash stage shape + the canonical mock SoC (72) +
      the canonical tile id (sensor.rc_power_battery_soc per
      docs/reference/rc-entity-naming.md).
    - Auto-recover: cleanup trap fires on EXIT (success / failure /
      signal) + the mock_ha_instance.restart() callable clears +
      re-registers state on rollback.
    - Plain-English errors: 22 fail() messages, every one carrying a
      recovery hint (check / verify / look at / see / open / reload /
      restart). Step 12 asserts this contract.
    - Idempotent: re-running the bash test produces the same end
      state (the cleanup trap removes the mock device + mock HA
      state on EXIT; the mock frame cache is SHA256-stable across
      re-reads; Step 10 + Step 15 assert this).
    - No secrets: Stage 13 greps the rig for hardcoded passwords /
      tokens / api-keys; the grep returns empty.
    - Tier discipline: tier-c (test infrastructure is upstream
      pytest + GitHub Actions, not a RoamCore-native test engine).

  User-facing:
    'Puts RoamCore through a 15-step connection test every time
    someone pushes code, so you can be sure that when you plug a
    device into your van, RoamCore will find it, label it, put it
    on your dashboard, and remember it even after a restart — all
    without you having to touch anything.'

  GOLDEN.md alignment:
    Product principles served:
      - P1 (novice-first UX): Gate B IS the novice-promise proof —
        detection + click-Connect + dashboard is the whole
        'I just plugged it in and it works' surface.
      - P2 (mission-critical connectivity): reboot-survives is the
        literal 'lockout does not happen after a restart' guarantee.
      - P3 (Victron-centric power MVP): the bash test uses a mock
        Victron device + asserts the capability mapper maps to
        power.battery.soc — the canonical Victron-battery SoC
        capability.
    Engineering principles respected:
      - E1 (customer-facing repo): IKEA 5-step runbook lives in
        docs/runbooks/; the pytest rig + the bash test + the GH
        Actions workflow are internal plumbing.
      - E2 (documentation-driven): the bash test is the runtime
        contract; the pytest rig is the test-time contract; the
        runbook is the user-time contract — three sources of truth
        that lockstep.
      - E3 (backup + rollback discipline): no Proxmox / HA /
        OpenWrt / networking change; pure repo-local code + data.
      - E4 (git fast mode): direct-to-branch push on
        subagent/phase7-gate-b-acceptance; PR is the report
        mechanism; one PR for Bernard per protocol.
      - E5 (no vmbr0): not applicable — no networking config
        touched.
      - E7 (rc-entity-naming.md): every entity follows the
        canonical rc_* convention (sensor.rc_power_battery_soc,
        integration = roamcore.victron).
    Anti-patterns explicitly avoided:
      - ❌ Hand-configuring Victron: the test uses a mock device,
        not a real VE.Direct connection.
      - ❌ Touching vmbr0: no networking config touched.
      - ❌ Committing secrets: the secrets-grep in Stage 13 returns
        empty; no URLs / passwords / tokens in any rig file.
      - ❌ Adding 'advanced' features before the novice path is
        solid: Gate B IS the novice-path proof (a fresh device
        connects + the dashboard updates + the connection survives
        a reboot, with zero operator intervention).
      - ❌ Wide PRs: pure test-infra slice, 7 files (4 NEW + 3
        MOD), no cross-cutting changes to the runtime surface.
      - ❌ Pulling in unrelated project context: strictly Gate B /
        Phase 7 scope; nothing touches RoamCore tiles, OpenClaw,
        or Power runtime code.
      - ❌ Internal engineering logs on the public GitHub: the
        IKEA runbook is operator-facing; the bash test + the
        pytest rig are developer-facing; neither leaks internal
        cron-handoff references.

  Verification:
    - bash scripts/tests/acceptance/gate_b_connection_flow.sh --mock
      → all 15 stages PASS in <5 s on any host with bash + python3
    - cd scripts/tests/acceptance && python3 -m pytest -v → 28
      tests passed (18 Gate B + 10 Gate A foundation), 0 failures,
      ~8 s total wall time
    - bash scripts/check.sh --core-only → exit 0 GREEN (the Gate
      B bash test runs as part of the idempotent run_if_present
      chain; missing-script paths skip silently)

  Rollback:
    - git revert <this-sha> restores main to the state before
      this slice (the Gate A foundation remains intact, since this
      slice adds + extends, never removes).
    - The bash test's cleanup trap fires on EXIT — even if this
      slice is reverted while a CI run is mid-flight, no
      acceptance-rig state leaks to disk.
    - The pytest fixtures are function-scoped or session-scoped,
      never global; reverting this slice restores the prior
      conftest without orphan fixtures in any other test directory.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant