Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"compile:security": "compact-compiler --dir security",
"compile:token": "compact-compiler --dir token",
"compile:utils": "compact-compiler --dir utils",
"compile:integration": "SKIP_ZK=true compact-compiler --src test/integration/_mocks",
"compile:integration": "compact-compiler --src test/integration/_mocks",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"build": "compact-builder --hierarchical --out dist --clean-dist --exclude '*/archive/*' --exclude 'Mock*' --exclude '*.mock.compact' --copy package.json --copy ../README.md && find dist -type d -empty -delete",
"test": "vitest run --project unit",
"test:coverage": "vitest run --project unit --coverage",
Expand Down
59 changes: 57 additions & 2 deletions contracts/test-utils/harness/live.globalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { fetchCoinEvents, indexerHead } from './ledgerEvents.js';
* of after a slow wallet build. Gated on `MIDNIGHT_BACKEND === 'live'` so a dry
* `vitest run` that happens to glob the live tests is a no-op.
*
* It guards two things:
* It guards three things:
* - **One live project per invocation.** Every live project derives its
* wallets from the same `VITEST_POOL_ID` partition, so two of them in one
* vitest run would spend the same genesis deployer's coins. See
* {@link assertSoleLiveProject}.
* - **Freshness.** The live tests are not isolated from one another: they all
* run against the same node, so shielded-coin state left by an earlier run
* changes a later run's outcome (a coin re-spent against stale state is
Expand Down Expand Up @@ -49,6 +53,50 @@ const ENV_UP_HINT = "run 'yarn env:up' to reset the local stack";
const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));

// --- one live project per invocation ---------------------------------------

// Parked on `globalThis`, not in a module-level binding: each project loads this
// file through its OWN module runner, so a module variable would not be shared
// between two projects in the same process.
const PROJECT_CLAIM = '__midnightLiveProject';

/**
* Reject a second live project in the same vitest invocation.
*
* Every live project builds its wallets from `walletSeedsFor(VITEST_POOL_ID)`,
* so worker 1 of `unit-live` and worker 1 of `integration-live` resolve to the
* SAME genesis deployer seed. Two pools would then balance transactions against
* one deployer's UTXO snapshot — the stale-UTXO race (node `Custom error: 103`)
* that `WalletPool.ensureReady`'s serial build exists to prevent — and both
* would write `logs/live-harness-w1.log`, interleaving two runs' diagnostics.
*
* The run lock below cannot catch this: it is deliberately reentrant for our own
* pid, precisely so one process CAN run several live globalSetups. So the claim
* is tracked separately here.
*
* @param project - the project whose globalSetup is running
* @param claimed - the project that already claimed this process, if any
*/
export function assertSoleLiveProject(
project: string,
claimed: string | undefined,
): void {
if (claimed === undefined || claimed === project) return;
throw new Error(
`two live projects in one vitest run ('${claimed}' and '${project}'): ` +
'both derive their wallets from the same VITEST_POOL_ID partition, so ' +
"each project's worker 1 would spend the same genesis deployer's coins " +
'(node "Custom error: 103"). Pass one --project per invocation.',
);
}

/** Claim this process for `project`, or throw if another live project holds it. */
function claimLiveProject(project: string): void {
const registry = globalThis as Record<string, unknown>;
assertSoleLiveProject(project, registry[PROJECT_CLAIM] as string | undefined);
registry[PROJECT_CLAIM] = project;
}

// --- lock ------------------------------------------------------------------

export interface LockInfo {
Expand Down Expand Up @@ -206,8 +254,15 @@ async function assertFreshNode(): Promise<void> {
}
}

