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
70 changes: 70 additions & 0 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Compact Contracts Integration Suite

on:
pull_request:
types: [labeled, synchronize]
schedule:
- cron: "0 6 * * *" # nightly at 06:00 UTC
workflow_dispatch:

jobs:
run-integration:
# Run on scheduled/manual triggers, or on PRs carrying the `integration` label.
if: >
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'integration'))
name: Run Integration Suite
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 30

steps:
- name: Harden Runner
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit

- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2

- name: Setup Environment
uses: ./.github/actions/setup

- name: Compile contracts
run: turbo compact --filter=@openzeppelin/compact-contracts

- name: Start local Midnight stack
run: make env-up

- name: Wait for local stack health
run: |
for i in $(seq 1 60); do
if docker compose -f local-env.yml ps --format json | \
grep -q '"Health":"healthy"'; then
echo "Local stack healthy"; exit 0
fi
sleep 5
done
echo "Local stack did not become healthy in time"
docker compose -f local-env.yml ps
exit 1
Comment on lines +44 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Health-check loop exits as soon as ONE service is healthy — tests may fire before the indexer is ready.

grep -q '"Health":"healthy"' succeeds on the first matching line, so the loop exits as soon as any service becomes healthy. Given that node has a 2-second health-check interval and indexer only starts after node is healthy (and then needs its own 10-second health-check cycle), the loop can exit while the indexer is still initialising. Integration tests that hit the indexer endpoint would fail flakily.

Pin the loop exit to all three services being healthy:

🛠️ Proposed fix
       - name: Wait for local stack health
         run: |
           for i in $(seq 1 60); do
-            if docker compose -f local-env.yml ps --format json | \
-               grep -q '"Health":"healthy"'; then
-              echo "Local stack healthy"; exit 0
+            healthy=$(docker compose -f local-env.yml ps --format json \
+              | grep -c '"Health":"healthy"' || true)
+            if [ "$healthy" -ge 3 ]; then
+              echo "All 3 services healthy"; exit 0
             fi
             sleep 5
           done
           echo "Local stack did not become healthy in time"
           docker compose -f local-env.yml ps
           exit 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test-integration.yml around lines 44 - 55, The
health-check loop currently exits when any service is healthy because it greps
for a single '"Health":"healthy"' match; modify the loop to require all expected
services (e.g., node and indexer and any third service — total 3) be healthy
before exiting by counting healthy entries in the compose JSON output. Replace
the grep check with a JSON-aware count (e.g., use jq to count items with
Health=="healthy" and compare that count to 3) inside the "Wait for local stack
health" step so the script only exits with success when the healthy count equals
the expected number of services.


- name: Run integration tests
run: yarn test:integration

- name: Upload container logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: midnight-stack-logs
path: logs/
if-no-files-found: warn

- name: Tear down local stack
if: always()
run: make env-down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ coverage
*~

*temp

.claude/
31 changes: 31 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
COMPOSE_FILE := local-env.yml
LOGS_DIR := logs
SERVICES := proof-server indexer node

.PHONY: env-up env-down env-logs env-logs-clean env-status

## Start local environment and stream logs to logs/
env-up: env-down
docker compose -f $(COMPOSE_FILE) up -d
@mkdir -p $(LOGS_DIR)
@for svc in $(SERVICES); do \
docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \
done
@echo "Logs streaming to $(LOGS_DIR)/"
Comment on lines +8 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wait for the stack to become ready before env-up exits.

Line 9 only starts the containers; it does not guarantee that the node, indexer, and proof server are accepting requests yet. Since CI appears to run tests right after make env-up, this target can introduce startup races and flaky integration runs. Please add an explicit readiness check here before returning.

🧰 Tools
🪛 checkmake (0.3.2)

[warning] 8-8: Target body for "env-up" exceeds allowed length of 5 lines (6).

(maxbodylength)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 8 - 14, The env-up Makefile target currently only
starts containers (target name env-up, using COMPOSE_FILE and SERVICES) but
doesn't wait for services to be ready; add a readiness check after docker
compose up -d that polls each required service (e.g., node, indexer, proof
server) until they respond on their health/readiness endpoints or TCP ports
before allowing env-up to exit. Implement by looping with a timeout and retries,
using curl or nc against configured ports/URLs (driven by env vars or docker
compose service names) and fail the target if any service doesn't become healthy
within the timeout; keep the existing log streaming logic (SERVICES -> LOGS_DIR)
intact but run it in the background only after readiness checks succeed or
concurrently while still gating exit on the readiness loop.


## Stop local environment
env-down:
@-pkill -f "docker compose -f $(COMPOSE_FILE) logs" 2>/dev/null || true
docker compose -f $(COMPOSE_FILE) down

