Skip to content

ci: tiered integration testing — per-PR mock tier + nightly device tier [QA proposal]#1149

Draft
DevenDucommun wants to merge 2 commits into
mainfrom
qa/ci-integration-tiers
Draft

ci: tiered integration testing — per-PR mock tier + nightly device tier [QA proposal]#1149
DevenDucommun wants to merge 2 commits into
mainfrom
qa/ci-integration-tiers

Conversation

@DevenDucommun

Copy link
Copy Markdown

Summary

QA proposal to get the existing integration_test/ suite (21 flows) running automatically instead of manually. It splits the flows into two tiers:

  • Mock tier (13 flows): UI + JNAP-contract flows that keep nearly all their regression value with mocked JNAP. New workflow runs them headless on every PR that touches lib/, integration_test/, or pubspec.yaml (1.x line branches only).
  • Device tier (8 flows): flows whose value depends on real device behavior (PnP state machine, radio apply, WAN transitions, speed test). New nightly workflow runs them on the QA lab router through the repo's own integration_web.sh harness.

Full classification with per-flow reasoning: integration_test/FLOW-TIERS.md.

What this PR contains

  • .github/workflows/ci-integration-mock.yml (per-PR, ubuntu, headless Chrome)
  • .github/workflows/ci-integration-device.yml (nightly cron at 02:30 Taipei, self-hosted lab-lan runner)
  • integration_test/FLOW-TIERS.md (the classification + open items)

Status / what we need from you

This is a draft for async review and discussion. Nothing runs until it lands and prerequisites are met:

  1. Mock flag: the mock workflow assumes --dart-define=FORCE_COMMAND_TYPE=local activates the mocked JNAP layer the demo builds use. Please correct if wrong. This is the main thing blocking the mock tier.
  2. Classification sanity-check: two rows flagged in FLOW-TIERS.md (dashboard_home_test, local_network_settings_test).
  3. Device runner: QA is provisioning a self-hosted runner (Jetson) on the lab LAN with label lab-lan; the device workflow stays dormant until it's registered and the ROUTER_ADMIN_PASSWORD secret is set. Expected when Deven is back in the lab.
  4. tier field in test_meta (proposal, follow-up PR): "tier": "mock" | "device" so selection is data-driven instead of the hardcoded matrix.
  5. 2.x line question: integration_test/ only exists on main/1.x. Worth porting the mock tier to usp?

Context: QA validated the enforcement side of this suite live on the lab M62DU-EU (FW 1.0.12) this week; a Playwright browser-layer smoke suite (login + CPE-SEC-15 sweep) also went green against the same unit and will follow once the runner exists.

🤖 Generated with Claude Code

Splits the 21 integration flows into a mockable per-PR tier (13 flows,
headless, no router) and a device-bound nightly tier (8 flows, lab
router via integration_web.sh). Classification and rationale in
integration_test/FLOW-TIERS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated Review — Round 1 · 61bef92..ec12768 (full)

Verdict: 💬 COMMENT — 1 Critical finding (--dart-define key mismatch renders mock tier permanently inoperative); manual decision required before merge.

