diff --git a/.github/workflows/full-dev-path.yml b/.github/workflows/full-dev-path.yml new file mode 100644 index 000000000000..5eee9fa514a0 --- /dev/null +++ b/.github/workflows/full-dev-path.yml @@ -0,0 +1,49 @@ +# Tests that the installed Aztec toolchain works end-to-end. +# Exercises the full developer onboarding path: aztec init -> compile -> test -> start -> codegen -> TS end-to-end test. +name: Full Dev Path Test + +on: + workflow_dispatch: + inputs: + version: + description: "Version to install (e.g. latest, nightly, 4.3.0, 4.3.0-nightly.20260420)" + required: true + type: string + push: + tags: + - "v*" + +jobs: + full-dev-path: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + VERSION: ${{ github.event.inputs.version || github.ref_name }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Run full dev path test + run: ./aztec-up/test/full-dev-path/run-test.sh + + - name: Notify Slack on success + if: success() && github.event_name != 'workflow_dispatch' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + run: | + export CI=1 + ./ci3/slack_notify "#team-fairies" \ + "Full Dev Path Test passed for version ${VERSION} :white_check_mark:" + + - name: Notify Slack and dispatch ClaudeBox on failure + if: failure() && github.event_name != 'workflow_dispatch' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }} + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + export CI=1 + ./ci3/slack_notify_with_claudebox_kickoff "#team-fairies" \ + "Full Dev Path Test FAILED (version ${VERSION}): <${RUN_URL}|View Run>" \ + "Full dev path test failed for version ${VERSION}. CI run: ${RUN_URL}. Investigate the failure and explain the root cause." \ + --link "$RUN_URL" diff --git a/aztec-up/test/full-dev-path/README.md b/aztec-up/test/full-dev-path/README.md new file mode 100644 index 000000000000..fa3ef2896770 --- /dev/null +++ b/aztec-up/test/full-dev-path/README.md @@ -0,0 +1,35 @@ +# Full Dev Path Test + +Tests that the installed Aztec toolchain works end-to-end. Exercises the complete developer onboarding path: + +1. `aztec init` - scaffold a new workspace with a Counter contract and test crate +2. `aztec compile` - compile the scaffolded contract +3. `aztec test` - run the TXE tests from the scaffold's test crate +4. `aztec start --local-network` - start a local sandbox (anvil + aztec node) +5. `aztec codegen` - generate TypeScript bindings from the compiled artifact +6. TS end-to-end test - run a `node --test` suite inside the scaffolded workspace that imports the codegen'd `CounterContract`, stands up an in-process wallet + PXE via `@aztec/wallets/embedded`, deploys Counter, calls `increment`, and reads the value back through the `get_counter` utility function + +## Running + +With an existing local install (fast inner loop): +```bash +SKIP_INSTALL=1 ./run-test.sh +``` + +With a fresh install from a specific version: +```bash +VERSION=4.3.0 ./run-test.sh +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `SKIP_INSTALL` | Set to `1` to skip the installer and use the already-installed toolchain. | +| `VERSION` | Version to install (e.g. `4.3.0` or `v4.3.0`). Required unless `SKIP_INSTALL=1`. | + +## Architecture + +- **`run-test.sh`** - Bash launcher. Runs the aztec installer (unless skipped), sets up PATH, then `exec node full-dev-path.ts`. +- **`full-dev-path.ts`** - Orchestrator. Runs each CLI step against the installed toolchain and, after codegen, copies `counter.test.ts` into the scaffolded workspace and spawns `node --test` on it. Each phase is wrapped in `step(name, fn)` so failures clearly identify which step broke. Always emits a machine-readable result line for CI/Slack integration: `TEST_RESULT=pass version=...` on success, or `TEST_RESULT=fail step=... version=... error="..."` on failure (with a full banner printed above it). +- **`counter.test.ts`** - The `node:test` suite that drives the deployed Counter end-to-end through the codegen'd bindings. Lives here as a template; copied into the workspace at test time so it can statically `import { CounterContract } from './artifacts/Counter.js'` with real codegen types and resolve `@aztec/*` via the workspace's `node_modules` symlink to the install. diff --git a/aztec-up/test/full-dev-path/counter.test.ts b/aztec-up/test/full-dev-path/counter.test.ts new file mode 100644 index 000000000000..12594f1694a9 --- /dev/null +++ b/aztec-up/test/full-dev-path/counter.test.ts @@ -0,0 +1,43 @@ +// End-to-end test for the scaffolded Counter contract. +// +// Copied into the scaffolded workspace at test time by ../test.ts, then executed with +// `node --test`. Runs from inside the workspace so that: +// - `./artifacts/Counter.js` resolves to the codegen'd bindings (and its types flow). +// - `@aztec/*` imports resolve via the workspace's `node_modules` symlink to the +// installed Aztec toolchain — i.e. the same packages a real user would have. +// +// The test expects an `aztec start --local-network` node reachable at NODE_URL. It uses the +// pre-funded test0 account that local-network already deployed, stands up an in-process +// EmbeddedWallet + PXE, deploys a fresh Counter, and exercises a full round trip through +// the codegen'd bindings (send + simulate). + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { getInitialTestAccountsData } from '@aztec/accounts/testing'; +import { EmbeddedWallet } from '@aztec/wallets/embedded'; + +import { CounterContract } from './artifacts/Counter.ts'; + +const NODE_URL = process.env.NODE_URL ?? 'http://localhost:8080'; +const INITIAL_COUNTER_VALUE = 0n; + +test('Counter deploys and increments through codegen bindings', async () => { + const wallet = await EmbeddedWallet.create(NODE_URL, { ephemeral: true }); + + const [test0] = await getInitialTestAccountsData(); + await wallet.createSchnorrAccount(test0.secret, test0.salt, test0.signingKey); + const owner = test0.address; + + const { contract: counter } = await CounterContract.deploy(wallet, INITIAL_COUNTER_VALUE, owner).send({ + from: owner, + }); + + const initial = await counter.methods.get_counter(owner).simulate({ from: owner }); + assert.equal(initial.result, INITIAL_COUNTER_VALUE, 'counter value just after deploy'); + + await counter.methods.increment(owner).send({ from: owner }); + + const afterIncrement = await counter.methods.get_counter(owner).simulate({ from: owner }); + assert.equal(afterIncrement.result, INITIAL_COUNTER_VALUE + 1n, 'counter value after increment'); +}); diff --git a/aztec-up/test/full-dev-path/full-dev-path.ts b/aztec-up/test/full-dev-path/full-dev-path.ts new file mode 100644 index 000000000000..93c7eaa8fb0c --- /dev/null +++ b/aztec-up/test/full-dev-path/full-dev-path.ts @@ -0,0 +1,284 @@ +// Test for the installed Aztec toolchain. Exercises the full developer onboarding path end-to-end: +// 1. aztec init - scaffold a workspace with a Counter contract and test crate +// 2. aztec compile - compile the scaffolded contract +// 3. aztec test - run the TXE tests from the scaffold's test crate +// 4. aztec start - start a local sandbox (anvil + aztec node) +// 5. aztec codegen - generate TypeScript bindings from the compiled artifact +// 6. TS end-to-end test - run a node:test suite (counter.test.ts) inside the scaffolded +// workspace that deploys Counter via codegen'd bindings, increments +// it, and reads the value back through the `get_counter` utility +// +// Invoked by run-test.sh, which handles installation and PATH setup before executing this file. +// +// Every phase is wrapped in step(name, ...). The script always emits a `TEST_RESULT=pass|fail ...` line for CI +// parsing; on failure it also prints a banner identifying the step that failed. + +import { execFileSync, spawn } from 'node:child_process'; +import { + closeSync, + copyFileSync, + existsSync, + globSync, + mkdirSync, + mkdtempSync, + openSync, + readFileSync, + rmSync, + symlinkSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { setTimeout as delay } from 'node:timers/promises'; + +const NODE_PORT = 8080; +const LOCAL_NETWORK_READY_TIMEOUT_MS = 600_000; // 10 minutes +const POLL_INTERVAL_MS = 2000; // 2 seconds + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const COUNTER_TEST_TEMPLATE = join(SCRIPT_DIR, 'counter.test.ts'); + +// Defaults to ~/.aztec/current (the symlink aztec-up maintains); fails if no package.json is found there. +const AZTEC_INSTALL_DIR = process.env.AZTEC_INSTALL_DIR ?? join(process.env.HOME ?? '', '.aztec/current'); +if (!existsSync(join(AZTEC_INSTALL_DIR, 'package.json'))) { + console.error(`FATAL: AZTEC_INSTALL_DIR does not point at an installed aztec: ${AZTEC_INSTALL_DIR}`); + process.exit(2); +} + +const TMP_DIR = mkdtempSync(join(tmpdir(), 'aztec-full-dev-path-')); +const WORKSPACE_DIR = join(TMP_DIR, 'my_workspace'); + +// Exit codes follow the Unix 128+signal convention for signal terminations. +process.on('SIGINT', () => { + leaveTmpDirForInspection(); + process.exit(130); +}); +process.on('SIGTERM', () => { + leaveTmpDirForInspection(); + process.exit(143); +}); + +type RunResult = + | { ok: true; aztecVersion: string } + | { ok: false; stepName: string; aztecVersion: string; error: unknown }; + +const totalStart = Date.now(); +const result = await main(); +if (result.ok) { + log(`All steps PASSED (${msToSecs(Date.now() - totalStart)}s total)`); + console.log(`TEST_RESULT=pass version=${result.aztecVersion}`); + rmSync(TMP_DIR, { recursive: true, force: true }); +} else { + reportFailure(result.stepName, result.aztecVersion, result.error); + leaveTmpDirForInspection(); + process.exitCode = 1; +} + +async function main(): Promise { + log(`Working in ${TMP_DIR}`); + let stepName = ''; + let aztecVersion = 'unknown'; + + async function step(name: string, fn: () => T | Promise): Promise { + stepName = name; + const start = Date.now(); + log(`[step] ${name}`); + const out = await fn(); + log(` done (${msToSecs(Date.now() - start)}s)`); + return out; + } + + try { + aztecVersion = await step('Checking installed tool versions', logVersions); + await step('Scaffolding new workspace (aztec init)', scaffoldWorkspace); + await step('Verifying scaffold structure', assertScaffold); + await step('Compiling contract (aztec compile)', () => run('aztec', ['compile'], WORKSPACE_DIR)); + + const artifactPath = await step('Locating compiled artifact', locateArtifact); + log(` artifact at ${artifactPath}`); + + await step('Running TXE tests (aztec test)', () => run('aztec', ['test'], WORKSPACE_DIR)); + + await step('Starting local sandbox (aztec start --local-network)', startLocalNetwork); + + await step('Generating TypeScript bindings (aztec codegen)', () => codegen(artifactPath)); + + await step('Running TypeScript end-to-end test (node --test)', runTsEndToEndTest); + return { ok: true, aztecVersion }; + } catch (error) { + return { ok: false, stepName, aztecVersion, error }; + } +} + +function scaffoldWorkspace() { + // aztec init scaffolds in pwd and uses the directory name as the package name; create the dir first. + mkdirSync(WORKSPACE_DIR, { recursive: true }); + run('aztec', ['init'], WORKSPACE_DIR); +} + +function logVersions(): string { + log('Tool versions:'); + let aztecVersion = 'unknown'; + for (const cmd of ['aztec', 'nargo', 'bb', 'aztec-wallet']) { + const version = execFileSync(cmd, ['--version'], { encoding: 'utf8' }).trim().split('\n')[0]; + console.log(` ${cmd}: ${version}`); + if (cmd === 'aztec') { + aztecVersion = version; + } + } + return aztecVersion; +} + +function assertScaffold() { + // aztec init scaffolds a workspace with `_contract/` (Counter) and `_test/` crates. + const packageName = 'my_workspace'; + const required = [ + 'Nargo.toml', + `${packageName}_contract/Nargo.toml`, + `${packageName}_contract/src/main.nr`, + `${packageName}_test/Nargo.toml`, + `${packageName}_test/src/lib.nr`, + ]; + for (const rel of required) { + const abs = join(WORKSPACE_DIR, rel); + if (!existsSync(abs)) { + fail(`expected scaffold file missing: ${rel}`); + } + } +} + +function locateArtifact(): string { + const matches = globSync('**/target/*-Counter.json', { cwd: WORKSPACE_DIR }); + if (matches.length === 0) { + fail('compiled Counter artifact not found under target/'); + } + if (matches.length > 1) { + fail(`expected one Counter artifact, found ${matches.length}: ${matches.join(', ')}`); + } + return resolve(WORKSPACE_DIR, matches[0]); +} + +async function startLocalNetwork(): Promise { + const logPath = join(TMP_DIR, 'local_network.log'); + const logFd = openSync(logPath, 'a'); + const proc = spawn('aztec', ['start', '--local-network'], { + cwd: TMP_DIR, + stdio: ['ignore', logFd, logFd], + env: { ...process.env, LOG_LEVEL: 'silent', PXE_PROVER: 'none' }, + }); + closeSync(logFd); + log(` local-network pid=${proc.pid}, log=${logPath}`); + + // Kill the network on process exit (including SIGINT/SIGTERM via the signal handlers). + process.on('exit', () => { + if (proc.exitCode === null) { + try { + proc.kill('SIGTERM'); + } catch {} + } + }); + + const deadline = Date.now() + LOCAL_NETWORK_READY_TIMEOUT_MS; + while (true) { + if (proc.exitCode !== null) { + dumpTail(logPath); + fail(`local-network exited early with code ${proc.exitCode} (see ${logPath})`); + } + if (Date.now() > deadline) { + dumpTail(logPath); + fail(`timed out after ${msToSecs(LOCAL_NETWORK_READY_TIMEOUT_MS)}s waiting for local-network /status (see ${logPath})`); + } + try { + const res = await fetch(`http://localhost:${NODE_PORT}/status`); + if (res.ok) { + log(' local-network ready'); + return; + } + } catch { + // not ready yet + } + await delay(POLL_INTERVAL_MS); + } +} + +function codegen(artifactPath: string) { + const artifactsOutDir = join(WORKSPACE_DIR, 'artifacts'); + mkdirSync(artifactsOutDir, { recursive: true }); + const targetDir = resolve(artifactPath, '..'); + run('aztec', ['codegen', targetDir, '-o', artifactsOutDir], WORKSPACE_DIR); + const codegenTs = join(artifactsOutDir, 'Counter.ts'); + if (!existsSync(codegenTs)) { + fail(`codegen did not emit Counter.ts (wrote to ${artifactsOutDir})`); + } +} + +function runTsEndToEndTest() { + // Point the workspace at the installed node_modules so @aztec/* imports (and transitive deps + // of the codegen'd Counter.ts) resolve to the same bundle a real user would have. + const modulesLink = join(WORKSPACE_DIR, 'node_modules'); + if (!existsSync(modulesLink)) { + symlinkSync(join(AZTEC_INSTALL_DIR, 'node_modules'), modulesLink, 'dir'); + } + const testDest = join(WORKSPACE_DIR, 'counter.test.ts'); + copyFileSync(COUNTER_TEST_TEMPLATE, testDest); + + run('node', ['--no-warnings', '--test', testDest], WORKSPACE_DIR); +} + +function reportFailure(stepName: string, aztecVersion: string, err: unknown) { + const message = err instanceof Error ? err.message : String(err); + const childExit = typeof (err as { status?: unknown })?.status === 'number' ? (err as { status: number }).status : undefined; + const banner = '='.repeat(72); + + console.error(`\n${banner}`); + console.error('FULL DEV PATH TEST FAILED'); + console.error(banner); + console.error(`Step: ${stepName}`); + console.error(`Version: ${aztecVersion}`); + if (childExit !== undefined) { + console.error(`Child exit: ${childExit}`); + } + console.error(`Tmp dir: ${TMP_DIR}`); + console.error(`Error: ${message}`); + if (err instanceof Error && err.stack) { + console.error(''); + console.error(err.stack); + } + console.error(banner); + const safeStep = stepName.replace(/\s+/g, '_'); + const safeError = message.replace(/[\r\n]+/g, ' ').slice(0, 240); + console.log(`TEST_RESULT=fail step=${safeStep} version=${aztecVersion} error="${safeError}"`); +} + +function msToSecs(ms: number): string { + return (ms / 1000).toFixed(1); +} + +function run(cmd: string, args: string[], cwd: string) { + execFileSync(cmd, args, { cwd, stdio: 'inherit' }); +} + +function log(msg: string) { + console.log(`>>> ${msg}`); +} + +function fail(msg: string): never { + throw new Error(msg); +} + +function leaveTmpDirForInspection() { + console.error(`>>> Left tmp dir at ${TMP_DIR} for inspection`); +} + +function dumpTail(path: string, lines = 100) { + if (!existsSync(path)) { + return; + } + console.error(`--- last ${lines} lines of ${path} ---`); + try { + console.error(readFileSync(path, 'utf8').split('\n').slice(-lines).join('\n')); + } catch { + console.error(`(failed to read ${path})`); + } + console.error(`--- end of ${path} ---`); +} diff --git a/aztec-up/test/full-dev-path/run-test.sh b/aztec-up/test/full-dev-path/run-test.sh new file mode 100755 index 000000000000..52e79d56a2a9 --- /dev/null +++ b/aztec-up/test/full-dev-path/run-test.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Launcher for the full-dev-path test. +# +# Steps: +# 1. Install Node via NVM if not present (skipped with SKIP_INSTALL=1) +# 2. Install the Aztec toolchain via the public installer (skipped with SKIP_INSTALL=1) +# 3. Run full-dev-path.ts which exercises the installed toolchain end-to-end +# +# Env vars: +# SKIP_INSTALL=1 Skip steps 1-2 and use the already-installed toolchain (dev-box inner loop). +# VERSION= Version to install (e.g. 4.3.0 or v4.3.0). Required unless SKIP_INSTALL=1. +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +if [ "${SKIP_INSTALL:-0}" = "1" ]; then + echo ">>> Skipping install (SKIP_INSTALL=1)" +else + if [ -z "${VERSION:-}" ]; then + echo "ERROR: VERSION must be set when SKIP_INSTALL is not 1." >&2 + exit 1 + fi + if ! command -v node &>/dev/null; then + echo ">>> Installing Node via NVM" + curl -sL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh -o /tmp/nvm-install.sh + PROFILE=/dev/null bash /tmp/nvm-install.sh + export NVM_DIR="$HOME/.nvm" + set +eu; . "$NVM_DIR/nvm.sh"; set -eu + nvm install --lts + fi + echo ">>> Installing aztec ${VERSION}" + NO_NEW_SHELL=1 VERSION="${VERSION}" bash <(curl -sL https://install.aztec.network) +fi + +# Mirrors update_path_env_var() in aztec-install — profile files aren't sourced in non-interactive shells. +export PATH="$HOME/.aztec/current/bin:$HOME/.aztec/bin:$PATH" +export AZTEC_INSTALL_DIR="${AZTEC_INSTALL_DIR:-$HOME/.aztec/current}" + +echo ">>> Running test" +exec node --no-warnings "${script_dir}/full-dev-path.ts"