## Tail all logs
env-logs:
tail -f $(LOGS_DIR)/*.log

## Clear log files
env-logs-clean:
rm -rf $(LOGS_DIR)/*.log

## Show container status
env-status:
docker compose -f $(COMPOSE_FILE) ps
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,34 @@ turbo compact

### Run tests

#### Unit tests

In-memory simulator, no network. Completes in seconds:

```bash
turbo test
```

#### Integration tests

Drive the modules against a real local Midnight stack (proof-server + indexer + node). Contracts are deployed, transactions are proven and submitted, and assertions read live ledger state. Useful for exercising the contract-maintenance authority (CMA) upgrade pathway, multi-signer access control, and any behaviour the simulator can't model.

Bring up the local stack, then run the suite:

```bash
make env-up
yarn test:integration
make env-down # when finished
```

**Expect this to be slow.** Each `describe` typically deploys a fresh contract and the genesis-funded wallet syncs against the local indexer (~30s per fresh deploy) before transactions can be submitted. The full suite (15 spec files, ~50 tests) takes **~60–65 minutes** wall-clock end-to-end.

The dominant cost is per-describe wallet sync; iterating on a single spec is much faster than running everything. Filter to one file via vitest's `--config` invocation directly if you're in `contracts/`:

```bash
cd contracts && yarn test:integration:watch -- specs/cma/freeze.spec.ts
```

### Check/apply Biome formatter

```bash
Expand Down
44 changes: 43 additions & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@
"compact:security": "compact-compiler --dir security",
"compact:token": "compact-compiler --dir token",
"compact:utils": "compact-compiler --dir utils",
"compact:mocks": "compact-compiler --src-root mocks",
"compact:mocks:access": "compact-compiler --src-root mocks --dir access",
"compact:mocks:security": "compact-compiler --src-root mocks --dir security",
"compact:mocks:token": "compact-compiler --src-root mocks --dir token",
"compact:mocks:utils": "compact-compiler --src-root mocks --dir utils",
"compact:integration-mocks": "compact-compiler --src-root test/integration/_mocks",
"build": "compact-builder",
"test": "compact-compiler --skip-zk && vitest run",
"test:integration": "COMPACT_TOOLCHAIN_VERSION=0.30.0 yarn compact && COMPACT_TOOLCHAIN_VERSION=0.30.0 yarn compact:integration-mocks && vitest run --config vitest.integration.config.ts",
"test:integration:watch": "vitest --config vitest.integration.config.ts",
Comment on lines +41 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

test:integration:watch skips the required bootstrap step.

Line 42 drops both the artifact generation and the pinned COMPACT_TOOLCHAIN_VERSION that line 41 relies on. On a clean repo, or after .compact changes, the watch command can start Vitest against stale or missing generated artifacts. Please make the watch script reuse the same compile/bootstrap path before entering watch mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/package.json` around lines 41 - 42, The watch script
"test:integration:watch" currently skips the bootstrap steps used by
"test:integration"; update "test:integration:watch" to first run the same
artifact generation with the pinned COMPACT_TOOLCHAIN_VERSION (i.e., run the
COMPACT_TOOLCHAIN_VERSION=0.30.0 yarn compact &&
COMPACT_TOOLCHAIN_VERSION=0.30.0 yarn compact:integration-mocks sequence or call
into the same npm scripts) and only then start Vitest in watch mode (vitest
--config vitest.integration.config.ts) so it never runs against stale/missing
generated artifacts.

"types": "tsc -p tsconfig.json --noEmit",
"clean": "git clean -fXd"
},
Expand All @@ -42,11 +50,45 @@
"@openzeppelin-compact/compact": "workspace:^"
},
"devDependencies": {
"@apollo/client": "^3.11.8",
"@midnight-ntwrk/compact-js": "2.5.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.2",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-network-id": "4.0.2",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-types": "4.0.2",
"@midnight-ntwrk/midnight-js-utils": "4.0.2",
"@midnight-ntwrk/testkit-js": "4.0.2",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "3.0.0",
"@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
"@midnight-ntwrk/wallet-sdk-shielded": "2.1.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0",
"@openzeppelin-compact/contracts-simulator": "workspace:^",
"@scure/bip39": "^1.2.1",
"@tsconfig/node24": "^24.0.4",
"@types/node": "24.10.0",
"axios": "^1.12.0",
"buffer": "^6.0.3",
"cross-fetch": "^4.0.0",
"effect": "^3.20.0",
"fetch-retry": "^6.0.0",
"graphql": "^16.8.1",
"graphql-ws": "^5.16.0",
"isomorphic-ws": "^5.0.0",
"level": "^8.0.1",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"rxjs": "^7.8.1",
"superjson": "^2.2.1",
"testcontainers": "^10.28.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^4.1.2"
"vitest": "^4.1.2",
"ws": "^8.16.0"
}
}
2 changes: 1 addition & 1 deletion contracts/test-utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
encodeCoinPublicKey,
isContractAddress,
} from '@midnight-ntwrk/compact-runtime';
import { encodeContractAddress } from '@midnight-ntwrk/ledger-v7';
import { encodeContractAddress } from '@midnight-ntwrk/ledger-v8';

type ZswapCoinPublicKey = { bytes: Uint8Array };

Expand Down
Loading