Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,13 @@ Code in:

Run:
```bash
./scripts/preflight-browser-smoke.sh
cd examples/smoke-web && npm run typecheck && npm run build
cd examples/web && npm run build
```

The preflight is required after any branch switch: these apps alias the SDK packages to `packages/*/dist/index.js`, and `dist/` is gitignored, so a build left over from another protocol line survives the checkout and fails only at MASM compile time.

Manual canary:
- use `smoke-test-ts-multisig-sdk`

Expand Down
49 changes: 36 additions & 13 deletions .agents/skills/smoke-test-ts-multisig-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ If the commitment does not round-trip into `status().multisig.guardianPubkey` af

## Testing Deployed SDKs

`examples/smoke-web/package.json` pins `@openzeppelin/miden-multisig-client` and `@openzeppelin/guardian-client` to `file:../../packages/...`, so `npm run dev` smokes the **workspace source**, not the npm release. When the user asks to smoke "the deployed SDK" or "the released version":
`examples/smoke-web/package.json` pins `@openzeppelin/miden-multisig-client` and `@openzeppelin/guardian-client` to `file:../../packages/...`, and `vite.config.ts` additionally aliases both to `packages/<name>/dist/index.js`. So `npm run dev` smokes the **workspace build output**, not the npm release and not `src/`. When the user asks to smoke "the deployed SDK" or "the released version":

1. Create a scratch Vite (or equivalent) project outside the workspace (e.g. `/tmp/guardian-ts-smoke-<version>`).
2. Declare the published packages pinned to the release under test:
Expand All @@ -48,6 +48,24 @@ If the commitment does not round-trip into `status().multisig.guardianPubkey` af

Treat workspace-path smoke-web runs and deployed-npm scratch-project runs as different smoke targets. Never collapse them in the report.

## Stale Build Output

`packages/*/dist` is gitignored, so `git checkout` never touches it. A dist built on another protocol line (for example a `miden-0.16` branch) keeps being served on a 0.15 branch until something rebuilds it, and the apps load `dist`, not `src`. Nothing in a source-level check can see this.

The signature is a MASM assembler error at account creation, most often:

```text
Failed to compile account component: invalid syntax
```

That means the MASM baked into the stale `dist` is a different dialect from the `@miden-sdk/miden-sdk` WASM assembler the app loaded. Cross-line MASM differences to look for: the `openzeppelin::auth::*` namespace versus `miden::standards::auth::*`, and `pub use multisig::name` versus `pub use {name} from module`.

The same applies to `node_modules`: another branch can leave an out-of-range `@miden-sdk/miden-sdk` installed that the workspace's own lockfile does not pin. Compare installed against the lockfile, not against the `package.json` range, because a caret range hides it.

`./scripts/preflight-browser-smoke.sh` detects and repairs both. Rebuild with `npm run clean && npm run build`, never a bare `build`: export names differ across lines, so `tsc` alone leaves orphaned files from the previous build. Restart any running dev server afterwards, and clear `examples/*/node_modules/.vite`.

Do not report this class of failure as an SDK or contract bug until the preflight passes.

## Browser Automation

The smoke harness is designed to be driven from DevTools (`window.smoke.*`), which makes it drivable by browser-automation tools when available. Prefer in this order:
Expand All @@ -71,29 +89,34 @@ When reporting, capture the concrete tool invocation path used (Chrome MCP vs Cl
- `examples/_shared/multisig-browser/src/multisigApi.ts`
- `examples/_shared/multisig-browser/src/initClient.ts`
- `examples/smoke-web/src/App.tsx`
2. Run targeted TypeScript validation before manual smoke:
2. Run the browser preflight before anything else. It is mandatory after any branch switch:
```bash
./scripts/preflight-browser-smoke.sh
```
It reports, fixes, then re-verifies two things source-level checks cannot see: a workspace whose installed `@miden-sdk/miden-sdk` does not match its own lockfile pin, and a `packages/*/dist` older than its `src`. Use `--check` to report without writing. See Stale Build Output below for why this class of failure is silent.
3. Run targeted TypeScript validation before manual smoke:
```bash
cd packages/miden-multisig-client && npm test
cd examples/smoke-web && npm run typecheck && npm run build
cd examples/web && npm run build
```
3. Start one GUARDIAN server from the repo root:
4. Start one GUARDIAN server from the repo root:
```bash
cargo run -p guardian-server --bin server
```
4. Start the smoke harness:
5. Start the smoke harness:
```bash
cd examples/smoke-web && npm run dev
```
5. Open the smoke harness in separate real browsers or fully isolated browser profiles. Do not rely on same-profile parallel tabs. Prefer Chrome + Brave or Chrome + Firefox when driving concurrent cosigners.
6. Let the page-load bootstrap settle first. If `await window.smoke.status()` is not `ready`, or you need to override the default endpoints or signer settings, initialize the session with `window.smoke.initSession(...)`.
7. Default to local signers and Falcon unless the prompt explicitly asks for ECDSA, Para, or Miden Wallet.
8. Record each browser's signer commitment from `await window.smoke.status()` before account creation.
9. Create the multisig in one browser by passing the other browsers' commitments to `createAccount`.
10. Load and sync the account in the other browsers with `loadAccount` and `sync`.
11. Choose the smallest workflow from `references/workflow-matrix.md`.
12. Record the exact browser/profile labels, endpoints, signer source, signature scheme, workflow, observed result, and timing data.
13. Compare the recorded timings with `references/timing-baseline.md`.
6. Open the smoke harness in separate real browsers or fully isolated browser profiles. Do not rely on same-profile parallel tabs. Prefer Chrome + Brave or Chrome + Firefox when driving concurrent cosigners.
7. Let the page-load bootstrap settle first. If `await window.smoke.status()` is not `ready`, or you need to override the default endpoints or signer settings, initialize the session with `window.smoke.initSession(...)`.
8. Default to local signers and Falcon unless the prompt explicitly asks for ECDSA, Para, or Miden Wallet.
9. Record each browser's signer commitment from `await window.smoke.status()` before account creation.
10. Create the multisig in one browser by passing the other browsers' commitments to `createAccount`.
11. Load and sync the account in the other browsers with `loadAccount` and `sync`.
12. Choose the smallest workflow from `references/workflow-matrix.md`.
13. Record the exact browser/profile labels, endpoints, signer source, signature scheme, workflow, observed result, and timing data.
14. Compare the recorded timings with `references/timing-baseline.md`.

## Baseline Harness

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ Map the changed area to the smallest browser smoke workflow that still proves th

## Environment

Run validation first:
Run the preflight first, then validation:

```bash
./scripts/preflight-browser-smoke.sh
cd packages/miden-multisig-client && npm test
cd examples/smoke-web && npm run typecheck && npm run build
cd examples/web && npm run build
```

The preflight is mandatory after any branch switch. The apps load `packages/*/dist`, which is gitignored and survives checkouts; see Stale Build Output in `SKILL.md`.

Primary smoke server commands:

```bash
Expand Down
220 changes: 220 additions & 0 deletions scripts/preflight-browser-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
#
# Preflight for the browser example harnesses (examples/smoke-web, examples/web).
#
# Both apps alias @openzeppelin/miden-multisig-client and @openzeppelin/guardian-client
# to packages/<name>/dist/index.js in their vite.config.ts, so they run the BUILT
# package, never src/. dist/ is gitignored, which means it survives every branch
# switch: a dist built on another protocol line (e.g. miden-0.16) keeps being served
# on a 0.15 branch until something rebuilds it. The MASM baked into that dist then
# meets a different WASM assembler and fails with a bare
# "Failed to compile account component: invalid syntax".
#
# The same applies to node_modules: a foreign branch can leave an out-of-range
# @miden-sdk/miden-sdk installed that the workspace's own lockfile does not pin.
#
# This script reports what drifted, fixes it, then re-verifies.
#
# Usage:
# ./scripts/preflight-browser-smoke.sh # report, fix, verify
# ./scripts/preflight-browser-smoke.sh --check # report only, never write
#
# Exit codes: 0 = clean (or all issues fixed), 1 = drift remains / fix failed,
# 2 = usage error.

set -uo pipefail

CHECK_ONLY=0
case "$#" in
0) ;;
1) case "$1" in
--check) CHECK_ONLY=1 ;;
*) echo "error: expected --check or no arguments" >&2; exit 2 ;;
esac ;;
*) echo "error: expected --check or no arguments" >&2; exit 2 ;;
esac

ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
echo "error: not inside a git repository" >&2
exit 1
}
cd "$ROOT"

# Packages the browser apps alias to dist/index.js. Keep in sync with the
# resolve.alias blocks in examples/web/vite.config.ts and examples/smoke-web/vite.config.ts.
DIST_PACKAGES=(
packages/guardian-client
packages/miden-multisig-client
)

# Workspaces that declare @miden-sdk/miden-sdk and must match their own lockfile pin.
SDK_WORKSPACES=(
packages/miden-multisig-client
examples/_shared/multisig-browser
examples/smoke-web
examples/web
examples/operator-smoke-web
)

RED=$'\033[31m'; GRN=$'\033[32m'; YEL=$'\033[33m'; DIM=$'\033[2m'; OFF=$'\033[0m'
[[ -t 1 ]] || { RED=""; GRN=""; YEL=""; DIM=""; OFF=""; }

ok() { printf ' %s%-46s%s %s\n' "$DIM" "$1" "$OFF" "${GRN}ok${OFF}"; }
bad() { printf ' %s%-46s%s %s\n' "$DIM" "$1" "$OFF" "${RED}$2${OFF}"; }
note() { printf ' %s%s%s\n' "$DIM" "$1" "$OFF"; }

# Version installed in a workspace's node_modules, or empty.
installed_sdk() {
python3 -c "
import json,sys
try: print(json.load(open('$1/node_modules/@miden-sdk/miden-sdk/package.json'))['version'])
except Exception: print('')
" 2>/dev/null
}

# Version the workspace's own lockfile pins, or empty.
locked_sdk() {
python3 -c "
import json,sys
try:
d=json.load(open('$1/package-lock.json'))['packages']
print(d.get('node_modules/@miden-sdk/miden-sdk',{}).get('version',''))
except Exception: print('')
" 2>/dev/null
}

declared_sdk() {
python3 -c "
import json,sys
try:
d=json.load(open('$1/package.json'))
print(d.get('dependencies',{}).get('@miden-sdk/miden-sdk','') or d.get('devDependencies',{}).get('@miden-sdk/miden-sdk',''))
except Exception: print('')
" 2>/dev/null
}

# Count src/*.ts files newer than the built entrypoint. Echoes STALE reason or empty.
dist_staleness() {
local pkg="$1" entry="$1/dist/index.js"
[[ -d "$pkg/dist" ]] || { echo "dist/ missing"; return; }
[[ -f "$entry" ]] || { echo "dist/index.js missing"; return; }
local n
n=$(find "$pkg/src" -name '*.ts' -newer "$entry" 2>/dev/null | wc -l | tr -d ' ')
[[ "$n" -gt 0 ]] && echo "$n src file(s) newer than dist"
Comment on lines +101 to +103
}

SDK_DRIFT=(); LOCK_ERRORS=(); STALE=(); ISSUES=0

scan() {
SDK_DRIFT=(); LOCK_ERRORS=(); STALE=()

printf '\n%s[1/4]%s @miden-sdk/miden-sdk: installed vs lockfile pin\n' "$YEL" "$OFF"
for w in "${SDK_WORKSPACES[@]}"; do
[[ -d "$w" ]] || continue
local dec ins loc
dec="$(declared_sdk "$w")"; [[ -n "$dec" ]] || continue
ins="$(installed_sdk "$w")"; loc="$(locked_sdk "$w")"
if [[ -z "$loc" ]]; then
bad "$w" "lockfile pin missing"
note "declared $dec -> run npm install to update package-lock.json"
LOCK_ERRORS+=("$w"); continue
fi
if [[ -z "$ins" ]]; then
bad "$w" "not installed"; note "declared $dec -> run npm install"
SDK_DRIFT+=("$w"); continue
fi
if [[ "$ins" != "$loc" ]]; then
bad "$w" "MISMATCH"
note "declared $dec | lockfile $loc | installed $ins"
note "out-of-range install from another branch -> npm ci"
SDK_DRIFT+=("$w")
else
ok "$w $ins"
fi
Comment thread
zeljkoX marked this conversation as resolved.
done

printf '\n%s[2/4]%s built dist freshness (apps alias dist, not src)\n' "$YEL" "$OFF"
for p in "${DIST_PACKAGES[@]}"; do
[[ -d "$p" ]] || continue
local why
why="$(dist_staleness "$p")"
if [[ -n "$why" ]]; then
bad "$p/dist" "STALE"; note "$why"
STALE+=("$p")
else
ok "$p/dist"
fi
done

ISSUES=$(( ${#SDK_DRIFT[@]} + ${#LOCK_ERRORS[@]} + ${#STALE[@]} ))
}

scan
FOUND=$ISSUES

if [[ "$ISSUES" -eq 0 ]]; then
printf '\n%s[3/4]%s rebuild: not needed\n' "$YEL" "$OFF"
printf '%s[4/4]%s vite caches: left alone\n' "$YEL" "$OFF"
printf '\n%sPREFLIGHT OK%s browser harnesses are safe to smoke\n\n' "$GRN" "$OFF"
exit 0
fi

if [[ "$CHECK_ONLY" -eq 1 ]]; then
printf '\n%s[3/4]%s rebuild: skipped (--check)\n' "$YEL" "$OFF"
printf '%s[4/4]%s vite caches: skipped (--check)\n' "$YEL" "$OFF"
printf '\n%sPREFLIGHT FAILED%s %d issue(s); re-run without --check to fix\n\n' "$RED" "$OFF" "$FOUND"
exit 1
fi

if [[ ${#SDK_DRIFT[@]} -eq 0 && ${#STALE[@]} -eq 0 ]]; then
printf '\n%s[3/4]%s rebuild: skipped (lockfile pin missing; npm ci cannot create it)\n' "$YEL" "$OFF"
printf '%s[4/4]%s vite caches: left alone\n' "$YEL" "$OFF"
printf '\n%sPREFLIGHT FAILED%s %d issue(s); add the missing lockfile pin(s) then re-run\n\n' "$RED" "$OFF" "$FOUND"
exit 1
fi

printf '\n%s[3/4]%s fixing\n' "$YEL" "$OFF"
FIX_FAILED=0

for w in ${SDK_DRIFT[@]+"${SDK_DRIFT[@]}"}; do
printf ' npm ci %s ... ' "$w"
if (cd "$w" && npm ci >/dev/null 2>&1); then
echo "${GRN}ok${OFF}"
# A reinstall invalidates anything already built here.
for p in "${DIST_PACKAGES[@]}"; do
[[ "$p" == "$w" ]] && { case " ${STALE[*]+${STALE[*]}} " in *" $p "*) ;; *) STALE+=("$p");; esac; }
done
else
echo "${RED}FAILED${OFF}"; FIX_FAILED=1
fi
done

for p in ${STALE[@]+"${STALE[@]}"}; do
# clean, not a bare build: export names change across protocol lines, and tsc
# leaves orphaned files from the previous build behind.
printf ' clean+build %s ... ' "$p"
if (cd "$p" && npm run clean >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
Comment on lines +195 to +196
echo "${GRN}ok${OFF}"
else
echo "${RED}FAILED${OFF}"; FIX_FAILED=1
fi
done

printf '\n%s[4/4]%s clearing vite dep caches\n' "$YEL" "$OFF"
shopt -s nullglob
for c in examples/*/node_modules/.vite examples/_shared/*/node_modules/.vite; do
rm -rf "$c" && printf ' removed %s\n' "$c"
done
shopt -u nullglob

printf '\n%sre-verifying%s\n' "$DIM" "$OFF"
scan

if [[ "$ISSUES" -eq 0 && "$FIX_FAILED" -eq 0 ]]; then
printf '\n%sPREFLIGHT FIXED%s %d issue(s) resolved\n' "$GRN" "$OFF" "$FOUND"
printf '%sRestart any running vite dev server so it picks up the rebuilt dist.%s\n\n' "$DIM" "$OFF"
exit 0
fi

printf '\n%sPREFLIGHT FAILED%s %d issue(s) remain after fix\n\n' "$RED" "$OFF" "$ISSUES"
exit 1
Loading