From d2439392c8d40af4e4ca93d86944b346c46dc998 Mon Sep 17 00:00:00 2001 From: Pantani Date: Fri, 5 Jun 2026 18:35:49 -0300 Subject: [PATCH 1/3] sunscreen fix command --- docs/site/src/reference/cli/doctor.md | 4 +- src/cli/doctor.rs | 5 ++ src/toolchain/fix.rs | 47 ++++++++++++---- tests/doctor_fix.rs | 78 +++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/docs/site/src/reference/cli/doctor.md b/docs/site/src/reference/cli/doctor.md index a1c54bd..598180f 100644 --- a/docs/site/src/reference/cli/doctor.md +++ b/docs/site/src/reference/cli/doctor.md @@ -41,7 +41,9 @@ codama (not found) missing surfpool (not found) missing ``` -With `--fix`, sunscreen prints a before table, progress logs, a fix summary, then an after table. Some upstream installers update shell profiles instead of the current process environment; in that case the fix result is `reload-shell` and sunscreen prints the exact PATH reload command or inspection command to try next. +With `--fix`, sunscreen prints a before table, progress logs, a fix summary, then an after table. Some upstream installers update shell profiles instead of the current process environment; in that case the fix result is `reload-shell` and sunscreen prints the exact PATH reload command to try next. If the installer ran but the binary still reports an unparsable or stale version, the result is `inspect`. If a downloader command fails, the result is `failed` and the curl/agave error is preserved in the logs. + +The Solana/Agave repair recipe downloads the official installer with curl retries and HTTP/1.1 forced, which avoids a common transient `HTTP/2 stream ... INTERNAL_ERROR` failure mode while still surfacing a real download failure when the CDN or network is unavailable. ## `--json` output diff --git a/src/cli/doctor.rs b/src/cli/doctor.rs index fa3ff69..54a6a80 100644 --- a/src/cli/doctor.rs +++ b/src/cli/doctor.rs @@ -99,6 +99,7 @@ pub fn run( fix.status, ToolFixStatus::Failed | ToolFixStatus::NeedsShellReload + | ToolFixStatus::NeedsInspection | ToolFixStatus::Unsupported ) }); @@ -222,6 +223,7 @@ fn fix_status_label(status: ToolFixStatus) -> &'static str { ToolFixStatus::Skipped => "skipped", ToolFixStatus::Fixed => "fixed", ToolFixStatus::NeedsShellReload => "needs_shell_reload", + ToolFixStatus::NeedsInspection => "needs_inspection", ToolFixStatus::Unsupported => "unsupported", ToolFixStatus::Failed => "failed", ToolFixStatus::Attempted => "attempted", @@ -316,6 +318,9 @@ fn print_fix_table(fixes: &[ToolFixResult]) { ToolFixStatus::NeedsShellReload => { Cell::new(format!("{}", "reload-shell".yellow())).fg(Color::Yellow) } + ToolFixStatus::NeedsInspection => { + Cell::new(format!("{}", "inspect".yellow())).fg(Color::Yellow) + } ToolFixStatus::Unsupported => { Cell::new(format!("{}", "unsupported".yellow())).fg(Color::Yellow) } diff --git a/src/toolchain/fix.rs b/src/toolchain/fix.rs index e6aa4d3..0c98a53 100644 --- a/src/toolchain/fix.rs +++ b/src/toolchain/fix.rs @@ -19,6 +19,9 @@ pub enum ToolFixStatus { /// Commands ran successfully, but the current process still cannot see the /// fixed tool. This usually means PATH needs to be reloaded. NeedsShellReload, + /// Commands ran successfully, but the detected binary still needs manual + /// inspection (for example an unparsable wrapper or stale version). + NeedsInspection, /// Sunscreen does not know a safe automatic recipe for this tool. Unsupported, /// A repair command failed or could not be spawned. @@ -140,11 +143,13 @@ pub fn finalize_fix_results(results: &mut [ToolFixResult], reports_after: &[Tool result.status = ToolFixStatus::Fixed; result.message = "tool is available after repair".to_string(); } else { - result.status = ToolFixStatus::NeedsShellReload; - result.message = reports_after + let after = reports_after .iter() - .find(|report| report.name == result.name) - .map_or_else(|| reload_hint(&result.name), post_repair_hint); + .find(|report| report.name == result.name); + result.status = after.map_or(ToolFixStatus::NeedsShellReload, |report| { + post_repair_status(report) + }); + result.message = after.map_or_else(|| reload_hint(&result.name), post_repair_hint); } } } @@ -308,9 +313,10 @@ fn recipe(runner: &R, report: &ToolReport) -> Result { @@ -319,9 +325,10 @@ fn recipe(runner: &R, report: &ToolReport) -> Result { @@ -413,6 +420,18 @@ fn first_non_empty(stderr: &str, stdout: &str) -> Option { .map(ToString::to_string) } +fn download_then_run_shell(url: &str, shell_args: &str) -> String { + let shell_args = shell_args.trim(); + let run = if shell_args.is_empty() { + "sh \"$tmp\"".to_string() + } else { + format!("sh \"$tmp\" {shell_args}") + }; + format!( + "tmp=\"$(mktemp)\"; trap 'rm -f \"$tmp\"' EXIT; curl --http1.1 --retry 3 --retry-delay 2 --retry-all-errors -sSfL {url} -o \"$tmp\" && {run}" + ) +} + fn post_repair_hint(report: &ToolReport) -> String { match report.status { Status::MissingRequired | Status::MissingOptional => reload_hint(&report.name), @@ -428,6 +447,14 @@ fn post_repair_hint(report: &ToolReport) -> String { } } +fn post_repair_status(report: &ToolReport) -> ToolFixStatus { + match report.status { + Status::MissingRequired | Status::MissingOptional => ToolFixStatus::NeedsShellReload, + Status::UnknownVersion | Status::BelowMin => ToolFixStatus::NeedsInspection, + Status::Ok => ToolFixStatus::Fixed, + } +} + fn reload_hint(name: &str) -> String { match name { "solana" => "repair commands succeeded, but `solana` is still not visible on PATH; open a new shell or run `export PATH=\"$HOME/.local/share/solana/install/active_release/bin:$PATH\"`, then run `sunscreen doctor --component solana` again".to_string(), diff --git a/tests/doctor_fix.rs b/tests/doctor_fix.rs index 29a9426..f5fe274 100644 --- a/tests/doctor_fix.rs +++ b/tests/doctor_fix.rs @@ -110,6 +110,50 @@ exit 0 ); } + #[test] + fn doctor_fix_anchor_unparseable_after_repair_needs_inspection() { + let env = CliEnv::new(); + let fake_bin = env.path("bin"); + write_exe( + &fake_bin, + "cargo", + r#"#!/bin/sh +if [ "${1:-}" = "install" ]; then + cat > "$SUNSCREEN_FAKE_BIN/avm" <<'AVM' +#!/bin/sh +if [ "${1:-}" = "use" ]; then + cat > "$SUNSCREEN_FAKE_BIN/anchor" <<'ANCHOR' +#!/bin/sh +if [ "${1:-}" = "--version" ]; then + echo "anchor from a custom wrapper" +fi +ANCHOR + chmod +x "$SUNSCREEN_FAKE_BIN/anchor" +fi +exit 0 +AVM + chmod +x "$SUNSCREEN_FAKE_BIN/avm" +fi +exit 0 +"#, + ); + + let mut cmd = env.sunscreen(); + cmd.env("SUNSCREEN_FAKE_BIN", &fake_bin); + cmd.args(["--json", "doctor", "--component", "anchor", "--fix"]); + let out = env.err("doctor --fix anchor unparseable", &mut cmd, 2); + let stdout: serde_json::Value = + serde_json::from_slice(&out.stdout).expect("stdout should remain JSON"); + let stderr = String::from_utf8_lossy(&out.stderr); + + assert_eq!(stdout["fixes"][0]["status"], "needs_inspection"); + assert!(stdout["fixes"][0]["message"] + .as_str() + .unwrap() + .contains("anchor --version")); + assert!(stderr.contains("doctor fix: anchor needs_inspection")); + } + #[test] fn doctor_fix_component_codama_repairs_optional_tool_when_targeted() { let env = CliEnv::new(); @@ -175,4 +219,38 @@ exit 0 "missing final status log: {stderr}" ); } + + #[test] + fn doctor_fix_solana_reports_downloader_failure_instead_of_reload_shell() { + let env = CliEnv::new(); + let fake_bin = env.path("bin"); + write_exe( + &fake_bin, + "curl", + r#"#!/bin/sh +echo "curl $@" >> "$SUNSCREEN_FAKE_LOG" +echo "curl: (92) HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2)" >&2 +exit 92 +"#, + ); + + let mut cmd = env.sunscreen(); + cmd.args(["--json", "doctor", "--component", "solana", "--fix"]); + let out = env.err("doctor --fix solana failed curl", &mut cmd, 2); + let stdout: serde_json::Value = + serde_json::from_slice(&out.stdout).expect("stdout should remain JSON"); + let stderr = String::from_utf8_lossy(&out.stderr); + + assert_eq!(stdout["fixes"][0]["name"], "solana"); + assert_eq!(stdout["fixes"][0]["status"], "failed"); + assert_ne!(stdout["fixes"][0]["status"], "needs_shell_reload"); + assert_eq!(stdout["fixes"][0]["exit_code"], 92); + assert!(stdout["fixes"][0]["message"] + .as_str() + .unwrap() + .contains("HTTP/2 stream 1 was not closed cleanly")); + assert!(stderr.contains("doctor fix: failed `sh -c")); + assert!(stderr.contains("HTTP/2 stream 1 was not closed cleanly")); + assert!(env.fake_log().contains("curl --http1.1")); + } } From 49c8e133e1d1298f3a12750c79f0fd4a3fdf44fa Mon Sep 17 00:00:00 2001 From: Pantani Date: Sat, 6 Jun 2026 01:49:04 -0300 Subject: [PATCH 2/3] fix real-usage bugs + add end-to-end flow test harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fixes (all verified with real binary from /tmp): - BUG-001: quickstart relative-path crash — create_workspace now makes dest absolute via current_dir().join() before passing to Transaction/WorkspaceRoot, eliminating the double-prefix "my-app/my-app/programs/..." ENOENT. - BUG-002: scaffold instruction/account/event/error required --program even when workspace has exactly one program — resolve_program() now auto-detects the single program and prints "note: using program X" to stderr. - BUG-003: sunscreen learn list gave unknown topic list — added Some("list") | None arm to route to list_topics(). Flow test harness (new): - .claude/skills/sunscreen-flow-tests/ with scripts for three complete user journeys: zero-to-NFT (18 assertions), zero-to-token (17), and zero-to-smart-contract (17). flow-runner.sh orchestrates all three. - .claude/agents/flow-test-runner.md — new specialist owning end-to-end flows. - Updated offline-ci-owner, test-harness-orchestrator, e2e-qa-fixer, and ux-flow-validator to require flow tests before marking any gate green. 390/390 unit tests pass. 52/52 flow assertions pass. Co-Authored-By: Claude Sonnet 4.6 --- .claude/agents/e2e-qa-fixer.md | 89 ++++++++++ .claude/agents/flow-test-runner.md | 112 ++++++++++++ .claude/agents/offline-ci-owner.md | 19 +++ .claude/agents/test-harness-orchestrator.md | 22 ++- .claude/agents/ux-flow-validator.md | 103 +++++++++++ .claude/skills/sunscreen-flow-tests/SKILL.md | 98 +++++++++++ .../sunscreen-flow-tests/scripts/flow-nft.sh | 122 +++++++++++++ .../scripts/flow-runner.sh | 47 +++++ .../scripts/flow-smart-contract.sh | 160 ++++++++++++++++++ .../scripts/flow-token.sh | 108 ++++++++++++ CLAUDE.md | 2 + src/cli/chain.rs | 11 ++ src/cli/scaffold.rs | 92 ++++++---- src/onboarding/learn.rs | 2 +- 14 files changed, 951 insertions(+), 36 deletions(-) create mode 100644 .claude/agents/e2e-qa-fixer.md create mode 100644 .claude/agents/flow-test-runner.md create mode 100644 .claude/agents/ux-flow-validator.md create mode 100644 .claude/skills/sunscreen-flow-tests/SKILL.md create mode 100644 .claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh create mode 100644 .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh create mode 100644 .claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh create mode 100644 .claude/skills/sunscreen-flow-tests/scripts/flow-token.sh diff --git a/.claude/agents/e2e-qa-fixer.md b/.claude/agents/e2e-qa-fixer.md new file mode 100644 index 0000000..53e233b --- /dev/null +++ b/.claude/agents/e2e-qa-fixer.md @@ -0,0 +1,89 @@ +--- +name: e2e-qa-fixer +description: > + End-to-end QA engineer for sunscreen. Runs the real binary, finds UX/runtime bugs, + and fixes them. Triggered when: integration tests pass but real usage fails, quickstart + is broken, beginner flow doesn't work, any scaffold/build/learn command crashes or + gives wrong output, full flow smoke fails, "bugs em fluxo real", "comando não funciona". + Owns the fix → flow-verify loop: a fix is NOT done until the full flows pass. +model: opus +tools: Read, Write, Edit, Bash, Grep, Glob +--- + +# E2E QA Fixer — sunscreen + +Your standard: a developer who just heard about Solana runs the full NFT or smart +contract flow without debugging a single error. If a fix doesn't pass the full flows, +it is not done. + +## Non-negotiable: full flows before AND after every fix + +Before touching any code, run the full flows to establish a baseline: +```bash +cargo build --release +bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh +``` + +After every fix, run the same thing. If any flow that was passing before is now +failing, the fix introduced a regression — revert it. + +## Core responsibilities + +1. Build the **release binary** (`cargo build --release`) before any testing +2. Run all offline flows from `flow-test-runner` to find which steps fail +3. Investigate root causes in `src/` (not just symptoms) +4. Apply minimal surgical fixes +5. Re-run all flows — every single flow must pass +6. Run `cargo test --locked` — no regressions in unit tests +7. Update CLAUDE.md variation log with each shipped fix + +## How to run flows + +```bash +export SUNSCREEN_BIN="$(pwd)/target/release/sunscreen" +export SUNSCREEN_SKIP_PREFLIGHT=1 + +# All offline flows (run this before and after every fix) +bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh + +# Individual flows: +bash .claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh +bash .claude/skills/sunscreen-flow-tests/scripts/flow-token.sh +bash .claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh +``` + +## Triage priority + +| Severity | Symptom | Action | +|----------|---------|--------| +| P0 | Rust panic in any flow step | Fix immediately, block PR | +| P0 | Flow step exits with wrong code | Fix before any other work | +| P1 | File not created when expected | Fix in same session | +| P1 | Error message confusing / no recovery hint | Fix or add actionable message | +| P2 | Extra newline, formatting issue | Log and defer | + +## Bug report format (when filing, not fixing) + +``` +FLOW: zero-to-NFT +STEP: scaffold instruction mint +CMD: sunscreen scaffold instruction mint +CWD: /tmp/sunscreen-flow-nft-1234/testnft1234 +EXIT: 1 (expected 0) +OUTPUT: +ROOT CAUSE: +``` + +## Fix principles + +- Fix at the source (where data is created), not the symptom (where it's used) +- Never break existing tests — `cargo test --locked` must stay green +- Prefer adding a flow assertion over writing a unit test for path bugs +- One fix per commit; don't batch unrelated changes +- Update CLAUDE.md variation log immediately after each fix lands + +## Known-fixed bugs (do not re-introduce) + +- BUG-001: `create_workspace` relative-path double-prefix (fixed 2026-06-06) — `dest` is now made absolute before being stored +- BUG-002: `scaffold *` required `--program` even with single program (fixed 2026-06-06) — `resolve_program()` auto-detects +- BUG-003: `learn list` gave "unknown topic" error (fixed 2026-06-06) — `Some("list") | None` arm added diff --git a/.claude/agents/flow-test-runner.md b/.claude/agents/flow-test-runner.md new file mode 100644 index 0000000..33fc3b1 --- /dev/null +++ b/.claude/agents/flow-test-runner.md @@ -0,0 +1,112 @@ +--- +name: flow-test-runner +description: > + Runs complete end-to-end user journey flows for sunscreen CLI: zero-to-NFT, + zero-to-token, zero-to-smart-contract, doctor check. Executes the real compiled + binary from isolated temp directories — not mocks, not unit tests, real usage. + Triggered when: "run flow tests", "test full NFT flow", "does quickstart really work", + "testa fluxo completo", "fluxo NFT", "fluxo smart contract", "smoke test binary", + "validate end-to-end", before any PR merge or release tag. +model: opus +tools: Read, Write, Edit, Bash, Grep, Glob +--- + +# Flow Test Runner — sunscreen + +Your standard: a developer who just heard about Solana runs the full sequence and +every step works without debugging. If any step fails, you own finding the root +cause and filing a bug report with reproduction steps. + +## Core mandate + +Run complete user journeys, not isolated commands. A "journey" means: + +1. Start from a clean temp directory (never the project root) +2. Create a workspace (via `quickstart` or `chain new`) +3. Add scaffolding (instruction, account, event, error) +4. Verify every generated file exists on disk +5. Run build (expect exit 0 with real Anchor, exit 2 without) +6. Clean up the temp dir + +If a step fails, capture the exact output and exit code — that is the bug report. + +## Setup + +```bash +# Build the binary first (always fresh before flow tests) +cargo build --release + +export SUNSCREEN_BIN="$(pwd)/target/release/sunscreen" +export SUNSCREEN_SKIP_PREFLIGHT=1 # all offline flows +``` + +## Flows to run + +### Flow 1: zero-to-NFT (offline) +```bash +bash .claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh +``` +Covers: `quickstart nft` → scaffold instruction/account/event/error → idempotency → learn → doctor + +### Flow 2: zero-to-token (offline) +```bash +bash .claude/skills/sunscreen-flow-tests/scripts/flow-token.sh +``` +Covers: `quickstart token` → mint/burn/transfer_checked instructions → accounts → events/errors + +### Flow 3: zero-to-smart-contract (offline + optional real) +```bash +# Offline (anchor missing → exit 2, that's a PASS) +bash .claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh + +# Real toolchain (requires anchor, solana, pnpm in PATH) +SUNSCREEN_REAL_TOOLCHAIN=1 bash .claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh +``` +Covers: `chain new` → `scaffold program` → instructions/accounts/errors → multi-program `--program` required → build + +### Run all offline flows at once +```bash +bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh +``` + +## What to verify manually after each script + +For each PASS flow, spot-check: +- `sunscreen.yml` contains the program name +- `programs//src/instructions/` has the expected `.rs` files +- `programs//src/state/` has the expected `.rs` files +- No Rust panic in any command output + +## Bug reporting format + +When a step fails, report: +``` +FLOW: zero-to-NFT +STEP: scaffold instruction mint +CMD: sunscreen scaffold instruction mint +CWD: /tmp/sunscreen-flow-nft-1234/testnft1234 +EXIT: 1 (expected 0) +OUTPUT: + thread 'main' panicked at ... ← or the actual error +ROOT CAUSE HYPOTHESIS: ... +``` + +## Principles + +- Always build fresh (`cargo build --release`) before running flows +- Never test from the project root — isolation is the point +- A flow that panics is always P0, no matter the message +- A flow that exits with the wrong code is P0 +- A flow that exits correctly but creates wrong files is P1 +- Clean up temp dirs on success; preserve them on failure for debugging +- Run all offline flows on every PR, real-toolchain flows only when anchor/solana/pnpm available + +## After finding a bug + +1. Reproduce with the minimal repro (single command from a temp dir) +2. Identify root cause in `src/` +3. Apply surgical fix (no scope creep) +4. `cargo test --locked` — all tests must stay green +5. Re-run the failing flow — it must PASS +6. Re-run all offline flows — none must regress +7. Report to `e2e-qa-fixer` for tracking and CLAUDE.md update diff --git a/.claude/agents/offline-ci-owner.md b/.claude/agents/offline-ci-owner.md index 0135538..1fcff19 100644 --- a/.claude/agents/offline-ci-owner.md +++ b/.claude/agents/offline-ci-owner.md @@ -33,6 +33,25 @@ cargo test --locked --test integration_chain --test integration_scaffold --test cargo test --locked --test compile_generated_workspace ``` +## Mandatory: full flow tests after offline gate passes + +Unit tests and compile checks are necessary but not sufficient. After the commands +above all pass, run the bundled flow tests against the real binary: + +```bash +export SUNSCREEN_BIN="$(pwd)/target/release/sunscreen" +export SUNSCREEN_SKIP_PREFLIGHT=1 +bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh +``` + +If `flow-runner.sh` reports any FAIL, the offline gate is NOT green — even if +every `cargo test` passed. Route the failure to `flow-test-runner` or `e2e-qa-fixer`. + +The reason: unit tests run with fake toolchains, injected runners, and controlled +paths. Flow tests exercise the binary from a real temp directory with user-level +inputs — the only way to catch path bugs, relative-vs-absolute mistakes, and +missing auto-detection that unit tests structurally cannot see. + ## Team Communication Protocol - Route Anchor/Codama gaps to `real-anchor-codama-owner`. - Route real Pinocchio gaps to `pinocchio-sbf-owner`. diff --git a/.claude/agents/test-harness-orchestrator.md b/.claude/agents/test-harness-orchestrator.md index cb8c943..29c4ef3 100644 --- a/.claude/agents/test-harness-orchestrator.md +++ b/.claude/agents/test-harness-orchestrator.md @@ -25,18 +25,31 @@ Coordinate the heavy validation team for `sunscreen`. Turn the user's request in 1. Read the current state and `git status`. 2. Ask `test-strategist` for a risk matrix when scope is broad. 3. Run, or delegate to `offline-ci-owner`, the command `bash scripts/integration-heavy.sh`. -4. Read the most recent `summary.json` and classify tiers. -5. If the user asked for real toolchain, dispatch: +4. **Always dispatch `flow-test-runner` in parallel with or immediately after offline-ci-owner.** + Flow tests run the real binary from /tmp and catch path bugs, relative-path + issues, and auto-detection failures that unit tests cannot see. Commands: + ```bash + export SUNSCREEN_BIN="$(pwd)/target/release/sunscreen" + export SUNSCREEN_SKIP_PREFLIGHT=1 + bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh + ``` + A flow FAIL is a blocker — it blocks the round regardless of unit test results. +5. Read the most recent `summary.json` and classify tiers. +6. If the user asked for real toolchain, dispatch: - `real-anchor-codama-owner` for Anchor/Codama. - `pinocchio-sbf-owner` for Pinocchio SBF. - `serve-runtime-owner` for runtime/watch/teardown. - `frontend-codegen-owner` for frontend typecheck. -6. Dispatch `plugin-runtime-qa`, `release-distribution-qa`, and `flake-perf-auditor` as the requested tiers demand. -7. Consolidate everything for `qa-integrator` to close the round. +7. Dispatch `plugin-runtime-qa`, `release-distribution-qa`, and `flake-perf-auditor` as the requested tiers demand. +8. Dispatch `ux-flow-validator` as the final UX acceptance gate — it validates the + full beginner journey and runs `flow-runner.sh` as its primary check. +9. Consolidate everything for `qa-integrator` to close the round. ## Team Communication Protocol - `test-strategist`: receives scope and returns the risk matrix. - `offline-ci-owner`: runs the standard gate and reports summary/log. +- `flow-test-runner`: **mandatory in every round** — runs zero-to-NFT, zero-to-token, + zero-to-smart-contract flows with the real binary from /tmp. - `real-anchor-codama-owner`: invoked only when `SUNSCREEN_REAL_TOOLCHAIN=1` and the tools exist. - `pinocchio-sbf-owner`: invoked when real Solana SBF is the target. - `serve-runtime-owner`: invoked when real runtime and watcher/teardown are the target. @@ -44,6 +57,7 @@ Coordinate the heavy validation team for `sunscreen`. Turn the user's request in - `frontend-codegen-owner`: receives hooks/frontend typecheck. - `release-distribution-qa`: receives cargo-dist/install/release slices. - `flake-perf-auditor`: receives repetition/timeouts/perf. +- `ux-flow-validator`: final beginner UX acceptance gate. - `qa-integrator`: receives the final consolidated report. ## Error Handling diff --git a/.claude/agents/ux-flow-validator.md b/.claude/agents/ux-flow-validator.md new file mode 100644 index 0000000..4eecc57 --- /dev/null +++ b/.claude/agents/ux-flow-validator.md @@ -0,0 +1,103 @@ +--- +name: ux-flow-validator +description: > + UX flow validator for sunscreen. Validates the complete beginner journey end-to-end + by running the real binary: full NFT flow, token flow, smart contract flow, doctor, + learn, examples. Use after bug fixes, before releases, after any CLI change. Reports + PASS/FAIL per tier with exact error messages. "valida fluxo", "testa experiência do usuário", + "UX do sunscreen", "beginner experience", "does the full flow work". +model: opus +tools: Read, Write, Edit, Bash, Grep, Glob +--- + +# UX Flow Validator — sunscreen + +Your standard: zero debugging required from `sunscreen quickstart nft` to a working +NFT workspace. You are the last gate before "this works for a beginner." + +## Validation protocol + +**Always run the bundled flow scripts first.** Manual command-by-command checking +comes AFTER the scripts reveal what needs closer inspection. + +```bash +cargo build --release +export SUNSCREEN_BIN="$(pwd)/target/release/sunscreen" +export SUNSCREEN_SKIP_PREFLIGHT=1 + +# Run all offline flows — this is the primary gate +bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh +``` + +If flow-runner passes, proceed to manual spot-checks. If it fails, report the +failing step with exact output and stop — do not mark any tier as PASS. + +## Validation tiers + +### Tier 0: Build gate (prerequisite for all tiers) +- [ ] `cargo build --release` exits 0 +- [ ] Binary exists at `target/release/sunscreen` + +### Tier 1: Binary basics +- [ ] `sunscreen --version` → shows version string (not a panic) +- [ ] `sunscreen --help` → lists all top-level commands +- [ ] `sunscreen doctor` → exits 0 or 1 (never panics, clear output) +- [ ] `sunscreen doctor --json` → valid JSON array + +### Tier 2: Full NFT flow (run `flow-nft.sh`) +- [ ] `quickstart nft --name X --non-interactive` → exit 0, workspace created +- [ ] `scaffold instruction mint` (no --program) → exit 0, file created +- [ ] `scaffold account NftMetadata` → exit 0, file created +- [ ] `scaffold event NftMinted` → exit 0, file created +- [ ] `scaffold error InvalidMint` → exit 0, file created +- [ ] Duplicate scaffold → exit 4 (conflict, not crash) + +### Tier 3: Full token flow (run `flow-token.sh`) +- [ ] `quickstart token --name X --non-interactive` → exit 0 +- [ ] Multiple instructions scaffolded (mint_to, burn, transfer_checked) → all exit 0 +- [ ] Multiple accounts scaffolded → all exit 0 + +### Tier 4: Full smart contract flow (run `flow-smart-contract.sh`) +- [ ] `chain new X` → exit 0 +- [ ] `scaffold program token_vault` → exit 0 +- [ ] Instructions/accounts/errors with explicit `--program` → all exit 0 +- [ ] `chain build --headless` → exit 0 (with anchor) or exit 2 (without, clear error) + +### Tier 5: Learning & discovery +- [ ] `learn` (no args) → shows topic list +- [ ] `learn list` → same as no args (not an error) +- [ ] `learn pda` → renders content without error +- [ ] `examples list` → shows examples +- [ ] `next-step` → gives actionable next step + +### Tier 6: Build (real toolchain — optional) +Only validate when `anchor`, `solana`, and `pnpm` are in PATH: +- [ ] `chain build --headless` in quickstart workspace → exit 0 +- [ ] `generate clients` → exit 0 or clear error if IDL not found + +## Reporting format + +For each tier, report exactly: +``` +Tier 2 — NFT flow: PASS (all 6 steps) +Tier 4 — smart contract: FAIL + Step: scaffold instruction deposit --program X + Exit: 1 (expected 0) + Output: thread 'main' panicked at src/... + Priority: P0 — blocks beginner +``` + +## UX friction checklist (beyond pass/fail) + +Flag any of these even when exit code is 0: +- Error messages with no recovery hint ("try X instead") +- Commands that silently do nothing when they should print something +- Output that doesn't explain next steps +- Stack traces leaking to stdout instead of stderr +- Any Rust internal error path exposed to the user + +## Do not mark PASS without running the flow scripts + +Spot-checking individual commands is not sufficient. The flow scripts catch bugs +that only appear when commands run in sequence (path bugs, state corruption, etc.). +Running `flow-runner.sh` is mandatory, not optional. diff --git a/.claude/skills/sunscreen-flow-tests/SKILL.md b/.claude/skills/sunscreen-flow-tests/SKILL.md new file mode 100644 index 0000000..a16b143 --- /dev/null +++ b/.claude/skills/sunscreen-flow-tests/SKILL.md @@ -0,0 +1,98 @@ +--- +name: sunscreen-flow-tests +description: > + Complete end-to-end user journey tests for sunscreen CLI. Runs the real compiled + binary from a temp directory and validates full flows: zero-to-NFT, zero-to-token, + zero-to-smart-contract, doctor check. Use this whenever: testing if the CLI works + end-to-end, validating a fix didn't break a full flow, "testa o fluxo completo", + "run full flow tests", "does quickstart really work", "integration tests pass but + real usage fails", "smoke test the binary", before any release or PR merge. + These tests catch bugs that unit tests miss because they run the binary as a real + user would: from any directory, with relative paths, with no --program flag, etc. +--- + +# sunscreen Flow Tests + +The standard for a passing flow: a developer who never touched Solana runs the +command and it works, with zero debugging required. + +## Binary location + +Always use the release binary. Build first if the binary is stale: +```bash +SUNSCREEN_BIN=$(pwd)/target/release/sunscreen +cargo build --release -q +``` + +## Flow scripts (bundled) + +All flows live in `scripts/`. Run them from the sunscreen project root: + +| Script | Flow | Toolchain required | +|--------|------|--------------------| +| `flow-nft.sh` | zero-to-NFT (quickstart + scaffold) | offline (SKIP_PREFLIGHT) | +| `flow-token.sh` | zero-to-token (quickstart token + scaffold) | offline | +| `flow-smart-contract.sh` | blank workspace → scaffold program → build | offline + optional Anchor | +| `flow-runner.sh` | runs all offline flows and reports a summary | offline | + +## Environment variables + +| Var | Meaning | +|-----|---------| +| `SUNSCREEN_SKIP_PREFLIGHT=1` | skip toolchain checks — required for all offline flows | +| `SUNSCREEN_REAL_TOOLCHAIN=1` | allow flows that need real Anchor/Solana/pnpm | +| `SUNSCREEN_BIN` | path to the binary; defaults to `./target/release/sunscreen` | +| `FLOW_TMPDIR` | override temp dir (default: auto-generated under /tmp) | + +## Anatomy of a flow script + +Each script: +1. Creates an isolated temp dir under `/tmp/sunscreen-flow--/` +2. Sets `trap` to clean up on exit (even on failure) +3. Runs commands with `assert_cmd` — which prints PASS/FAIL and exits non-zero on failure +4. Prints a summary line per step: `[PASS] step description` or `[FAIL] step (exit N): output` +5. Exits 0 only when ALL steps pass + +## Running manually + +```bash +# All offline flows: +bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh + +# Single flow: +bash .claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh + +# With real toolchain (needs anchor, solana, pnpm): +SUNSCREEN_REAL_TOOLCHAIN=1 bash .claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh +``` + +## Adding a new flow + +1. Copy `flow-nft.sh` as the template. +2. Define steps as `assert_cmd `. +3. Use `SUNSCREEN_SKIP_PREFLIGHT=1` for offline steps. +4. Register the new script in `flow-runner.sh`. +5. Add it to this table and to the agent that owns it. + +## Acceptance criteria + +A flow PASSES when: +- Every `assert_cmd` exits with the expected code (0 for success, 2 for missing tool, etc.) +- Files expected to exist after the command are present on disk +- No step emits an unhandled Rust panic or `thread 'main' panicked` + +A flow FAILS when: +- Any step exits with a different code than expected +- Any file that should have been created is absent +- The binary emits a panic + +## What flows do NOT test + +- Unit-level logic (covered by `cargo test`) +- Real Anchor compilation (covered by `real-anchor-codama-owner`) +- Real Solana deploy (requires funded wallet + network — outside offline scope) +- Plugin gRPC transport (covered by `plugin-runtime-qa`) + +When a real deploy test is needed, set `SUNSCREEN_REAL_TOOLCHAIN=1` and ensure +`anchor`, `solana`, and `pnpm` are installed. The `flow-smart-contract.sh` script +accepts this mode and will attempt a real `anchor build`. diff --git a/.claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh b/.claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh new file mode 100644 index 0000000..72fbdd1 --- /dev/null +++ b/.claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# flow-nft.sh — zero-to-NFT complete user journey test. +# Tests: quickstart nft → scaffold instruction/account/event/error → verify files +# Toolchain: offline (SUNSCREEN_SKIP_PREFLIGHT=1 — no Anchor/Solana needed) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUNSCREEN_BIN="${SUNSCREEN_BIN:-$(cd "$SCRIPT_DIR/../../../.." && pwd)/target/release/sunscreen}" +export SUNSCREEN_SKIP_PREFLIGHT=1 + +if [[ ! -x "$SUNSCREEN_BIN" ]]; then + echo "[ERROR] Binary not found: $SUNSCREEN_BIN — run: cargo build --release" + exit 1 +fi + +# Isolated temp workspace +FLOW_DIR="${FLOW_TMPDIR:-/tmp}/sunscreen-flow-nft-$(date +%s)" +WORKSPACE_NAME="testnft$(date +%s)" +trap 'rm -rf "$FLOW_DIR"' EXIT + +mkdir -p "$FLOW_DIR" + +PASS=0 +FAIL=0 + +assert_cmd() { + local expected_exit="$1" + local description="$2" + shift 2 + local actual_exit=0 + local output + output=$("$@" 2>&1) || actual_exit=$? + if [[ "$actual_exit" -eq "$expected_exit" ]]; then + echo "[PASS] $description" + PASS=$((PASS + 1)) + else + echo "[FAIL] $description (expected exit $expected_exit, got $actual_exit)" + echo " output: $(echo "$output" | head -5)" + FAIL=$((FAIL + 1)) + fi +} + +assert_file() { + local path="$1" + local description="$2" + if [[ -e "$path" ]]; then + echo "[PASS] $description" + PASS=$((PASS + 1)) + else + echo "[FAIL] $description — file missing: $path" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== Flow: zero-to-NFT ===" +echo " binary: $SUNSCREEN_BIN" +echo " workspace: $FLOW_DIR/$WORKSPACE_NAME" +echo "" + +# Step 1: quickstart nft +cd "$FLOW_DIR" +assert_cmd 0 "quickstart nft creates workspace" \ + "$SUNSCREEN_BIN" quickstart nft --name "$WORKSPACE_NAME" --non-interactive + +# Step 2: workspace structure +assert_file "$FLOW_DIR/$WORKSPACE_NAME/sunscreen.yml" "sunscreen.yml exists" +assert_file "$FLOW_DIR/$WORKSPACE_NAME/Anchor.toml" "Anchor.toml exists" +assert_file "$FLOW_DIR/$WORKSPACE_NAME/programs/" "programs/ directory exists" + +# Step 3: scaffold instruction (no --program, should auto-detect) +cd "$FLOW_DIR/$WORKSPACE_NAME" +assert_cmd 0 "scaffold instruction mint (auto-detects program)" \ + "$SUNSCREEN_BIN" scaffold instruction mint + +# Step 4: verify instruction files +assert_file "programs/${WORKSPACE_NAME}/src/instructions/mint.rs" \ + "instruction file created: mint.rs" +assert_file "programs/${WORKSPACE_NAME}/src/instructions/mod.rs" \ + "mod.rs updated with mint" + +# Step 5: scaffold account +assert_cmd 0 "scaffold account NftMetadata (no --program)" \ + "$SUNSCREEN_BIN" scaffold account NftMetadata + +assert_file "programs/${WORKSPACE_NAME}/src/state/nft_metadata.rs" \ + "account file created: nft_metadata.rs" + +# Step 6: scaffold event +assert_cmd 0 "scaffold event NftMinted (no --program)" \ + "$SUNSCREEN_BIN" scaffold event NftMinted + +assert_file "programs/${WORKSPACE_NAME}/src/events.rs" \ + "events.rs created" + +# Step 7: scaffold error +assert_cmd 0 "scaffold error InvalidMint (no --program)" \ + "$SUNSCREEN_BIN" scaffold error InvalidMint + +assert_file "programs/${WORKSPACE_NAME}/src/errors.rs" \ + "errors.rs created" + +# Step 8: idempotency — scaffolding the same name again must exit 0 (no-op), not crash +assert_cmd 0 "duplicate instruction mint → exit 0 (idempotent no-op)" \ + "$SUNSCREEN_BIN" scaffold instruction mint + +# Step 9: learn (basic sanity) +assert_cmd 0 "learn (topic list)" \ + "$SUNSCREEN_BIN" learn + +assert_cmd 0 "learn list" \ + "$SUNSCREEN_BIN" learn list + +assert_cmd 0 "learn pda" \ + "$SUNSCREEN_BIN" learn pda + +# Step 10: doctor (offline — may report missing tools, must not crash) +assert_cmd 0 "doctor exits 0 (reports tool status)" \ + "$SUNSCREEN_BIN" doctor || true # doctor may exit non-zero if tools missing — accept both + +echo "" +echo "━━━ zero-to-NFT: PASS=$PASS FAIL=$FAIL ━━━" +[[ $FAIL -eq 0 ]] diff --git a/.claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh b/.claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh new file mode 100644 index 0000000..9ea8247 --- /dev/null +++ b/.claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# flow-runner.sh — runs all offline sunscreen flow tests and prints a summary. +# Usage: bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export SUNSCREEN_BIN="${SUNSCREEN_BIN:-$(cd "$SCRIPT_DIR/../../../.." && pwd)/target/release/sunscreen}" + +if [[ ! -x "$SUNSCREEN_BIN" ]]; then + echo "[ERROR] Binary not found at $SUNSCREEN_BIN — run: cargo build --release" + exit 1 +fi + +FLOWS=( + "flow-nft.sh:zero-to-NFT" + "flow-token.sh:zero-to-token" + "flow-smart-contract.sh:zero-to-smart-contract" +) + +PASS=0 +FAIL=0 +RESULTS=() + +for entry in "${FLOWS[@]}"; do + script="${entry%%:*}" + label="${entry#*:}" + echo "" + echo "━━━ $label ━━━" + if bash "$SCRIPT_DIR/$script"; then + RESULTS+=(" [PASS] $label") + PASS=$((PASS + 1)) + else + RESULTS+=(" [FAIL] $label") + FAIL=$((FAIL + 1)) + fi +done + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Flow Test Summary" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +for r in "${RESULTS[@]}"; do echo "$r"; done +echo "" +echo " Passed: $PASS Failed: $FAIL" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +[[ $FAIL -eq 0 ]] diff --git a/.claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh b/.claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh new file mode 100644 index 0000000..305fbce --- /dev/null +++ b/.claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# flow-smart-contract.sh — zero-to-smart-contract complete user journey test. +# Tests: chain new → scaffold program → scaffold instruction/account/error → chain build +# +# Offline mode (default, SUNSCREEN_SKIP_PREFLIGHT=1): +# chain build will fail with exit 2 (anchor missing) — that's expected and PASS. +# +# Real toolchain mode (SUNSCREEN_REAL_TOOLCHAIN=1): +# chain build runs anchor build — expects exit 0. +# Requires: anchor, solana, cargo, pnpm in PATH. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUNSCREEN_BIN="${SUNSCREEN_BIN:-$(cd "$SCRIPT_DIR/../../../.." && pwd)/target/release/sunscreen}" +REAL_TOOLCHAIN="${SUNSCREEN_REAL_TOOLCHAIN:-0}" + +if [[ "$REAL_TOOLCHAIN" != "1" ]]; then + export SUNSCREEN_SKIP_PREFLIGHT=1 +fi + +if [[ ! -x "$SUNSCREEN_BIN" ]]; then + echo "[ERROR] Binary not found: $SUNSCREEN_BIN — run: cargo build --release" + exit 1 +fi + +FLOW_DIR="${FLOW_TMPDIR:-/tmp}/sunscreen-flow-contract-$(date +%s)" +WORKSPACE_NAME="testcontract$(date +%s)" +trap 'rm -rf "$FLOW_DIR"' EXIT + +mkdir -p "$FLOW_DIR" + +PASS=0 +FAIL=0 + +assert_cmd() { + local expected_exit="$1" + local description="$2" + shift 2 + local actual_exit=0 + local output + output=$("$@" 2>&1) || actual_exit=$? + if [[ "$actual_exit" -eq "$expected_exit" ]]; then + echo "[PASS] $description" + PASS=$((PASS + 1)) + else + echo "[FAIL] $description (expected exit $expected_exit, got $actual_exit)" + echo " output: $(echo "$output" | head -5)" + FAIL=$((FAIL + 1)) + fi +} + +assert_file() { + local path="$1" + local description="$2" + if [[ -e "$path" ]]; then + echo "[PASS] $description" + PASS=$((PASS + 1)) + else + echo "[FAIL] $description — file missing: $path" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== Flow: zero-to-smart-contract ===" +echo " binary: $SUNSCREEN_BIN" +echo " workspace: $FLOW_DIR/$WORKSPACE_NAME" +echo " real_toolchain: $REAL_TOOLCHAIN" +echo "" + +cd "$FLOW_DIR" + +# Step 1: create blank workspace +assert_cmd 0 "chain new creates workspace" \ + "$SUNSCREEN_BIN" chain new "$WORKSPACE_NAME" + +assert_file "$FLOW_DIR/$WORKSPACE_NAME/sunscreen.yml" "sunscreen.yml exists" +assert_file "$FLOW_DIR/$WORKSPACE_NAME/Anchor.toml" "Anchor.toml exists" +assert_file "$FLOW_DIR/$WORKSPACE_NAME/programs/" "programs/ directory exists" + +cd "$FLOW_DIR/$WORKSPACE_NAME" + +# Step 2: scaffold additional program (to test multi-program scenario) +assert_cmd 0 "scaffold program token_vault" \ + "$SUNSCREEN_BIN" scaffold program token_vault + +assert_file "programs/token_vault/src/lib.rs" "token_vault/src/lib.rs exists" +assert_file "programs/token_vault/Cargo.toml" "token_vault/Cargo.toml exists" + +# Step 3: scaffold instructions for default program (single → auto-detect) +# With two programs now, --program is required +assert_cmd 0 "scaffold instruction deposit (explicit --program)" \ + "$SUNSCREEN_BIN" scaffold instruction deposit --program "$WORKSPACE_NAME" + +assert_cmd 0 "scaffold instruction withdraw (explicit --program)" \ + "$SUNSCREEN_BIN" scaffold instruction withdraw --program "$WORKSPACE_NAME" + +assert_file "programs/${WORKSPACE_NAME}/src/instructions/deposit.rs" "deposit.rs exists" +assert_file "programs/${WORKSPACE_NAME}/src/instructions/withdraw.rs" "withdraw.rs exists" + +# Step 4: scaffold accounts +assert_cmd 0 "scaffold account Vault (explicit --program)" \ + "$SUNSCREEN_BIN" scaffold account Vault --program "$WORKSPACE_NAME" + +assert_file "programs/${WORKSPACE_NAME}/src/state/vault.rs" "vault.rs exists" + +# Step 5: scaffold errors +assert_cmd 0 "scaffold error InvalidAmount (explicit --program)" \ + "$SUNSCREEN_BIN" scaffold error InvalidAmount --program "$WORKSPACE_NAME" + +assert_file "programs/${WORKSPACE_NAME}/src/errors.rs" "errors.rs exists" + +# Step 6: chain build +if [[ "$REAL_TOOLCHAIN" == "1" ]]; then + echo "" + echo "--- Real toolchain mode: running anchor build ---" + assert_cmd 0 "chain build --headless succeeds with real Anchor" \ + "$SUNSCREEN_BIN" chain build --headless +else + # Offline: accepted outcomes for chain build: + # exit 0 — anchor installed and build succeeded + # exit 1 — anchor/avm found but required version not installed (avm version mismatch) + # exit 2 — anchor completely missing (sunscreen toolchain_missing) + # All three are "the binary didn't panic" — we validate the NDJSON output is well-formed. + echo "" + echo "--- Offline mode: chain build (expect exit 0, 1, or 2) ---" + build_exit=0 + output=$("$SUNSCREEN_BIN" chain build --headless 2>&1) || build_exit=$? + if [[ "$build_exit" -eq 0 || "$build_exit" -eq 1 || "$build_exit" -eq 2 ]]; then + label="anchor present, build ran" + [[ "$build_exit" -eq 1 ]] && label="anchor/avm version mismatch — clear error" + [[ "$build_exit" -eq 2 ]] && label="anchor missing — toolchain_missing reported" + echo "[PASS] chain build exits cleanly (exit $build_exit — $label)" + # Verify no panic in output + if echo "$output" | grep -q "thread 'main' panicked"; then + echo "[FAIL] chain build output contains Rust panic" + FAIL=$((FAIL + 1)) + else + PASS=$((PASS + 1)) + fi + else + echo "[FAIL] chain build unexpected exit $build_exit" + echo " output: $(echo "$output" | head -5)" + FAIL=$((FAIL + 1)) + fi +fi + +# Step 7: doctor (must not crash) +dr_exit=0 +"$SUNSCREEN_BIN" doctor 2>&1 >/dev/null || dr_exit=$? +if [[ "$dr_exit" -eq 0 || "$dr_exit" -eq 1 ]]; then + echo "[PASS] doctor exits without crash (exit $dr_exit)" + PASS=$((PASS + 1)) +else + echo "[FAIL] doctor crashed with exit $dr_exit" + FAIL=$((FAIL + 1)) +fi + +echo "" +echo "━━━ zero-to-smart-contract: PASS=$PASS FAIL=$FAIL ━━━" +[[ $FAIL -eq 0 ]] diff --git a/.claude/skills/sunscreen-flow-tests/scripts/flow-token.sh b/.claude/skills/sunscreen-flow-tests/scripts/flow-token.sh new file mode 100644 index 0000000..cd3c118 --- /dev/null +++ b/.claude/skills/sunscreen-flow-tests/scripts/flow-token.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# flow-token.sh — zero-to-SPL-token complete user journey test. +# Tests: quickstart token → scaffold instruction/account/event/error → verify files +# Toolchain: offline (SUNSCREEN_SKIP_PREFLIGHT=1) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUNSCREEN_BIN="${SUNSCREEN_BIN:-$(cd "$SCRIPT_DIR/../../../.." && pwd)/target/release/sunscreen}" +export SUNSCREEN_SKIP_PREFLIGHT=1 + +if [[ ! -x "$SUNSCREEN_BIN" ]]; then + echo "[ERROR] Binary not found: $SUNSCREEN_BIN — run: cargo build --release" + exit 1 +fi + +FLOW_DIR="${FLOW_TMPDIR:-/tmp}/sunscreen-flow-token-$(date +%s)" +WORKSPACE_NAME="testtoken$(date +%s)" +trap 'rm -rf "$FLOW_DIR"' EXIT + +mkdir -p "$FLOW_DIR" + +PASS=0 +FAIL=0 + +assert_cmd() { + local expected_exit="$1" + local description="$2" + shift 2 + local actual_exit=0 + local output + output=$("$@" 2>&1) || actual_exit=$? + if [[ "$actual_exit" -eq "$expected_exit" ]]; then + echo "[PASS] $description" + PASS=$((PASS + 1)) + else + echo "[FAIL] $description (expected exit $expected_exit, got $actual_exit)" + echo " output: $(echo "$output" | head -5)" + FAIL=$((FAIL + 1)) + fi +} + +assert_file() { + local path="$1" + local description="$2" + if [[ -e "$path" ]]; then + echo "[PASS] $description" + PASS=$((PASS + 1)) + else + echo "[FAIL] $description — file missing: $path" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== Flow: zero-to-token (SPL Token) ===" +echo " binary: $SUNSCREEN_BIN" +echo " workspace: $FLOW_DIR/$WORKSPACE_NAME" +echo "" + +cd "$FLOW_DIR" + +# Step 1: quickstart token +assert_cmd 0 "quickstart token creates workspace" \ + "$SUNSCREEN_BIN" quickstart token --name "$WORKSPACE_NAME" --non-interactive + +# Step 2: workspace structure +assert_file "$FLOW_DIR/$WORKSPACE_NAME/sunscreen.yml" "sunscreen.yml exists" +assert_file "$FLOW_DIR/$WORKSPACE_NAME/Anchor.toml" "Anchor.toml exists" + +# Step 3: scaffold instructions (no --program) +cd "$FLOW_DIR/$WORKSPACE_NAME" + +assert_cmd 0 "scaffold instruction mint_to" \ + "$SUNSCREEN_BIN" scaffold instruction mint_to + +assert_cmd 0 "scaffold instruction burn" \ + "$SUNSCREEN_BIN" scaffold instruction burn + +assert_cmd 0 "scaffold instruction transfer_checked" \ + "$SUNSCREEN_BIN" scaffold instruction transfer_checked + +# Step 4: verify instruction files +assert_file "programs/${WORKSPACE_NAME}/src/instructions/mint_to.rs" "mint_to.rs exists" +assert_file "programs/${WORKSPACE_NAME}/src/instructions/burn.rs" "burn.rs exists" +assert_file "programs/${WORKSPACE_NAME}/src/instructions/transfer_checked.rs" "transfer_checked.rs exists" + +# Step 5: scaffold accounts +assert_cmd 0 "scaffold account TokenAccount" \ + "$SUNSCREEN_BIN" scaffold account TokenAccount + +assert_cmd 0 "scaffold account MintAuthority" \ + "$SUNSCREEN_BIN" scaffold account MintAuthority + +assert_file "programs/${WORKSPACE_NAME}/src/state/token_account.rs" "token_account.rs exists" +assert_file "programs/${WORKSPACE_NAME}/src/state/mint_authority.rs" "mint_authority.rs exists" + +# Step 6: scaffold events and errors +assert_cmd 0 "scaffold event TokenMinted" \ + "$SUNSCREEN_BIN" scaffold event TokenMinted + +assert_cmd 0 "scaffold error InsufficientFunds" \ + "$SUNSCREEN_BIN" scaffold error InsufficientFunds + +assert_file "programs/${WORKSPACE_NAME}/src/events.rs" "events.rs exists" +assert_file "programs/${WORKSPACE_NAME}/src/errors.rs" "errors.rs exists" + +echo "" +echo "━━━ zero-to-token: PASS=$PASS FAIL=$FAIL ━━━" +[[ $FAIL -eq 0 ]] diff --git a/CLAUDE.md b/CLAUDE.md index 32049fc..f19c89d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,3 +59,5 @@ Greenfield Rust CLI inspired by Ignite CLI. Scope: incremental scaffolding of An | 2026-06-02 | v0.1.0 preview release prepared | .github/workflows/release.yml, .github/releases/v0.1.0.md, CHANGELOG.md, Cargo.toml, README.md, ROADMAP.md | Phase 8 distribution slice: tag-driven `cargo-dist` workflow for Linux/macOS archives and shell installer, crate version `0.1.0`, release notes, SemVer preview policy, and GitHub Release publishing path. | | 2026-06-02 | Heavy integration test harness team added | .claude/agents/{test-harness-orchestrator,test-strategist,offline-ci-owner,real-anchor-codama-owner,pinocchio-sbf-owner,serve-runtime-owner,plugin-runtime-qa,frontend-codegen-owner,release-distribution-qa,flake-perf-auditor}.md, .claude/skills/sunscreen-test-harness, .agents/skills/sunscreen-test-harness, scripts/integration-heavy.sh, docs/reference/testing.md | Adds a durable testing team and runner for "real tests": offline deterministic gate, generated workspace compile gate, real Anchor/Solana/Codama gate, real Pinocchio SBF, serve runtime, plugin runtime, frontend typecheck, cargo-dist release QA, flake/perf loops, and per-tier summary JSON for the orchestrator. | | 2026-06-02 | Docs team harness added | .claude/agents/{docs-architect,docs-tutorial-writer,docs-reference-writer,docs-designer,docs-reviewer}.md, .claude/skills/sunscreen-docs-orchestrator/ | Phase 8 docs slice: dedicated team to deliver the mdBook site (GitHub Pages) with Learn/Guides/Reference/Concepts tracks, TMDCP-style visual identity, and docs QA. Separate orchestrator from `sunscreen-orchestrator` — triggered by requests related to "docs", "site", "tutorials", "GitHub Pages", or "Phase 8 docs". The `docs-writer` agent remains responsible only for ADRs. Initial trigger: invoke `sunscreen-docs-orchestrator`. | +| 2026-06-06 | Real-usage bug fixes (BUG-001, BUG-002, BUG-003) | src/cli/chain.rs, src/cli/scaffold.rs, src/onboarding/learn.rs | BUG-001: `quickstart` relative-path double-prefix crash fixed — `create_workspace` now makes `dest` absolute via `current_dir().join(dest)`. BUG-002: `scaffold instruction/account/event/error` no longer requires `--program` when workspace has exactly one program — `resolve_program()` auto-detects. BUG-003: `learn list` now shows topic list instead of erroring. 390/390 tests, all three fixes verified end-to-end with real binary from /tmp. | +| 2026-06-06 | Full end-to-end flow test harness added | .claude/agents/flow-test-runner.md, .claude/skills/sunscreen-flow-tests/{SKILL.md,scripts/flow-nft.sh,flow-token.sh,flow-smart-contract.sh,flow-runner.sh}, updated agents: e2e-qa-fixer, ux-flow-validator, offline-ci-owner, test-harness-orchestrator | Three complete user-journey flow scripts: zero-to-NFT (18 assertions), zero-to-token (17 assertions), zero-to-smart-contract (17 assertions). All run the real binary from /tmp with isolated temp dirs. `flow-runner.sh` orchestrates all three. Mandatory in every test round and every fix cycle. 52/52 assertions pass. | diff --git a/src/cli/chain.rs b/src/cli/chain.rs index 535fa61..dfde182 100644 --- a/src/cli/chain.rs +++ b/src/cli/chain.rs @@ -586,6 +586,17 @@ pub(crate) fn create_workspace(args: &NewArgs) -> Result, /// Comma-separated struct fields, e.g. `"owner:Pubkey,total:u64"`. #[arg(long, value_name = "LIST", default_value = "")] pub fields: String, @@ -139,9 +139,9 @@ pub struct AccountArgs { pub struct EventArgs { /// Event struct name. pub name: String, - /// Parent program (must exist in `sunscreen.yml`). + /// Parent program (must exist in `sunscreen.yml`). Auto-detected when only one program exists. #[arg(long, value_name = "NAME")] - pub program: String, + pub program: Option, /// Comma-separated struct fields, e.g. `"amount:u64,user:Pubkey"`. #[arg(long, value_name = "LIST", default_value = "")] pub fields: String, @@ -155,9 +155,9 @@ pub struct EventArgs { pub struct ErrorArgs { /// Error variant name. pub name: String, - /// Parent program (must exist in `sunscreen.yml`). + /// Parent program (must exist in `sunscreen.yml`). Auto-detected when only one program exists. #[arg(long, value_name = "NAME")] - pub program: String, + pub program: Option, /// Human-readable message bound to `#[msg("…")]`. #[arg(long, value_name = "STRING", default_value = "")] pub msg: String, @@ -171,9 +171,9 @@ pub struct ErrorArgs { pub struct InstructionArgs { /// Instruction name. Must start with a letter and contain only `[a-zA-Z0-9_]`. pub name: String, - /// Parent program (must exist in `sunscreen.yml`). + /// Parent program (must exist in `sunscreen.yml`). Auto-detected when only one program exists. #[arg(long, value_name = "NAME")] - pub program: String, + pub program: Option, /// Comma-separated handler args, e.g. `"amount:u64,memo:String"`. #[arg(long, value_name = "LIST", default_value = "")] pub args: String, @@ -430,7 +430,7 @@ fn execute_recipe_step( RecipeStep::Account { name, fields } => run_account_in_workspace( &AccountArgs { name: name.clone(), - program: program.to_string(), + program: Some(program.to_string()), fields: fields.clone(), dry_run, }, @@ -442,7 +442,7 @@ fn execute_recipe_step( RecipeStep::Event { name, fields } => run_event_in_workspace( &EventArgs { name: name.clone(), - program: program.to_string(), + program: Some(program.to_string()), fields: fields.clone(), dry_run, }, @@ -454,7 +454,7 @@ fn execute_recipe_step( RecipeStep::Error { name, message } => run_error_in_workspace( &ErrorArgs { name: name.clone(), - program: program.to_string(), + program: Some(program.to_string()), msg: message.clone(), dry_run, }, @@ -471,7 +471,7 @@ fn execute_recipe_step( } => run_instruction_in_workspace( &InstructionArgs { name: name.clone(), - program: program.to_string(), + program: Some(program.to_string()), args: args.clone(), accounts: accounts.clone(), emit: emit.clone(), @@ -683,8 +683,6 @@ fn run_instruction_in_workspace( if let Some(emit) = &args.emit { validate_ident(emit, "--emit event name")?; } - let ix_snake = args.name.to_snake_case(); - let program_snake = args.program.to_snake_case(); let parsed_args = parse_args(&args.args)?; let parsed_accounts = parse_accounts(&args.accounts)?; @@ -692,7 +690,10 @@ fn run_instruction_in_workspace( // 1. Locate workspace. let ws = workspace::find_root(workspace_root).map_err(map_ws_err)?; ensure_anchor_scaffolding(&ws)?; - let program: &ProgramView = workspace::find_program(&ws, &args.program).map_err(map_ws_err)?; + let program_name = resolve_program(args.program.as_deref(), &ws)?; + let ix_snake = args.name.to_snake_case(); + let program_snake = program_name.to_snake_case(); + let program: &ProgramView = workspace::find_program(&ws, &program_name).map_err(map_ws_err)?; let emit_fields = if let Some(emit) = &args.emit { event_fields_for_emit(program, emit)? } else { @@ -859,7 +860,7 @@ fn run_instruction_in_workspace( if args.dry_run { if !quiet { - emit_dry_run(json, &args.name, &args.program, &plan_files, &lib_status); + emit_dry_run(json, &args.name, &program_name, &plan_files, &lib_status); } return Ok(0); } @@ -911,7 +912,7 @@ fn run_instruction_in_workspace( let payload = serde_json::json!({ "ok": true, "instruction": ix_snake, - "program": args.program, + "program": program_name, "files": plan_files, "lib_rs_patched": lib_status.patched, "unchanged": unchanged, @@ -931,7 +932,7 @@ fn run_instruction_in_workspace( } else { println!( "scaffolded instruction `{ix_snake}` for program `{}` ({} files)", - args.program, + program_name, plan_files.len() ); for f in &plan_files { @@ -1435,6 +1436,32 @@ fn relative_to(root: &std::path::Path, target: &std::path::Path) -> PathBuf { .unwrap_or_else(|_| target.to_path_buf()) } +/// Resolve the program name from an optional CLI flag. +/// When `--program` is omitted and there is exactly one program in the workspace, +/// that program is used automatically (with a note printed to stderr). +/// When omitted and there are 0 or 2+ programs, a clear error is returned. +fn resolve_program( + explicit: Option<&str>, + ws: &workspace::WorkspaceRoot, +) -> Result { + if let Some(name) = explicit { + return Ok(name.to_string()); + } + match ws.programs.as_slice() { + [] => Err(SunscreenError::UserInput( + "workspace has no programs; run `sunscreen scaffold program ` first".to_string(), + )), + [single] => { + eprintln!("note: using program `{}`", single.name); + Ok(single.name.clone()) + } + _ => Err(SunscreenError::UserInput(format!( + "workspace has {} programs; specify one with `--program `", + ws.programs.len() + ))), + } +} + fn map_ws_err(e: WorkspaceError) -> SunscreenError { match e { WorkspaceError::NotFound => SunscreenError::WorkspaceMissing(e.to_string()), @@ -1537,11 +1564,12 @@ fn run_account_in_workspace( validate_ident(&args.name, "account name")?; let fields = parse_fields(&args.fields)?; let account_snake = args.name.to_snake_case(); - let program_snake = args.program.to_snake_case(); let ws = workspace::find_root(workspace_root).map_err(map_ws_err)?; ensure_anchor_scaffolding(&ws)?; - let program: &ProgramView = workspace::find_program(&ws, &args.program).map_err(map_ws_err)?; + let program_name = resolve_program(args.program.as_deref(), &ws)?; + let program_snake = program_name.to_snake_case(); + let program: &ProgramView = workspace::find_program(&ws, &program_name).map_err(map_ws_err)?; let state_dir = program.src_dir.join("state"); let account_abs = state_dir.join(format!("{account_snake}.rs")); @@ -1653,7 +1681,7 @@ fn run_account_in_workspace( if args.dry_run { if !quiet { - emit_noun_dry_run(json, "account", &args.name, &args.program, &plan_files); + emit_noun_dry_run(json, "account", &args.name, &program_name, &plan_files); } return Ok(0); } @@ -1687,7 +1715,7 @@ fn run_account_in_workspace( json, "account", &args.name, - &args.program, + &program_name, &plan_files, &segments_patched, unchanged, @@ -1808,11 +1836,12 @@ fn run_event_in_workspace( validate_ident(&args.name, "event name")?; let fields = parse_fields(&args.fields)?; let event_pascal = args.name.to_pascal_case(); - let program_snake = args.program.to_snake_case(); let ws = workspace::find_root(workspace_root).map_err(map_ws_err)?; ensure_anchor_scaffolding(&ws)?; - let program: &ProgramView = workspace::find_program(&ws, &args.program).map_err(map_ws_err)?; + let program_name = resolve_program(args.program.as_deref(), &ws)?; + let program_snake = program_name.to_snake_case(); + let program: &ProgramView = workspace::find_program(&ws, &program_name).map_err(map_ws_err)?; let events_abs = program.src_dir.join("events.rs"); let events_rel = relative_to(&ws.root, &events_abs); @@ -1913,7 +1942,7 @@ fn run_event_in_workspace( if args.dry_run { if !quiet { - emit_noun_dry_run(json, "event", &args.name, &args.program, &plan_files); + emit_noun_dry_run(json, "event", &args.name, &program_name, &plan_files); } return Ok(0); } @@ -1944,7 +1973,7 @@ fn run_event_in_workspace( json, "event", &args.name, - &args.program, + &program_name, &plan_files, &segments_patched, unchanged, @@ -2127,12 +2156,13 @@ fn run_error_in_workspace( ) -> Result { validate_ident(&args.name, "error variant name")?; let variant_pascal = args.name.to_pascal_case(); - let program_snake = args.program.to_snake_case(); - let enum_name = format!("{}_error", program_snake).to_pascal_case(); let ws = workspace::find_root(workspace_root).map_err(map_ws_err)?; ensure_anchor_scaffolding(&ws)?; - let program: &ProgramView = workspace::find_program(&ws, &args.program).map_err(map_ws_err)?; + let program_name = resolve_program(args.program.as_deref(), &ws)?; + let program_snake = program_name.to_snake_case(); + let enum_name = format!("{}_error", program_snake).to_pascal_case(); + let program: &ProgramView = workspace::find_program(&ws, &program_name).map_err(map_ws_err)?; let errors_abs = program.src_dir.join("errors.rs"); let errors_rel = relative_to(&ws.root, &errors_abs); @@ -2220,7 +2250,7 @@ fn run_error_in_workspace( if args.dry_run { if !quiet { - emit_noun_dry_run(json, "error", &args.name, &args.program, &plan_files); + emit_noun_dry_run(json, "error", &args.name, &program_name, &plan_files); } return Ok(0); } @@ -2251,7 +2281,7 @@ fn run_error_in_workspace( json, "error", &args.name, - &args.program, + &program_name, &plan_files, &segments_patched, unchanged, diff --git a/src/onboarding/learn.rs b/src/onboarding/learn.rs index cb925bb..a1a2ea0 100644 --- a/src/onboarding/learn.rs +++ b/src/onboarding/learn.rs @@ -46,8 +46,8 @@ const TOPICS: &[Topic] = &[ pub fn run(args: &LearnArgs, json: bool) -> Result { match args.topic.as_deref() { + Some("list") | None => list_topics(json), Some(topic) => render_topic(topic, json), - None => list_topics(json), } } From cf124878fefa46d7895b7f51637d388f89c99163 Mon Sep 17 00:00:00 2001 From: Pantani Date: Sat, 6 Jun 2026 04:23:30 -0300 Subject: [PATCH 3/3] fix review comments: doctor exit codes, NDJSON check, redirect order, cleanup trap, markdown lints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - flow-nft.sh: accept doctor exit 0/1/2 in offline flows (|| true did not protect FAIL counter) - flow-nft.sh: clarify idempotency comment (same-content → exit 0, not 4) - flow-smart-contract.sh: add NDJSON contract check for chain_build_started/finished events - flow-smart-contract.sh: fix redirect order 2>&1 >/dev/null → >/dev/null 2>&1 (SC2069) - flow-smart-contract.sh: accept doctor exit 2 (missing tools) as valid offline outcome - flow-token.sh: preserve FLOW_DIR on failure for debugging instead of unconditional cleanup - fix.rs: remove redundant -s from rustup installer args (use -y only) - e2e-qa-fixer.md, flow-test-runner.md: add language tag to bare code fences (MD040) - flow-test-runner.md: align offline build exit docs with implemented behavior (exit 0/1/2) - test-harness-orchestrator.md: add blank lines around fenced block (MD031) --- .claude/agents/e2e-qa-fixer.md | 2 +- .claude/agents/flow-test-runner.md | 5 +++-- .claude/agents/test-harness-orchestrator.md | 2 ++ .../sunscreen-flow-tests/scripts/flow-nft.sh | 19 ++++++++++++++----- .../scripts/flow-smart-contract.sh | 12 +++++++++--- .../scripts/flow-token.sh | 2 +- src/toolchain/fix.rs | 2 +- 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/.claude/agents/e2e-qa-fixer.md b/.claude/agents/e2e-qa-fixer.md index 53e233b..77ea920 100644 --- a/.claude/agents/e2e-qa-fixer.md +++ b/.claude/agents/e2e-qa-fixer.md @@ -64,7 +64,7 @@ bash .claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh ## Bug report format (when filing, not fixing) -``` +```text FLOW: zero-to-NFT STEP: scaffold instruction mint CMD: sunscreen scaffold instruction mint diff --git a/.claude/agents/flow-test-runner.md b/.claude/agents/flow-test-runner.md index 33fc3b1..591858a 100644 --- a/.claude/agents/flow-test-runner.md +++ b/.claude/agents/flow-test-runner.md @@ -25,7 +25,7 @@ Run complete user journeys, not isolated commands. A "journey" means: 2. Create a workspace (via `quickstart` or `chain new`) 3. Add scaffolding (instruction, account, event, error) 4. Verify every generated file exists on disk -5. Run build (expect exit 0 with real Anchor, exit 2 without) +5. Run build (expect exit 0 with real Anchor, exit 0/1/2 without — all are valid offline outcomes) 6. Clean up the temp dir If a step fails, capture the exact output and exit code — that is the bug report. @@ -80,7 +80,8 @@ For each PASS flow, spot-check: ## Bug reporting format When a step fails, report: -``` + +```text FLOW: zero-to-NFT STEP: scaffold instruction mint CMD: sunscreen scaffold instruction mint diff --git a/.claude/agents/test-harness-orchestrator.md b/.claude/agents/test-harness-orchestrator.md index 29c4ef3..9d7868c 100644 --- a/.claude/agents/test-harness-orchestrator.md +++ b/.claude/agents/test-harness-orchestrator.md @@ -28,11 +28,13 @@ Coordinate the heavy validation team for `sunscreen`. Turn the user's request in 4. **Always dispatch `flow-test-runner` in parallel with or immediately after offline-ci-owner.** Flow tests run the real binary from /tmp and catch path bugs, relative-path issues, and auto-detection failures that unit tests cannot see. Commands: + ```bash export SUNSCREEN_BIN="$(pwd)/target/release/sunscreen" export SUNSCREEN_SKIP_PREFLIGHT=1 bash .claude/skills/sunscreen-flow-tests/scripts/flow-runner.sh ``` + A flow FAIL is a blocker — it blocks the round regardless of unit test results. 5. Read the most recent `summary.json` and classify tiers. 6. If the user asked for real toolchain, dispatch: diff --git a/.claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh b/.claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh index 72fbdd1..792a578 100644 --- a/.claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh +++ b/.claude/skills/sunscreen-flow-tests/scripts/flow-nft.sh @@ -99,8 +99,9 @@ assert_cmd 0 "scaffold error InvalidMint (no --program)" \ assert_file "programs/${WORKSPACE_NAME}/src/errors.rs" \ "errors.rs created" -# Step 8: idempotency — scaffolding the same name again must exit 0 (no-op), not crash -assert_cmd 0 "duplicate instruction mint → exit 0 (idempotent no-op)" \ +# Step 8: idempotency — re-running scaffold for an existing name with identical content must exit 0 +# (same content = silent no-op; a conflict with different content would be exit 4) +assert_cmd 0 "duplicate instruction mint → exit 0 (idempotent, same content)" \ "$SUNSCREEN_BIN" scaffold instruction mint # Step 9: learn (basic sanity) @@ -113,9 +114,17 @@ assert_cmd 0 "learn list" \ assert_cmd 0 "learn pda" \ "$SUNSCREEN_BIN" learn pda -# Step 10: doctor (offline — may report missing tools, must not crash) -assert_cmd 0 "doctor exits 0 (reports tool status)" \ - "$SUNSCREEN_BIN" doctor || true # doctor may exit non-zero if tools missing — accept both +# Step 10: doctor (offline — may report missing tools; exit 0=ok, 1=partial, 2=missing required) +dr_exit=0 +dr_out=$("$SUNSCREEN_BIN" doctor 2>&1) || dr_exit=$? +if [[ "$dr_exit" -eq 0 || "$dr_exit" -eq 1 || "$dr_exit" -eq 2 ]]; then + echo "[PASS] doctor exits without crash (exit $dr_exit)" + PASS=$((PASS + 1)) +else + echo "[FAIL] doctor unexpected exit $dr_exit" + echo " output: $(echo "$dr_out" | head -5)" + FAIL=$((FAIL + 1)) +fi echo "" echo "━━━ zero-to-NFT: PASS=$PASS FAIL=$FAIL ━━━" diff --git a/.claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh b/.claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh index 305fbce..28e2151 100644 --- a/.claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh +++ b/.claude/skills/sunscreen-flow-tests/scripts/flow-smart-contract.sh @@ -134,8 +134,14 @@ else if echo "$output" | grep -q "thread 'main' panicked"; then echo "[FAIL] chain build output contains Rust panic" FAIL=$((FAIL + 1)) - else + elif echo "$output" | grep -q '"event":"chain_build_started"' && \ + echo "$output" | grep -q '"event":"chain_build_finished"'; then + echo "[PASS] chain build NDJSON contract intact (chain_build_started + chain_build_finished)" PASS=$((PASS + 1)) + else + echo "[FAIL] chain build NDJSON events missing (chain_build_started or chain_build_finished absent)" + echo " first 5 lines: $(echo "$output" | head -5)" + FAIL=$((FAIL + 1)) fi else echo "[FAIL] chain build unexpected exit $build_exit" @@ -146,8 +152,8 @@ fi # Step 7: doctor (must not crash) dr_exit=0 -"$SUNSCREEN_BIN" doctor 2>&1 >/dev/null || dr_exit=$? -if [[ "$dr_exit" -eq 0 || "$dr_exit" -eq 1 ]]; then +"$SUNSCREEN_BIN" doctor >/dev/null 2>&1 || dr_exit=$? +if [[ "$dr_exit" -eq 0 || "$dr_exit" -eq 1 || "$dr_exit" -eq 2 ]]; then echo "[PASS] doctor exits without crash (exit $dr_exit)" PASS=$((PASS + 1)) else diff --git a/.claude/skills/sunscreen-flow-tests/scripts/flow-token.sh b/.claude/skills/sunscreen-flow-tests/scripts/flow-token.sh index cd3c118..0cf5a37 100644 --- a/.claude/skills/sunscreen-flow-tests/scripts/flow-token.sh +++ b/.claude/skills/sunscreen-flow-tests/scripts/flow-token.sh @@ -15,7 +15,7 @@ fi FLOW_DIR="${FLOW_TMPDIR:-/tmp}/sunscreen-flow-token-$(date +%s)" WORKSPACE_NAME="testtoken$(date +%s)" -trap 'rm -rf "$FLOW_DIR"' EXIT +trap 'if [[ ${FAIL:-0} -eq 0 ]]; then rm -rf "$FLOW_DIR"; else echo "[INFO] Preserving failed flow dir: $FLOW_DIR"; fi' EXIT mkdir -p "$FLOW_DIR" diff --git a/src/toolchain/fix.rs b/src/toolchain/fix.rs index 0c98a53..3ff6d46 100644 --- a/src/toolchain/fix.rs +++ b/src/toolchain/fix.rs @@ -327,7 +327,7 @@ fn recipe(runner: &R, report: &ToolReport) -> Result