Conf. Where Issue (one-liner)
🔴 🟢 High ci-integration-mock.yml:71 [both reviewers] --dart-define=FORCE_COMMAND_TYPE=local does not match app's force key — mock JNAP layer never activates
⚠️ 🟢 High ci-integration-mock.yml:64 assets/agents/env.template missing from git — Setup Environment step fails, all 13 mock flows break immediately
⚠️ 🟢 High ci-integration-device.yml (setup steps) device workflow missing SSH key setup (UI_KIT_DEPLOY_KEY) — flutter pub get will fail if private deps present
⚠️ 🟢 High ci-integration-device.yml (setup steps) device workflow missing .env init step present in mock workflow and generate-screenshots.yml
⚠️ 🟢 High ci-integration-device.yml:55 inputs.tags directly interpolated into shell run: — injection surface on self-hosted runner
⚠️ 🟢 High ci-integration-device.yml:50-51 ROUTER_ADMIN_PASSWORD set as env but never consumed by integration_web.sh — device flows silently fail with wrong password
⚠️ 🟢 High ci-integration-mock.yml:65-77 mock workflow calls flutter drive directly, silently skips test_meta setUp/tearDown hooks declared in all 13 flow JSONs
⚠️ 🟢 High integration_test/FLOW-TIERS.md menu tag spans both tiers — integration_web.sh -t menu will mix mock and device-bound flows
⚠️ 🟢 High integration_test/FLOW-TIERS.md:28 dashboard_home_test mockable rationale ("speed-test card stubbed") contradicts test_meta description which includes Speed Test interaction
💡 🟢 High both new workflows Missing permissions: blocks — workflows run with default (potentially over-broad) GITHUB_TOKEN
💡 🟢 High ci-integration-mock.yml:63-64 chromedriver backgrounded with & without readiness check — flutter drive may connect before chromedriver is ready
💡 🟢 High ci-integration-device.yml:41 curl -sk skips TLS validation; acceptable on isolated lab LAN but should be documented explicitly
💡 🟢 High ci-integration-device.yml:18,51 Hardcoded tag list will drift from tier classification once flows are added; propose adopting tier field (Open item #3) for selection
💡 🟡 Med integration_test/FLOW-TIERS.md:35 local_network_settings_test rationale should clarify validation boundary (does mock verify post-re-IP reachability?)
💡 🟢 High integration_test/shell_scripts/jnap.sh:1 Pre-existing shebang typo #!/uer/bin/bash — opportunity to fix while touching this area

Confidence: 🟢High = code-verified · 🟡Med = located + reasoned, not fully confirmed · ⚪Low = speculative, please double-check.
Items marked [both reviewers] were independently flagged by two agents → higher confidence.

🔴 Critical — Complete Evidence Chain

C-1 · --dart-define Key Mismatch — Mock JNAP Layer Never Activates

Severity: Critical | Confidence: High | Both reviewers independently confirmed

Location chain:

  • .github/workflows/ci-integration-mock.yml:71 -- wrong key injected
  • lib/constants/build_config.dart:44-45 -- actual key the app reads
  • integration_web.sh:219 -- correct existing usage

(1) Code in the new workflow (ci-integration-mock.yml:71):

--dart-define=FORCE_COMMAND_TYPE=local \

(2) Code the app actually reads (build_config.dart:44-45):

static ForceCommand forceCommandType = ForceCommand.reslove(
    const String.fromEnvironment('force', defaultValue: 'none'));

The app reads key force, not FORCE_COMMAND_TYPE.

(3) Correct usage in the existing harness (integration_web.sh:219):

--dart-define=force="${force}" \     # force defaults to "local" (line 29)

Why it breaks: String.fromEnvironment('force', ...) receives no matching define -> returns 'none' -> ForceCommand.reslove('none') -> ForceCommand.none -> BuildConfig.isLocal() returns false -> JNAP calls are not intercepted -> all 13 mock-tier flows attempt to connect to a real JNAP endpoint (https://localhost/JNAP/) that does not exist on ubuntu-latest -> all flows fail with network errors rather than valid mock results.

Trigger condition: Every PR that touches lib/**, integration_test/**, or pubspec.yaml triggers ci-integration-mock.yml. All 13 matrix jobs will fail from day one.

Fix:

# ci-integration-mock.yml line 71
--dart-define=force=local \

Also update FLOW-TIERS.md Open item #1 with the confirmed correct flag name.

⚠️ Warnings — Evidence and Fixes

W-1 · assets/agents/env.template Missing from Git — Mock CI Breaks Immediately

Confidence: High | [single - Reviewer A]

Location: ci-integration-mock.yml:64

- name: Setup Environment
  run: cp assets/agents/env.template assets/agents/.env

git ls-files assets/agents/ returns empty. GitHub Actions run: steps default to set -eo pipefail; cp failure (exit 1) terminates the step -> all 13 matrix jobs fail at Setup Environment before any test runs.

Fix: Either commit a sanitized assets/agents/env.template to the repo, or add a guard:

run: |
  mkdir -p assets/agents
  [ -f assets/agents/env.template ] && cp assets/agents/env.template assets/agents/.env || touch assets/agents/.env

W-2 · Device Workflow Missing SSH Key Setup — flutter pub get Fails

Confidence: High | [single - Reviewer B]

Location: ci-integration-device.yml (between checkout and flutter pub get)

ci-integration-mock.yml:49-56 and generate-screenshots.yml:137-143 both include the webfactory/ssh-agent@v0.9.0 step with UI_KIT_DEPLOY_KEY. ci-integration-device.yml does not. If pubspec.yaml references private git dependencies (e.g. ui_kit_library), flutter pub get on the device runner will fail with SSH auth errors.

Fix: Add the same two SSH setup steps before Install Dependencies in ci-integration-device.yml.


W-3 · Device Workflow Missing .env Init Step

Confidence: High | [single - Reviewer B]

Location: ci-integration-device.yml (setup steps)

Both ci-integration-mock.yml:62 and generate-screenshots.yml:146 include the cp assets/agents/env.template assets/agents/.env step. ci-integration-device.yml has no equivalent. integration_web.sh calls init_config / set_global_config which may depend on .env being present.

Fix: Add the same Setup Environment step in ci-integration-device.yml.


W-4 · inputs.tags Directly Interpolated into Shell — Injection Surface

Confidence: High | [single - Reviewer A]

Location: ci-integration-device.yml:55

run: |
  bash integration_web.sh -t "${{ inputs.tags || 'pnp,wifi,internet_settings,speed_test' }}"

${{ inputs.tags }} is a free-text workflow_dispatch input interpolated directly into a run: shell block. GitHub's own hardening guide explicitly prohibits this pattern. On a self-hosted runner with lab LAN access (192.168.1.1 reachable), the attack surface is meaningful for anyone with repo write access.

Fix:

env:
  TAGS_INPUT: ${{ inputs.tags || 'pnp,wifi,internet_settings,speed_test' }}
run: |
  bash integration_web.sh -t "$TAGS_INPUT"

W-5 · ROUTER_ADMIN_PASSWORD Never Consumed — Device Flows Silently Fail

Confidence: High | [single - Reviewer A]

Location: ci-integration-device.yml:50-51

env:
  ROUTER_ADMIN_PASSWORD: ${{ secrets.ROUTER_ADMIN_PASSWORD }}

Searched all .sh and .dart files — zero references to $ROUTER_ADMIN_PASSWORD. The harness reads the admin password via is_default_admin_password_or_get_password() in utils.sh:190-195, which returns "admin" or a config JSON value — never the environment secret. If the lab router password is not the default "admin", all JNAP calls return 401 and flows fail silently.

Fix: Thread the secret into the harness via a --admin-password "$ROUTER_ADMIN_PASSWORD" argument to integration_web.sh, or inject it into the config JSON before running.


W-6 · Mock Workflow Directly Calls flutter drive — test_meta setUp Hooks Silently Skipped

Confidence: High | [single - Reviewer B]

Location: ci-integration-mock.yml:65-77

All 13 mockable flows' test_meta/*.json files declare setUp hooks (e.g. "setUp": ["factory_reset", "skip_setup"]). integration_web.sh:193-213 executes these hooks before each case. The mock workflow calls flutter drive directly, bypassing the harness entirely. If any test's Dart code assumes initial state prepared by setUp, it will fail or produce invalid results without explanation.

Fix: Document in FLOW-TIERS.md that mock-tier flows must be self-contained (no setUp pre-state assumed), or add a no-op mock setUp path in integration_web.sh.


W-7 · menu Tag Spans Both Tiers — integration_web.sh -t menu Mixes Flows

Confidence: High | [single - Reviewer B]

Location: integration_test/FLOW-TIERS.md (classification table)

menu_test.json has "tags": ["menu"] (mockable); incredible_wifi_test.json, speed_test_test.json, and incredible_wifi_advanced_and_mac_filtering_test.json also have the menu tag but are device-bound. integration_web.sh:57-67 does pure string tag matching with no tier filter.

Fix: Implement Open item #3 ("tier" field in test_meta JSONs + -r flag in harness), or add a warning in FLOW-TIERS.md about cross-tier tag overlap.


W-8 · dashboard_home_test Mockable Rationale Contradicts test_meta

Confidence: High | [single - Reviewer B]

Location: integration_test/FLOW-TIERS.md:28

FLOW-TIERS.md: "speed-test card stubbed" vs test_meta description: "Test... including Devices, Privacy, Speed Test, and Wi-Fi". If dashboard_home_test triggers a real speed-test measurement request, mock-tier runner will have no device to answer it.

Fix: Resolve Open item #2 — confirm whether the test merely renders the Speed Test card UI or triggers a measurement, and update the rationale.

✅ What looks good
  • concurrency.cancel-in-progress: false on device workflow correctly prevents mid-run factory-reset interruption.
  • concurrency group includes github.ref on mock workflow — per-branch isolation, stale runs cancelled on new push.
  • strategy.fail-fast: false on mock matrix — a single flow failure does not abort the other 12.
  • timeout-minutes: 240 on device workflow — realistic for PnP + factory-reset cycle.
  • upload-artifact with if: always() on both workflows — failure artifacts preserved for investigation.
  • matrix.flow list is static YAML — no runtime user input interpolated into flow names, no injection risk there.
  • max-parallel: 4 cap — friendly to shared runner pool.
  • retention-days tiered correctly: 7d for mock (frequent), 30d for device nightly (less frequent, higher debug value).
  • FLOW-TIERS.md Open items are honest and specific — author proactively identified unresolved questions.

Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.

@AustinChangLinksys AustinChangLinksys added the need-decide Automated review left findings needing Austin to decide (fix/defer/dismiss). label Jul 16, 2026
- mock: --dart-define=force=local (C-1, matches build_config.dart 'force' key
  and integration_web.sh:219), env.template guard (W-1), chromedriver
  readiness wait, permissions block
- device: SSH-agent + env setup steps (W-2/W-3), tags via env var not
  inline interpolation (W-4), permissions block, TLS + password notes
- FLOW-TIERS: open item 1 resolved, mock-tier self-containment caveat
  (W-6), cross-tier tag warning (W-7), dashboard_home rationale honesty
  (W-8), W-5 password threading recorded as open item

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DevenDucommun

Copy link
Copy Markdown
Author

Thanks for the fast round 1, this was exactly the review the draft needed. Pushed 4e13f92 addressing it:

Fixed

  • C-1: --dart-define=force=local (and open item 1 in FLOW-TIERS.md marked resolved with the build_config.dart reference). Good catch from both agents.
  • W-1: env.template fallback guard in both workflows.
  • W-2 / W-3: device workflow now has the SSH-agent and Setup Environment steps, matching the mock workflow.
  • W-4: inputs.tags goes through an env var now, no inline interpolation.
  • Suggestions: permissions: contents: read on both workflows, chromedriver readiness wait before flutter drive, -k TLS note documented at the preflight step.

Documented as open rather than fixed

  • W-5: password threading recorded as open item 6 in FLOW-TIERS.md. The lab device returns to its label-default password on every factory reset, so the simplest contract may be "device tier assumes default password"; want your take before touching the harness.
  • W-6: mock-tier self-containment caveat added to FLOW-TIERS.md. Whether all 13 flows really are self-contained without their setUp hooks is a first-run validation item.
  • W-7 / W-8: cross-tier tag warning added, and the dashboard_home row now carries the Speed Test contradiction inline instead of pretending it is settled.
  • jnap.sh shebang typo: left out of this PR to keep the diff scoped to CI; happy to fix in the tier-field follow-up.

Still a draft until W-5/W-6 have answers and the lab runner exists (Jetson provisioning is planned for next week).

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated Review — Round 2 · 61bef92..4e13f92 (full)

Verdict: ✅ APPROVE — Prior Critical (C-1, --dart-define key mismatch) is correctly fixed; no new Critical findings. 4 Warnings remain (chromedriver silent-failure gap, duplicated bootstrap, unpinned action, deferred password threading).

Conf. Where Issue (one-liner)
⚠️ 🟢High ci-integration-mock.yml:60-65 Chromedriver readiness loop exits code 0 even if chromedriver never starts — flutter drive gets cryptic connect error
⚠️ 🟢High Both workflows, bootstrap steps 5-step setup sequence duplicated verbatim — DRY violation, version-drift risk across workflows
⚠️ 🟢High ci-integration-mock.yml (Setup Chrome step) browser-actions/setup-chrome@v1 pinned at major tag only — no SHA pin on ubuntu-latest supply-chain boundary
⚠️ 🟢High ci-integration-device.yml:63 W-5 (acknowledged deferred): ROUTER_ADMIN_PASSWORD exported but never read by harness — silent wrong-password on non-default lab router
⚠️ 🟡Med ci-integration-mock.yml:72 Static matrix.flow path pattern safe today; latent traversal risk if matrix ever made dynamic — add safety comment
💡 🟢High Both workflows, long-running steps [both reviewers] No per-step timeout-minutesconcurrency.cancel-in-progress: false + 4h job ceiling = runner monopolized on hang
💡 🟡Med ci-integration-device.yml, device-flows job No environment: protection gate — ROUTER_ADMIN_PASSWORD + UI_KIT_DEPLOY_KEY exposed without approval gate
💡 🟡Med ci-integration-mock.yml, Upload step Artifact content from build/integration_test/ undocumented — confirm mock runs write no sensitive data
💡 🟢High ci-integration-mock.yml:40 max-parallel: 4 has no rationale comment (runner quota cap? cost? measured optimum?)
💡 🟡Med integration_test/FLOW-TIERS.md, Open items Open items 2–6 have no owner/milestone — consider converting to GitHub Issues to avoid tracking drift
💡 ⚪Low ci-integration-mock.yml:10-14 pubspec.lock absent from paths: trigger — lockfile-only dep upgrade won't trigger mock CI

Confidence: 🟢High = code-verified · 🟡Med = located + reasoned, not fully confirmed · ⚪Low = speculative, please double-check.
Items marked [both reviewers] were independently flagged by two agents → higher confidence.

⚠️ Warnings — Evidence and Fixes

W-new-1 · Chromedriver Readiness Loop Exits Code 0 on Failure

Confidence: High | [single — Reviewer B]

Location: ci-integration-mock.yml:60-65 (Start chromedriver step)

chromedriver --port=4444 &
for i in $(seq 1 30); do
  curl -sf http://127.0.0.1:4444/status > /dev/null && break
  sleep 1
done

The for loop exits with the exit code of the last command executed. If break was never hit (chromedriver never became ready), the last command is sleep 1, which exits 0. The step succeeds silently and the subsequent flutter drive step fails with a cryptic "cannot connect to driver" error rather than an actionable "chromedriver not ready" diagnostic.

Note: Reviewer A accepted the loop addition as sufficient fix from Round 1. Reviewer B's analysis of bash exit-code semantics is technically correct — the fix is incomplete.

Fix:

chromedriver --port=4444 &
READY=0
for i in $(seq 1 30); do
  curl -sf http://127.0.0.1:4444/status > /dev/null && READY=1 && break
  sleep 1
done
if [ "$READY" -ne 1 ]; then
  echo "::error::chromedriver did not become ready on port 4444 within 30 seconds"
  exit 1
fi

W-new-2 · 5-Step Bootstrap Sequence Duplicated Verbatim Across Both Workflows

Confidence: High | [single — Reviewer B]

Location: Both ci-integration-mock.yml and ci-integration-device.yml, steps 1–5

The following steps are copy-pasted identically:

  1. actions/checkout@v4
  2. Setup Flutter (subosito/flutter-action@v2, channel: stable, cache: true)
  3. Setup SSH for Private Deps (webfactory/ssh-agent@v0.9.0)
  4. Configure Git SSH (git config --global url rewrite)
  5. Setup Environment (mkdir + cp/touch .env)

This is ~25 lines of duplicated YAML. When the Flutter channel bumps, the SSH agent action updates, or the .env path changes, both files must be updated in sync. FLOW-TIERS.md open item 5 (2.x porting) would duplicate this block a third time.

Fix: Extract into .github/actions/setup-flutter-env/action.yml composite action, or a reusable workflow. Both integration workflows then call uses: ./.github/actions/setup-flutter-env.


W-new-3 · browser-actions/setup-chrome@v1 Pinned Only at Major Version

Confidence: High | [single — Reviewer B]

Location: ci-integration-mock.yml (Setup Chrome + chromedriver step)

- name: Setup Chrome + chromedriver
  uses: browser-actions/setup-chrome@v1
  with:
    install-chromedriver: true

@v1 is the least-specific pin in both workflows (others use @v4, @v2, @v0.9.0). This action installs binaries onto ubuntu-latest; a supply-chain compromise or breaking change pushes transparently to all mock-tier PR runs. GitHub's own hardening guide and OSSF Scorecard flag this pattern.

Fix: Pin to the current commit SHA with a version comment:

uses: browser-actions/setup-chrome@<40-char-SHA>  # v1.x.x

W-5 (Deferred/Acknowledged) · ROUTER_ADMIN_PASSWORD Exported but Harness Never Reads It

Confidence: High | [single — Reviewer A] — acknowledged in FLOW-TIERS.md open item 6

Location: ci-integration-device.yml:63 (env block in "Run device-bound flows" step) + FLOW-TIERS.md open item 6

The secret is correctly exported as an env var but integration_web.sh reads the admin password via is_default_admin_password_or_get_password() (config JSON or label-default), never $ROUTER_ADMIN_PASSWORD. FLOW-TIERS.md open item 6 explicitly tracks this: "Before the first live nightly run, thread the secret into the harness config."

Acceptable for now — factory resets restore the default password. Risk: if the lab router is ever hardened with a non-default password, nightly runs fail with silent 401s.

Fix (before first live run): Pass --admin-password "$ROUTER_ADMIN_PASSWORD" to integration_web.sh, or add a pre-flight credential-validation step.


W-new-4 · Static matrix.flow Path Pattern — Latent Traversal Risk

Confidence: Medium | [single — Reviewer A]

Location: ci-integration-mock.yml:72 (flutter drive --target line)

--target=integration_test/${{ matrix.flow }}.dart \

Matrix is currently a static hardcoded YAML allowlist — current risk is low. If the matrix is ever made dynamic (e.g., via fromJson reading FLOW-TIERS.md), a crafted entry like ../../../etc/passwd would produce --target=integration_test/../../../etc/passwd.dart, enabling path traversal.

Fix (minimal): Add a safety comment:

# SECURITY: matrix.flow is a static allowlist. Do NOT make this dynamic
# (e.g. via fromJson) without adding path-sanitization or allowlist validation.
✅ What looks good
  • C-1 RESOLVED: --dart-define=force=local now matches the key lib/constants/build_config.dart reads — mock JNAP layer activates correctly.
  • W-1 RESOLVED: Both workflows guard .env init with if [ -f env.template ]; then cp … else touch …; fi — graceful fallback when template not in git.
  • W-2 RESOLVED: Device workflow now includes webfactory/ssh-agent@v0.9.0 + Configure Git SSH — private deps will resolve on lab runner.
  • W-3 RESOLVED: Device workflow has a Setup Environment step matching the mock workflow.
  • W-4 RESOLVED: inputs.tags threaded via TAGS_INPUT env var — no inline ${{ }} interpolation inside shell run: block.
  • Permissions RESOLVED: Both workflows declare permissions: contents: read — GITHUB_TOKEN scoped to minimum.
  • chromedriver readiness PARTIALLY ADDRESSED: Polling loop added (see W-new-1 for the remaining gap).
  • TLS skip documented: curl -sk comment explains self-signed cert on isolated lab LAN.
  • W-6/W-7/W-8: All three acknowledged with clear caveats in both FLOW-TIERS.md and workflow inline comments — honest about architectural limitations.
  • concurrency.cancel-in-progress: false on device workflow correctly prevents mid-factory-reset interruption.
  • strategy.fail-fast: false on mock matrix — one flow failure does not abort the other 12.
  • Static matrix list has no user-input interpolation for flow names.
  • Artifact retention-days tiered correctly: 7d mock (frequent), 30d device nightly (higher debug value).
  • FLOW-TIERS.md open items are specific, honest, and forward-looking.

Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator

Thanks for picking this up, Deven. Worth some context up front: we actually ran this suite against real hardware in an automated setup for the better part of a month a while back. We stopped not because the flows were wrong, but because of stability — the device-tier reality of flaky runs and unreliable device initialization/reset made the results untrustworthy enough that the signal wasn't worth it. So the value of this PR isn't "finally automating it" — it's the tiering idea: pulling the mockable UI/contract flows out from under that device-stability problem so they can run reliably without a router. That framing is exactly right, and FLOW-TIERS.md is the most valuable thing here — the 13/8 split and the per-flow reasoning are worth keeping regardless of what happens to the workflows.

That said, I'd like us to treat this as a design proposal to align on, not a change to merge yet — and before we do, I want to pin down a few things about the suite itself, because they change what these workflows should look like. (The bot's APPROVE is on YAML quality — key, injection, permissions — it can't speak to whether the suite actually runs green, which is the real gate.)

1. What integration_test/ actually is (positioning)

These flows are grey-box, in-process E2E: they're compiled into the app binary and import app internals directly (main.dart, di.dart, and in places internal providers/widgets like static_routing_state.dart). Two consequences we should design around:

  • Only the device tier is true E2E. It goes tap → JNAP → real firmware → real radio. The mock tier (force=local) stops at the JNAP contract boundary — it's a UI/contract integration test, not E2E. Same file, different meaning depending on tier. That's fine, but we should name it that way.
  • The suite is version-locked to app source. Because of the white-box imports, the tests can't run against a source tree they weren't compiled with. This is why they live in this repo and travel with the code — and why a separate test repo isn't the right shape for this layer. The black-box layer (Playwright) is the piece that can live in a separate repo and test a released build without source. That's roughly the line we're leaning toward for 2.x (see item on usp below).

2. What we're testing (goal)

There are two orthogonal goals hiding in one PR:

  • Gate the working branch (per-PR, mock) — "did this diff break existing behavior?" Version = branch HEAD.
  • Validate a released/stable build (device) — "does release-1.x.y × firmware × model behave correctly?" Version = a pinned tag, and firmware/model become first-class matrix dimensions.

Right now the device tier is bound to a nightly cron on HEAD, which reads like an extension of goal 1. If the intent is goal 2 (validating releases), it should trigger on a release tag / dispatch with a ref, and we should surface the firmware × model matrix (this also connects to open item 4 on model genericity). Worth deciding which goal it serves.

Trigger cost — should the full suite gate every PR? The mock tier currently fires on every PR touching lib/** and runs all 13 flows, each doing its own Flutter web build (max-parallel: 4). That's tens of minutes of wall-clock on a blocking check, on a layer that's inherently flakier than unit tests — which is exactly what sank the earlier month-long real-hardware run, and it tends to breed a "just re-run it" culture. Worth asking whether the full suite needs to be a per-PR blocking gate at all. Options: run the full 13 post-merge + nightly (regressions caught within a merge cycle, PRs stay fast); keep it per-PR but non-blocking; or gate PRs on a small smoke subset and run the full set post-merge. Gating the working branch (goal 1) doesn't require every flow on every PR — that ties back to the two-goals split above.

3. Constraints / what actually blocks each tier

  • Mock tier is one step away, but the real gate isn't code review — it's a first run of the 13 flows without their test_meta setUp hooks (W-6). Nobody has run this yet; if any flow assumes state that setUp used to prepare, it'll fail. Let's do one manual run before trusting the row.
  • Device tier — missing -d <config> (not in the open items): it calls integration_web.sh -t "$TAGS_INPUT" with no -d <config>, so init_config runs jq on an empty path and the harness exit 1s before any flow runs. This alone means the device workflow cannot succeed as written.
  • Device tier — other blockers: the known W-5 (password never threaded into the harness) and the runner not existing yet. Combined with the -d issue above, this tier needs real work before a first live run — and given the stability history, it deserves that care.

Suggestion: split this into two PRs. The mock tier can land on its own once we've done the first-run validation and get real per-PR value. The device tier should wait for the runner, the -d/W-5 fixes, and a decision on goal 2 vs nightly-HEAD — no reason to couple its fate to the mock tier's.

On the open questions:

  • Rows flagged in open item 2 — checked both:
    • dashboard_home_test is not a clean mock candidate. It calls startSpeedTest() + checkSpeedTestResult() (dashboard_home_test.dart:72-73) — a real measurement, not just rendering the card. Either skip the speed-test segment in the mock tier or stub a speed-test response; as-is it doesn't belong in the 13.
    • local_network_settings_test is fine to mock. It only does form input + field verification (inputIPAddress/verifyIPAddress), no post-re-IP reachability assertion — W-8's concern doesn't apply. Confirmed for the mock tier.
  • tier field (item 3): agree, do it as the follow-up — it's the real fix for the cross-tier tag foot-gun (W-7), better than the hardcoded matrix.
  • 2.x / usp (item 5): speaking as devs, we're currently leaning toward not porting integration_test/ to 2.x, and covering 2.x with screenshot testing + Playwright E2E instead (linksys/PrivacyGUI-USP-E2E, currently on hold). Nothing's decided though — this is a direction, not a ruling, and we'd genuinely welcome other ways to round out the testing chain. QA and dev look at coverage from different angles, and that difference is useful; if there's a case for bringing part of this suite to 2.x, let's hear it. The reason we lean the way we do is the positioning above: the in-process suite is bound to 1.x source, whereas the black-box layer gives 2.x coverage without needing the source tree. Worth a proper conversation rather than settling it in this PR.

Really appreciate the work — this moved the suite forward. Let's lock the positioning and split the tiers, and the mock half can start earning its keep quickly.

@DevenDucommun

Copy link
Copy Markdown
Author

Austin, this is a great review and it sent me back to the source with fresh eyes. A couple of things changed once I actually reproduced the state, and some of them are corrections to my own PR, so let me lay it out straight.

The honest headline: the mock tier does not work yet, and not for the reasons my FLOW-TIERS notes implied. I had been treating force=local as a mocked JNAP layer. It isn't. force=local makes getLocalIp return window.location.host and the repository sends real HTTP to https://<host>/JNAP/ (web_get_local_ip.dart, and the checkForce path in router_repository.dart). There is no stub, proxy, fixture server, or provider override wired anywhere in integration_test/. So the per-PR job starts Chrome and the web server, but nothing answers JNAP. The tier split is a sound coverage idea, and I agree the classification is the most valuable part of this, but a runnable mock tier needs an explicit contract double first, either a fixture HTTP server or dependency/provider overrides with stateful Set/Get. The double is the gate before we can validate the 13-flow classification through execution, not a knock on the classification itself.

Second, the current runs are red before any of that even matters. The last run at 4e13f92 failed all 13 jobs at flutter pub get, because plugins/widgets is a git submodule and checkout never initialized it (No pubspec.yaml found for package privacygui_widgets, exit 66). So the W-6 question, whether the flows survive without their setUp hooks, is still unanswered. Nothing has run. Fix is either submodules: recursive on checkout with the deploy key, or an explicit init after the SSH step.

On the missing -d in the device workflow, you were right that it is broken, but my explanation of why was wrong. init_config does not exit there. config_loader.sh uses local json=$(jq ...), and the local builtin masks jq's exit status, so $? reads 0 and it writes a blank config. The failure lands later. That same pattern hides a global-config problem too: integration_web.sh reads .wired before set_global_config runs, and in -t mode testcase is empty so no group or global config loads at all. The device harness also assumes a JNAP endpoint at https://localhost/JNAP/ and uses macOS-only WiFi commands in utils.sh (airport, system_profiler, networksetup en0), which won't exist on a Linux runner.

And the runner plan needs to reopen. Flutter publishes no official Linux ARM64 SDK, so the Jetson can't just pull the pinned toolchain. Options are a community ARM64 build, an x64 runner, or building the engine. Not settled.

Here is where I land, and it lines up with your split:

  • Two PRs. The mock PR does not go in until there is a real contract double and the submodule fix, then we run all 13 and get the retained matrix green. I am not treating that as 13/13; some rows may move to the device tier or drop, and whatever stays in the mock matrix has to pass. The device PR is its own track behind the runner, the -d / global-config / localhost / macOS fixes, and W-5.
  • Name it honestly. The mock tier is UI/contract integration, not E2E. I will fix that wording in FLOW-TIERS.
  • Device tier as release validation, not nightly on HEAD. Trigger on a tag or dispatch with a ref, and make firmware and model matrix dimensions. Given the month you already spent fighting device flakiness, validating every commit on hardware is exactly the trap to avoid.
  • Per-PR cost. Since the version-branch ruleset has no required status checks today, this is advisory anyway. Once the tier can actually run, my preference is a small smoke subset per PR plus the full retained mock matrix post-merge and nightly. I would keep the smoke set advisory at first and only make it a required gate once we have per-flow runtime and flakiness data to pick which flows are stable and fast enough to block on.
  • The dashboard_home speed-test point still holds for classification. The measurement is gated behind isHealthCheckSupported, which defaults false and the mock job never sets it, and menu_test uses the same gate. But you are right that it only matters after a mock backend exists.

On 2.x, I agree the in-process suite is 1.x-source-bound and should not be mechanically ported. I am reading your steer as a direction and not a ruling, so I am treating 2.x as still open rather than settled. The one thing I want to keep is the fast contract-level regression the mock tier was supposed to give. If we are going Playwright plus screenshot on the usp line, I would like QA to define the contract flows that suite has to cover, so we do not lose that layer in the move. Happy to bring that to the E2E_MOCK session.

Bottom line: the classification stands as a proposal, but I oversold the mock tier as runnable. Contract double and clean bootstrap first, then we validate the 13 for real. Thanks for catching this before it went anywhere.

@AustinChangLinksys

AustinChangLinksys commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Deven — this is exactly the kind of dig-into-the-source reply that makes the difference, thank you. A few things land on my side:

On the -d / init_config point — you're right and I was wrong about the mechanism. I said the harness would exit 1 early on an empty config path. It doesn't: config_loader.sh uses local json=$(jq ...), and the local builtin swallows jq's exit status, so $? reads 0, a blank config gets written, and the failure surfaces later — exactly as you described. Same masking hides the .wired / global-config ordering issue in -t mode. Good catch, and the more accurate read.

On force=local not being a mock — confirmed, and it was never meant to be. I dug up the original design doc for this harness and attached it below. The prerequisites there are an Nginx reverse proxy to a fixed-IP router (proxy_pass http://192.168.1.1) plus ChromeDriver — force=local just makes the app hit https://localhost/JNAP/, which Nginx forwards to a real device. So the suite was always device-backed; there is no contract double anywhere in integration_test/. Your conclusion is right, and it lines up with the original intent — the mock tier needs a real double (fixture HTTP server or provider/DI overrides with stateful Set/Get) built first. That's the gate before we can validate the 13-flow classification by execution.

Two docs attached for context — this is the history the PR is picking up:

  • linksys_now_using_flutter_integration_testing.md — the original design doc for the harness. Covers the Nginx/ChromeDriver prerequisites, the flutter drive command, the action-class pattern, and the test_meta / meta-group / global-config model (i.e. the setUp/tearDown/tags you're building the tier split on). Its own TODO already listed "build up the Jenkins environment" — so per-PR automation was always the intended next step; this PR is really resuming it on GitHub Actions.
  • daily_integration_test_results.md — the daily device run from that automated period (Oct–Dec 2025, same M62CF-EU, same FW 1.0.5, same UI 1.2.6). It's the concrete record of the stability problem: 16/17 on Oct 1–3, then 11/17 on Oct 6, then 0/17 every day from Oct 7 through Dec 5 — runtime collapsing from ~1h45 to ~38min, i.e. the batch dying at init long before the flows, on an unchanged build. That's the "flaky device + unreliable initialization" story in data: the flows didn't rot, the device harness stopped being trustworthy. It's the strongest argument for not putting the device tier on every commit, and for the retained-matrix-must-go-green bar before we trust any run.

Agreed on the plan, as you laid it out:

  • Two PRs. Mock PR doesn't go in until the contract double + submodule bootstrap fix land, then we run the retained matrix for real — not assuming 13/13.
  • Name it honestly (UI/contract integration, not E2E).
  • Device tier as release validation (tag/dispatch with a ref + firmware/model matrix), not nightly-on-HEAD. The daily-report data above is exactly why validating every commit on hardware is the trap to avoid.
  • Per-PR: smoke subset advisory first, full retained matrix post-merge + nightly, promote to a required gate only once we have per-flow runtime/flakiness data.

On 2.x — yes, treat it as open, not settled. And I think your framing is the right one: if usp goes Playwright + screenshot, QA should define the contract-level flows that suite must cover, so we don't lose the fast contract regression this tier was meant to give. Let's take that to the E2E_MOCK session.

Thanks again — better to find all this now than after it shipped.

linksys_now_using_flutter_integration_testing.md
daily_integration_test_results.md

@DevenDucommun

Copy link
Copy Markdown
Author

Austin, the two docs did not come through. Nothing is attached on the comment for me, and I cannot find either file in the repo, so I think the upload did not take.

Could you re-attach linksys_now_using_flutter_integration_testing.md and daily_integration_test_results.md, or drop them as a gist? The design doc especially is exactly what I want to reference for scoping the contract double, and the daily-results record is worth capturing as the evidence behind the release-validation call. Thanks!

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator

Hi @DevenDucommun
I've updated the comment above with the file links.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

need-decide Automated review left findings needing Austin to decide (fix/defer/dismiss).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants