diff --git a/.changeset/devnet-auto-port-allocation.md b/.changeset/devnet-auto-port-allocation.md new file mode 100644 index 00000000..cf7f21e1 --- /dev/null +++ b/.changeset/devnet-auto-port-allocation.md @@ -0,0 +1,9 @@ +--- +"@evolution-sdk/devnet": patch +--- + +`Cluster.make` now auto-allocates host ports for cardano-node, Kupo, and Ogmios when not specified, so parallel test runs no longer collide on the previous fixed defaults (1442/1337/4001/8090). The returned `Cluster` exposes a new `ports` field with the assigned host ports. + +Consumers passing explicit `port` values on `kupo`, `ogmios`, or `ports` keep their existing behavior. Consumers that relied on the previous fixed defaults should read the assigned port from `cluster.ports.kupo`, `cluster.ports.ogmios`, `cluster.ports.node`, and `cluster.ports.submit` instead. + +Container starts now retry with exponential backoff to absorb transient Docker daemon pressure when many containers come up at once. diff --git a/packages/evolution-devnet/src/Cluster.ts b/packages/evolution-devnet/src/Cluster.ts index 23b0ec4c..a66c1fa9 100644 --- a/packages/evolution-devnet/src/Cluster.ts +++ b/packages/evolution-devnet/src/Cluster.ts @@ -2,6 +2,7 @@ import { NodeStream } from "@effect/platform-node" import Docker from "dockerode" import { Data, Effect, Stream } from "effect" import * as fs from "fs" +import { createServer } from "net" import * as os from "os" import * as path from "path" @@ -20,10 +21,29 @@ export interface Cluster { readonly kupo?: Container.Container | undefined readonly ogmios?: Container.Container | undefined readonly networkName: string + /** Host ports assigned to each container. */ + readonly ports: { + readonly node: number + readonly submit: number + readonly kupo?: number + readonly ogmios?: number + } /** The Shelley genesis config used by this cluster (needed for slot config) */ readonly shelleyGenesis: Config.ShelleyGenesis } +const findFreePort = (): Promise => + new Promise((resolve, reject) => { + const server = createServer() + server.unref() + server.on("error", reject) + server.listen(0, () => { + const addr = server.address() + const port = typeof addr === "object" && addr !== null ? addr.port : 0 + server.close(() => resolve(port)) + }) + }) + /** * Internal utilities for cluster operations. * @@ -93,12 +113,25 @@ const writeConfigFiles = (config: Required) => */ export const makeEffect = (config: Config.DevNetConfig = {}): Effect.Effect => Effect.gen(function* () { + const kupoEnabled = config.kupo?.enabled ?? Config.DEFAULT_DEVNET_CONFIG.kupo.enabled + const ogmiosEnabled = config.ogmios?.enabled ?? Config.DEFAULT_DEVNET_CONFIG.ogmios.enabled + const resolvedPorts = { + node: config.ports?.node ?? (yield* Effect.promise(() => findFreePort())), + submit: config.ports?.submit ?? (yield* Effect.promise(() => findFreePort())), + kupo: kupoEnabled + ? (config.kupo?.port ?? (yield* Effect.promise(() => findFreePort()))) + : undefined, + ogmios: ogmiosEnabled + ? (config.ogmios?.port ?? (yield* Effect.promise(() => findFreePort()))) + : undefined + } + const fullConfig: Required = { clusterName: config.clusterName ?? Config.DEFAULT_DEVNET_CONFIG.clusterName, image: config.image ?? Config.DEFAULT_DEVNET_CONFIG.image, ports: { - ...Config.DEFAULT_DEVNET_CONFIG.ports, - ...config.ports + node: resolvedPorts.node, + submit: resolvedPorts.submit }, networkMagic: config.networkMagic ?? Config.DEFAULT_DEVNET_CONFIG.networkMagic, nodeConfig: { @@ -135,11 +168,13 @@ export const makeEffect = (config: Config.DevNetConfig = {}): Effect.Effect { const createTestClient = () => Client.make(Cluster.getChain(devnetCluster!)) - .withKupmios({ kupoUrl: "http://localhost:1443", ogmiosUrl: "http://localhost:1338" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 }) beforeAll(async () => { @@ -41,10 +44,9 @@ describe("Client with Devnet", () => { devnetCluster = await Cluster.make({ clusterName: "client-kupmios-wallet-test", - ports: { node: 6001, submit: 9002 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1443, logLevel: "Info" }, - ogmios: { enabled: true, port: 1338, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/Devnet.Genesis.test.ts b/packages/evolution-devnet/test/Devnet.Genesis.test.ts index a447a3ac..3d7c5474 100644 --- a/packages/evolution-devnet/test/Devnet.Genesis.test.ts +++ b/packages/evolution-devnet/test/Devnet.Genesis.test.ts @@ -25,7 +25,6 @@ describe("Devnet.Genesis", () => { devnetCluster = await Cluster.make({ clusterName: "genesis-utxo-calculation-test", - ports: { node: 6002, submit: 9003 }, shelleyGenesis: genesisConfig, kupo: { enabled: false }, ogmios: { enabled: false } diff --git a/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts b/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts index 93751ecb..c13b498d 100644 --- a/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts @@ -25,7 +25,10 @@ describe("TxBuilder addSigner (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1449", ogmiosUrl: "http://localhost:1344" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -47,10 +50,9 @@ describe("TxBuilder addSigner (Devnet Submit)", () => { devnetCluster = await Cluster.make({ clusterName: "addsigner-test", - ports: { node: 6007, submit: 9008 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1449, logLevel: "Info" }, - ogmios: { enabled: true, port: 1344, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Chain.test.ts b/packages/evolution-devnet/test/TxBuilder.Chain.test.ts index e41d9a63..32e5a75b 100644 --- a/packages/evolution-devnet/test/TxBuilder.Chain.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Chain.test.ts @@ -18,7 +18,10 @@ describe("TxBuilder.chainResult", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1456", ogmiosUrl: "http://localhost:1348" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -40,10 +43,9 @@ describe("TxBuilder.chainResult", () => { devnetCluster = await Cluster.make({ clusterName: "chain-test", - ports: { node: 6013, submit: 9013 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1456, logLevel: "Info" }, - ogmios: { enabled: true, port: 1348, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Compose.test.ts b/packages/evolution-devnet/test/TxBuilder.Compose.test.ts index 1f0959f1..7a2c6784 100644 --- a/packages/evolution-devnet/test/TxBuilder.Compose.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Compose.test.ts @@ -26,7 +26,10 @@ describe("TxBuilder compose (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1458", ogmiosUrl: "http://localhost:1350" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -48,10 +51,9 @@ describe("TxBuilder compose (Devnet Submit)", () => { devnetCluster = await Cluster.make({ clusterName: "compose-test", - ports: { node: 6015, submit: 9015 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1458, logLevel: "Info" }, - ogmios: { enabled: true, port: 1350, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Governance.test.ts b/packages/evolution-devnet/test/TxBuilder.Governance.test.ts index e717c763..c3df93e0 100644 --- a/packages/evolution-devnet/test/TxBuilder.Governance.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Governance.test.ts @@ -29,7 +29,10 @@ describe("TxBuilder Governance Operations", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1457", ogmiosUrl: "http://localhost:1349" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -80,11 +83,10 @@ describe("TxBuilder Governance Operations", () => { devnetCluster = await Cluster.make({ clusterName: "governance-ops-test", - ports: { node: 6014, submit: 9014 }, shelleyGenesis: genesisConfig, conwayGenesis, - kupo: { enabled: true, port: 1457, logLevel: "Info" }, - ogmios: { enabled: true, port: 1349, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts b/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts index 3abea749..36e3b6b0 100644 --- a/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts @@ -25,7 +25,10 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1450", ogmiosUrl: "http://localhost:1345" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -47,10 +50,9 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => { devnetCluster = await Cluster.make({ clusterName: "metadata-test", - ports: { node: 6008, submit: 9009 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1450, logLevel: "Info" }, - ogmios: { enabled: true, port: 1345, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Mint.test.ts b/packages/evolution-devnet/test/TxBuilder.Mint.test.ts index 90ebe9ab..ce89f1d1 100644 --- a/packages/evolution-devnet/test/TxBuilder.Mint.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Mint.test.ts @@ -31,7 +31,10 @@ describe("TxBuilder Minting (Devnet Submit)", () => { const createTestClient = () => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1443", ogmiosUrl: "http://localhost:1338" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 }) } @@ -64,10 +67,9 @@ describe("TxBuilder Minting (Devnet Submit)", () => { devnetCluster = await Cluster.make({ clusterName: "client-minting-test", - ports: { node: 6001, submit: 9002 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1443, logLevel: "Info" }, - ogmios: { enabled: true, port: 1338, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts b/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts index a20123be..d093e2a9 100644 --- a/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts @@ -42,7 +42,10 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1449", ogmiosUrl: "http://localhost:1344" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -64,10 +67,9 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { devnetCluster = await Cluster.make({ clusterName: "nativescript-test", - ports: { node: 6007, submit: 9008 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1449, logLevel: "Info" }, - ogmios: { enabled: true, port: 1344, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts b/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts index 1b6782b0..b58c22bd 100644 --- a/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts @@ -66,7 +66,10 @@ describe("TxBuilder Plutus Minting (Devnet Submit)", () => { const createTestClient = () => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1444", ogmiosUrl: "http://localhost:1339" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 }) } @@ -92,10 +95,9 @@ describe("TxBuilder Plutus Minting (Devnet Submit)", () => { devnetCluster = await Cluster.make({ clusterName: "plutus-minting-test", - ports: { node: 6002, submit: 9003 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1444, logLevel: "Info" }, - ogmios: { enabled: true, port: 1339, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Pool.test.ts b/packages/evolution-devnet/test/TxBuilder.Pool.test.ts index b1d63eb6..57b3b5f9 100644 --- a/packages/evolution-devnet/test/TxBuilder.Pool.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Pool.test.ts @@ -34,7 +34,10 @@ describe("TxBuilder Pool Operations", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1453", ogmiosUrl: "http://localhost:1343" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -72,10 +75,9 @@ describe("TxBuilder Pool Operations", () => { devnetCluster = await Cluster.make({ clusterName: "pool-ops-test", - ports: { node: 6006, submit: 9007 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1453, logLevel: "Info" }, - ogmios: { enabled: true, port: 1343, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts b/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts index 74bb81dd..628ac7ec 100644 --- a/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts @@ -85,7 +85,10 @@ describe("TxBuilder RedeemerBuilder", () => { const createTestClient = () => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1445", ogmiosUrl: "http://localhost:1340" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 }) } @@ -111,10 +114,9 @@ describe("TxBuilder RedeemerBuilder", () => { devnetCluster = await Cluster.make({ clusterName: "redeemer-builder-test", - ports: { node: 6003, submit: 9004 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1445, logLevel: "Info" }, - ogmios: { enabled: true, port: 1340, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts b/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts index adc4b8ac..325cd21c 100644 --- a/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts @@ -83,7 +83,10 @@ describe("TxBuilder Script Stake Operations", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1447", ogmiosUrl: "http://localhost:1342" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -109,10 +112,9 @@ describe("TxBuilder Script Stake Operations", () => { devnetCluster = await Cluster.make({ clusterName: "script-stake-test", - ports: { node: 6005, submit: 9006 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1447, logLevel: "Info" }, - ogmios: { enabled: true, port: 1342, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts b/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts index ecd561a9..89196f84 100644 --- a/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts @@ -33,18 +33,13 @@ describe("TxBuilder Script Handling", () => { try { devnetCluster = await Cluster.make({ clusterName: "txbuilder-plutus-script-eval", - ports: { - node: 5001, - submit: 9001 - }, - shelleyGenesis: { + shelleyGenesis: { slotLength: 0.02, // 20ms per slot (fast) epochLength: 50, activeSlotsCoeff: 1.0 }, ogmios: { enabled: true, - port: 1337, logLevel: "info" } }) @@ -54,13 +49,12 @@ describe("TxBuilder Script Handling", () => { // Wait for Ogmios to be ready await new Promise((resolve) => setTimeout(resolve, 2_000)) - // Ogmios serves both HTTP (for JSON-RPC) and WebSocket on the same port - const ogmiosUrl = "http://localhost:1337" + const ogmiosUrl = `http://localhost:${devnetCluster.ports.ogmios}` // Create provider using local Ogmios // Note: Kupo URL is required but not used in these tests (only Ogmios for evaluation) kupmiosProvider = new KupmiosProvider( - "http://localhost:1442", // Kupo (not used) + `http://localhost:${devnetCluster.ports.kupo}`, // Kupo (not used) ogmiosUrl // Ogmios for script evaluation via HTTP ) diff --git a/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts b/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts index 6c15644e..d4aed5db 100644 --- a/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts @@ -39,7 +39,10 @@ describe("TxBuilder Spend ScriptRef (Devnet Submit)", () => { const createTestClient = () => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1454", ogmiosUrl: "http://localhost:1346" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 }) } @@ -63,10 +66,9 @@ describe("TxBuilder Spend ScriptRef (Devnet Submit)", () => { devnetCluster = await Cluster.make({ clusterName: "spend-scriptref-test", - ports: { node: 6011, submit: 9011 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1454, logLevel: "Info" }, - ogmios: { enabled: true, port: 1346, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Stake.test.ts b/packages/evolution-devnet/test/TxBuilder.Stake.test.ts index 69fd9ca2..75518a8c 100644 --- a/packages/evolution-devnet/test/TxBuilder.Stake.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Stake.test.ts @@ -37,7 +37,10 @@ describe("TxBuilder Stake Operations", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1446", ogmiosUrl: "http://localhost:1341" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -79,10 +82,9 @@ describe("TxBuilder Stake Operations", () => { devnetCluster = await Cluster.make({ clusterName: "stake-ops-test", - ports: { node: 6004, submit: 9005 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1446, logLevel: "Info" }, - ogmios: { enabled: true, port: 1341, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Validity.test.ts b/packages/evolution-devnet/test/TxBuilder.Validity.test.ts index da4e3d62..8e7cca03 100644 --- a/packages/evolution-devnet/test/TxBuilder.Validity.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Validity.test.ts @@ -33,7 +33,10 @@ describe("TxBuilder Validity Interval", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1451", ogmiosUrl: "http://localhost:1351" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -56,10 +59,9 @@ describe("TxBuilder Validity Interval", () => { devnetCluster = await Cluster.make({ clusterName: "validity-test", - ports: { node: 6009, submit: 9016 }, shelleyGenesis: genesisConfig, - kupo: { enabled: true, port: 1451, logLevel: "Info" }, - ogmios: { enabled: true, port: 1351, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.Vote.test.ts b/packages/evolution-devnet/test/TxBuilder.Vote.test.ts index 14bbce6c..2037e2e4 100644 --- a/packages/evolution-devnet/test/TxBuilder.Vote.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Vote.test.ts @@ -35,7 +35,10 @@ describe("TxBuilder Vote Operations (script-free)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1453", ogmiosUrl: "http://localhost:1343" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } @@ -61,7 +64,9 @@ describe("TxBuilder Vote Operations (script-free)", () => { } } - conwayGenesis = Config.DEFAULT_CONWAY_GENESIS + // Bump govActionLifetime past the default 8 epochs so a slow CI run can finish + // propose → vote within the proposal's active window. + conwayGenesis = { ...Config.DEFAULT_CONWAY_GENESIS, govActionLifetime: 30 } const genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig) @@ -72,11 +77,10 @@ describe("TxBuilder Vote Operations (script-free)", () => { devnetCluster = await Cluster.make({ clusterName: "vote-test", - ports: { node: 6010, submit: 9010 }, shelleyGenesis: genesisConfig, conwayGenesis, - kupo: { enabled: true, port: 1453, logLevel: "Info" }, - ogmios: { enabled: true, port: 1343, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts b/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts index 7acd038d..a6ecf504 100644 --- a/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts @@ -52,7 +52,10 @@ describe("TxBuilder Vote Validator (script DRep)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") return Client.make(Cluster.getChain(devnetCluster)) - .withKupmios({ kupoUrl: "http://localhost:1455", ogmiosUrl: "http://localhost:1347" }) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) } const genesisUtxosByAccount: Map = new Map() @@ -79,11 +82,10 @@ describe("TxBuilder Vote Validator (script DRep)", () => { devnetCluster = await Cluster.make({ clusterName: "vote-validator-test", - ports: { node: 6012, submit: 9012 }, shelleyGenesis: genesisConfig, conwayGenesis: { ...Config.DEFAULT_CONWAY_GENESIS, govActionLifetime: 30 }, - kupo: { enabled: true, port: 1455, logLevel: "Info" }, - ogmios: { enabled: true, port: 1347, logLevel: "info" } + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } }) await Cluster.start(devnetCluster) diff --git a/packages/evolution-devnet/test/globalSetup.ts b/packages/evolution-devnet/test/globalSetup.ts new file mode 100644 index 00000000..e9cbb254 --- /dev/null +++ b/packages/evolution-devnet/test/globalSetup.ts @@ -0,0 +1,18 @@ +import { Effect } from "effect" + +import * as Config from "../src/Config.js" +import * as Images from "../src/Images.js" + +const REQUIRED_IMAGES = [ + Config.DEFAULT_DEVNET_CONFIG.image, + Config.DEFAULT_KUPO_CONFIG.image, + Config.DEFAULT_OGMIOS_CONFIG.image +] + +// Pre-pull every image the integration tests need. +// Concurrent test forks would otherwise race dockerode's image pull stream. +export default async function setup(): Promise { + for (const image of REQUIRED_IMAGES) { + await Effect.runPromise(Images.ensureAvailableEffect(image)) + } +} diff --git a/packages/evolution-devnet/vitest.config.ts b/packages/evolution-devnet/vitest.config.ts index 00640b0a..9bdded84 100644 --- a/packages/evolution-devnet/vitest.config.ts +++ b/packages/evolution-devnet/vitest.config.ts @@ -8,14 +8,13 @@ export default defineConfig({ testTimeout: 120_000, hookTimeout: 90_000, teardownTimeout: 60_000, - // Serialize all test files — each file creates its own cluster (cardano-node + kupo + ogmios) - // Running them concurrently exhausts Docker resources on CI / local machines + // Pre-pull images before any test forks start, otherwise concurrent pulls of + // the same image race dockerode's pull stream. + globalSetup: ["./test/globalSetup.ts"], + // Each test file creates its own cluster (cardano-node + kupo + ogmios). + // Cap parallelism so total RAM stays within typical 7-8GB CI runner budget. pool: "forks", - poolOptions: { - forks: { - maxForks: 3 - } - }, + maxWorkers: 3, // Devnet tests are slow but should not be retried — flakiness here is a real infra failure retry: 0, exclude: ["**/node_modules/**", "**/dist/**", "**/temp/**", "**/.direnv/**", "**/.{idea,git,cache,output,temp}/**"]