export default async function setup(): Promise<() => void> {
/**
* Vitest calls this once per project, in the main process, passing that project.
* Typed structurally so this file keeps importing nothing but `node:` builtins.
*/
export default async function setup(project?: {
readonly name?: string;
}): Promise<() => void> {
if (process.env.MIDNIGHT_BACKEND !== 'live') return () => {};
claimLiveProject(project?.name ?? '(unnamed project)');
const { reentrant } = acquireLock();
try {
if (process.env.MIDNIGHT_LIVE_ALLOW_DIRTY !== '1') await assertFreshNode();
Expand Down
13 changes: 12 additions & 1 deletion contracts/test-utils/harness/liveProgressReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import type { Reporter, TestCase, TestModule } from 'vitest/node';
* worker in `live.setup` (the only place that knows its `VITEST_POOL_ID`). The
* total accrues as modules are collected, so the first few lines may show a
* smaller denominator until collection finishes.
*
* A SKIPPED test never runs `beforeEach`, so no worker stamps it. Rather than
* print `[w?]`, fall back to the last worker seen for that test's module: the
* module was loaded and run by that worker even where an individual test was
* skipped. `?` remains only for a module where nothing ever reported a worker.
*/
const MARKS: Record<string, string> = {
passed: '✓',
Expand All @@ -20,10 +25,13 @@ const MARKS: Record<string, string> = {
export default class LiveProgressReporter implements Reporter {
private total = 0;
private done = 0;
/** module id → the last worker that reported a test from it. */
private workerByModule = new Map<string, number>();

onTestRunStart(): void {
this.total = 0;
this.done = 0;
this.workerByModule.clear();
}

onTestModuleCollected(module: TestModule): void {
Expand All @@ -34,7 +42,10 @@ export default class LiveProgressReporter implements Reporter {
const { state } = testCase.result();
if (state === 'pending') return; // not finished yet
this.done += 1;
const worker = (testCase.meta() as { workerId?: number }).workerId ?? '?';
const moduleId = testCase.module.moduleId;
const stamped = (testCase.meta() as { workerId?: number }).workerId;
if (stamped !== undefined) this.workerByModule.set(moduleId, stamped);
const worker = stamped ?? this.workerByModule.get(moduleId) ?? '?';
const mark = MARKS[state] ?? '·';
const ms = Math.round(testCase.diagnostic()?.duration ?? 0);
console.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
// "Public" supply means the total, and therefore each mint/burn delta, is
// disclosed on chain. Balances stay confidential; only the aggregate is
// visible.
//
// The `Composed` prefix is load-bearing: compactc keys each `artifacts/<name>/`
// directory on the source basename, so this file must not share a basename with
// the `src/token/extensions` extension it composes.
pragma language_version >= 0.23.0;

import CompactStandardLibrary;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ledger,
Contract as MockConfidentialFungibleTokenPublicSupply,
type Token_EscrowEntry,
} from '../../../artifacts/ConfidentialFungibleTokenPublicSupply/contract/index.js';
} from '../../../artifacts/ComposedConfidentialFungibleTokenPublicSupply/contract/index.js';
import {
ConfidentialFungibleTokenPrivateState,
ConfidentialFungibleTokenWitnesses,
Expand All @@ -16,7 +16,8 @@ import {

/**
* Integration fixture for the assembled ConfidentialFungibleToken + PublicSupply
* contract (`test/integration/_mocks/ConfidentialFungibleTokenPublicSupply`).
* contract
* (`test/integration/_mocks/ComposedConfidentialFungibleTokenPublicSupply`).
*
* It reuses the base token's confidential-token witnesses and private state (SK,
* EK, plaintext cache, randomness seed) verbatim — the assembled contract's
Expand Down Expand Up @@ -45,7 +46,7 @@ const Base = createSimulator<
contractArgs: (name, symbol, decimals) => [name, symbol, decimals],
ledgerExtractor: (state) => ledger(state),
witnessesFactory: () => ConfidentialFungibleTokenWitnesses(),
artifactName: 'ConfidentialFungibleTokenPublicSupply',
artifactName: 'ComposedConfidentialFungibleTokenPublicSupply',
});

export class ConfidentialFungibleTokenPublicSupplySimulator extends Base {
Expand Down
Loading