UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

1,067 lines (1,062 loc) 43.4 kB
var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/internal/effect/ChopsticksService.ts var ChopsticksService_exports = {}; __export(ChopsticksService_exports, { ChopsticksBlockError: () => ChopsticksBlockError, ChopsticksCleanupError: () => ChopsticksCleanupError, ChopsticksConfigTag: () => ChopsticksConfigTag, ChopsticksExtrinsicError: () => ChopsticksExtrinsicError, ChopsticksService: () => ChopsticksService, ChopsticksSetupError: () => ChopsticksSetupError, ChopsticksStorageError: () => ChopsticksStorageError, ChopsticksXcmError: () => ChopsticksXcmError }); import { Context, Data } from "effect"; var ChopsticksSetupError, ChopsticksBlockError, ChopsticksStorageError, ChopsticksExtrinsicError, ChopsticksXcmError, ChopsticksCleanupError, ChopsticksService, ChopsticksConfigTag; var init_ChopsticksService = __esm({ "src/internal/effect/ChopsticksService.ts"() { "use strict"; ChopsticksSetupError = class extends Data.TaggedError("ChopsticksSetupError") { }; ChopsticksBlockError = class extends Data.TaggedError("ChopsticksBlockError") { }; ChopsticksStorageError = class extends Data.TaggedError("ChopsticksStorageError") { }; ChopsticksExtrinsicError = class extends Data.TaggedError("ChopsticksExtrinsicError") { }; ChopsticksXcmError = class extends Data.TaggedError("ChopsticksXcmError") { }; ChopsticksCleanupError = class extends Data.TaggedError("ChopsticksCleanupError") { }; ChopsticksService = class extends Context.Tag("ChopsticksService")() { }; ChopsticksConfigTag = class extends Context.Tag("ChopsticksConfig")() { }; } }); // src/internal/effect/chopsticksConfigParser.ts import { Effect as Effect2 } from "effect"; import * as fs from "fs"; import * as yaml from "yaml"; import { createLogger } from "@moonwall/util"; var BuildBlockModeValues, logger, resolveEnvVars, resolveEnvVarsDeep, parseBuildBlockMode, parseBlockField, parseChopsticksConfigFile; var init_chopsticksConfigParser = __esm({ "src/internal/effect/chopsticksConfigParser.ts"() { "use strict"; init_ChopsticksService(); BuildBlockModeValues = { Batch: "Batch", Manual: "Manual", Instant: "Instant" }; logger = createLogger({ name: "chopsticksConfigParser" }); resolveEnvVars = (value) => { return value.replace(/\$\{env\.([^}]+)\}/g, (_, varName) => { const envValue = process.env[varName]; if (envValue === void 0) { logger.warn(`Environment variable ${varName} is not set`); return ""; } return envValue; }); }; resolveEnvVarsDeep = (value) => { if (typeof value === "string") { return resolveEnvVars(value); } if (Array.isArray(value)) { return value.map(resolveEnvVarsDeep); } if (value !== null && typeof value === "object") { const result = {}; for (const [key, val] of Object.entries(value)) { result[key] = resolveEnvVarsDeep(val); } return result; } return value; }; parseBuildBlockMode = (mode) => { switch (mode?.toLowerCase()) { case "batch": return BuildBlockModeValues.Batch; case "instant": return BuildBlockModeValues.Instant; case "manual": default: return BuildBlockModeValues.Manual; } }; parseBlockField = (block) => { if (block === void 0 || block === null) { return block; } if (typeof block === "number") { return block; } if (block === "") { return void 0; } const blockNum = Number(block); if (!Number.isNaN(blockNum)) { return blockNum; } return block; }; parseChopsticksConfigFile = (configPath, overrides) => Effect2.gen(function* () { const fileContent = yield* Effect2.tryPromise({ try: async () => { const content = await fs.promises.readFile(configPath, "utf-8"); return content; }, catch: (cause) => new ChopsticksSetupError({ cause, endpoint: `file://${configPath}` }) }); const rawConfigUnresolved = yield* Effect2.try({ try: () => yaml.parse(fileContent), catch: (cause) => new ChopsticksSetupError({ cause: new Error(`Failed to parse YAML config: ${cause}`), endpoint: `file://${configPath}` }) }); const rawConfig = resolveEnvVarsDeep(rawConfigUnresolved); const rawEndpoint = rawConfig.endpoint; const endpoint = typeof rawEndpoint === "string" ? rawEndpoint : Array.isArray(rawEndpoint) ? rawEndpoint[0] : ""; if (!endpoint) { return yield* Effect2.fail( new ChopsticksSetupError({ cause: new Error( `Endpoint is required but not configured. Check that the environment variable in your chopsticks config is set. Raw value: "${rawConfigUnresolved.endpoint ?? ""}"` ), endpoint: String(rawConfigUnresolved.endpoint) || "undefined" }) ); } if (!endpoint.startsWith("ws://") && !endpoint.startsWith("wss://")) { return yield* Effect2.fail( new ChopsticksSetupError({ cause: new Error( `Invalid endpoint format: "${endpoint}" - must start with ws:// or wss://` ), endpoint }) ); } const block = parseBlockField(rawConfig.block); const rawBuildBlockMode = rawConfig["build-block-mode"]; const buildBlockMode = overrides?.buildBlockMode !== void 0 ? parseBuildBlockMode(overrides.buildBlockMode) : rawBuildBlockMode !== void 0 ? parseBuildBlockMode(rawBuildBlockMode) : BuildBlockModeValues.Manual; const finalPort = overrides?.port ?? rawConfig.port ?? 8e3; logger.debug(`Parsed chopsticks config from ${configPath}`); logger.debug(` endpoint: ${endpoint}`); logger.debug(` port: ${finalPort}`); const config = { ...rawConfig, block, "build-block-mode": buildBlockMode, port: finalPort, ...overrides?.host !== void 0 && { host: overrides.host }, ...overrides?.wasmOverride !== void 0 && { "wasm-override": overrides.wasmOverride }, ...overrides?.allowUnresolvedImports !== void 0 && { "allow-unresolved-imports": overrides.allowUnresolvedImports } }; return config; }); } }); // src/internal/effect/launchChopsticksEffect.ts var launchChopsticksEffect_exports = {}; __export(launchChopsticksEffect_exports, { BuildBlockModeValues: () => BuildBlockModeValues2, ChopsticksServiceLayer: () => ChopsticksServiceLayer, ChopsticksServiceLive: () => ChopsticksServiceLive, configureChopsticksLogger: () => configureChopsticksLogger, launchChopsticksEffect: () => launchChopsticksEffect, launchChopsticksEffectProgram: () => launchChopsticksEffectProgram, launchChopsticksFromSpec: () => launchChopsticksFromSpec, setChopsticksLogStream: () => setChopsticksLogStream }); import { Effect as Effect3, Layer } from "effect"; import { createLogger as createLogger2 } from "@moonwall/util"; import * as fs2 from "fs"; import * as path from "path"; function createLogFile(port) { const dirPath = path.join(process.cwd(), "tmp", "node_logs"); if (!fs2.existsSync(dirPath)) { fs2.mkdirSync(dirPath, { recursive: true }); } const logPath = path.join(dirPath, `chopsticks_${port}_${Date.now()}.log`); const writeStream = fs2.createWriteStream(logPath); process.env.MOON_LOG_LOCATION = logPath; return { logPath, writeStream }; } function writeLog(stream, level, message) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); stream.write(`[${timestamp}] [${level.toUpperCase()}] ${message} `); } function setChopsticksLogStream(stream) { chopsticksLogStream = stream; } function hookLoggerToFile(pinoInstance, loggerName) { const wrapMethod = (level, originalMethod) => { return function(...args) { originalMethod.apply(this, args); if (chopsticksLogStream && args.length > 0) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const message = typeof args[0] === "string" ? args[0] : JSON.stringify(args[0]); chopsticksLogStream.write( `[${timestamp}] [${level.toUpperCase()}] (${loggerName}) ${message} ` ); } }; }; const originalInfo = pinoInstance.info.bind(pinoInstance); const originalWarn = pinoInstance.warn.bind(pinoInstance); const originalError = pinoInstance.error.bind(pinoInstance); const originalDebug = pinoInstance.debug.bind(pinoInstance); pinoInstance.info = wrapMethod("info", originalInfo); pinoInstance.warn = wrapMethod("warn", originalWarn); pinoInstance.error = wrapMethod("error", originalError); pinoInstance.debug = wrapMethod("debug", originalDebug); const originalChild = pinoInstance.child?.bind(pinoInstance); if (originalChild) { pinoInstance.child = (bindings) => { const child = originalChild(bindings); const childName = bindings.name || bindings.child || loggerName; hookLoggerToFile(child, childName); return child; }; } } function configureChopsticksLogger(level = "inherit") { if (chopsticksLoggerConfigured) { return; } chopsticksLoggerConfigured = true; if (!chopsticksModuleCache) { return; } const resolvedLevel = level === "inherit" ? process.env.LOG_LEVEL || "info" : level; chopsticksModuleCache.pinoLogger.level = resolvedLevel; logger2.debug(`Chopsticks internal logger level: ${resolvedLevel}`); } async function getChopsticksModules(logLevel = "inherit") { if (chopsticksModuleCache) { return chopsticksModuleCache; } const chopsticks = await import("@acala-network/chopsticks"); chopsticksModuleCache = { setupWithServer: chopsticks.setupWithServer, setStorage: chopsticks.setStorage, pinoLogger: chopsticks.pinoLogger, defaultLogger: chopsticks.defaultLogger }; const resolvedLevel = logLevel === "inherit" ? process.env.LOG_LEVEL || "info" : logLevel; chopsticksModuleCache.pinoLogger.level = resolvedLevel; hookLoggerToFile(chopsticksModuleCache.defaultLogger, "chopsticks"); chopsticksLoggerConfigured = true; logger2.debug(`Chopsticks internal logger level: ${resolvedLevel}`); return chopsticksModuleCache; } async function launchChopsticksEffect(config) { const startTime = Date.now(); logger2.debug(`[T+0ms] Starting chopsticks with endpoint: ${config.endpoint}`); const port = config.port ?? 8e3; const { logPath, writeStream } = createLogFile(port); setChopsticksLogStream(writeStream); const program = Effect3.gen(function* () { const chopsticksModules = yield* Effect3.promise(() => getChopsticksModules("inherit")); const args = prepareConfigForSetup(config); logger2.debug(`[T+${Date.now() - startTime}ms] Calling setupWithServer...`); const context = yield* Effect3.tryPromise({ try: () => chopsticksModules.setupWithServer(args), catch: (cause) => new ChopsticksSetupError({ cause, endpoint: getEndpointString(config.endpoint), block: config.block ?? void 0 }) }); const actualPort = Number.parseInt(context.addr.split(":")[1], 10); const chainName = yield* Effect3.promise(() => context.chain.api.getSystemChain()); logger2.info(`${chainName} RPC listening on ws://${context.addr}`); logger2.debug(`[T+${Date.now() - startTime}ms] Chopsticks started at ${context.addr}`); logger2.debug(`Log file: ${logPath}`); writeLog(writeStream, "info", `Chopsticks started for ${chainName}`); writeLog(writeStream, "info", `RPC listening on ws://${context.addr}`); writeLog(writeStream, "info", `Endpoint: ${config.endpoint}`); if (config.block) { writeLog(writeStream, "info", `Block: ${config.block}`); } const cleanup2 = Effect3.tryPromise({ try: async () => { logger2.debug("Closing chopsticks..."); writeLog(writeStream, "info", "Shutting down chopsticks..."); await context.close(); writeLog(writeStream, "info", "Chopsticks closed"); setChopsticksLogStream(null); writeStream.end(); logger2.debug("Chopsticks closed"); }, catch: (cause) => new ChopsticksCleanupError({ cause }) }).pipe( Effect3.catchAll( (error) => Effect3.sync(() => { logger2.error(`Failed to cleanly close chopsticks: ${error}`); writeLog(writeStream, "error", `Failed to close: ${error}`); setChopsticksLogStream(null); writeStream.end(); }) ) ); const serviceMethods = createServiceMethods(context.chain); const service2 = { chain: context.chain, addr: context.addr, port: actualPort, ...serviceMethods }; return { service: service2, cleanup: cleanup2 }; }); const { service, cleanup } = await Effect3.runPromise(program); return { result: service, cleanup: () => Effect3.runPromise(cleanup) }; } async function launchChopsticksFromSpec(spec, options) { const timeout = options?.timeout ?? 6e4; const startTime = Date.now(); logger2.debug(`Launching chopsticks from spec: ${spec.configPath}`); const parseEffect = parseChopsticksConfigFile(spec.configPath, { port: spec.wsPort, host: spec.address, buildBlockMode: spec.buildBlockMode, wasmOverride: spec.wasmOverride, allowUnresolvedImports: spec.allowUnresolvedImports }); let configResult; try { configResult = await Effect3.runPromise( parseEffect.pipe( Effect3.timeout(timeout), Effect3.catchTag( "TimeoutException", () => Effect3.fail( new ChopsticksSetupError({ cause: new Error(`Config parsing timed out after ${timeout}ms`), endpoint: spec.configPath }) ) ) ) ); } catch (error) { const errorString = String(error); const causeMatch = errorString.match(/\[cause\]:\s*Error:\s*(.+)/s); if (causeMatch) { throw new Error(`Chopsticks config validation failed: ${causeMatch[1].trim()}`); } throw new Error(`Chopsticks config validation failed: ${errorString}`); } logger2.debug(`Config parsed in ${Date.now() - startTime}ms`); logger2.debug(` endpoint: ${configResult.endpoint}`); logger2.debug(` port: ${configResult.port}`); let service; let cleanup; try { const result = await launchChopsticksEffect(configResult); service = result.result; cleanup = result.cleanup; } catch (error) { const errorString = String(error); const causeMatch = errorString.match(/\[cause\]:\s*Error:\s*(.+)/s); const causeMessage = causeMatch ? causeMatch[1].trim() : errorString; throw new Error( `Chopsticks failed to connect to endpoint '${configResult.endpoint}': ${causeMessage}` ); } logger2.debug(`Chopsticks launched in ${Date.now() - startTime}ms at ${service.addr}`); return { service, cleanup, port: service.port, addr: service.addr }; } var BuildBlockModeValues2, chopsticksLoggerConfigured, chopsticksModuleCache, logger2, chopsticksLogStream, getEndpointString, prepareConfigForSetup, createServiceMethods, launchChopsticksEffectProgram, acquireChopsticks, ChopsticksServiceLayer, ChopsticksServiceLive; var init_launchChopsticksEffect = __esm({ "src/internal/effect/launchChopsticksEffect.ts"() { "use strict"; init_ChopsticksService(); init_chopsticksConfigParser(); BuildBlockModeValues2 = { Batch: "Batch", Manual: "Manual", Instant: "Instant" }; chopsticksLoggerConfigured = false; chopsticksModuleCache = null; logger2 = createLogger2({ name: "launchChopsticksEffect" }); chopsticksLogStream = null; getEndpointString = (endpoint) => { if (typeof endpoint === "string") return endpoint; if (Array.isArray(endpoint)) return endpoint[0]; return void 0; }; prepareConfigForSetup = (config) => ({ ...config, port: config.port ?? 8e3, host: config.host ?? "127.0.0.1", "build-block-mode": config["build-block-mode"] ?? "Manual" }); createServiceMethods = (chain) => ({ createBlock: (params) => Effect3.tryPromise({ try: async () => { const block = await chain.newBlock({ transactions: params?.transactions ?? [], upwardMessages: params?.ump ?? {}, downwardMessages: params?.dmp ?? [], horizontalMessages: params?.hrmp ?? {} }); return { block: { hash: block.hash, number: block.number } }; }, catch: (cause) => new ChopsticksBlockError({ cause, operation: "newBlock" }) }), setStorage: (params) => Effect3.tryPromise({ try: async () => { const storage = { [params.module]: { [params.method]: params.params } }; const modules = await getChopsticksModules(); await modules.setStorage(chain, storage); }, catch: (cause) => new ChopsticksStorageError({ cause, module: params.module, method: params.method }) }), submitExtrinsic: (extrinsic) => Effect3.tryPromise({ try: () => chain.submitExtrinsic(extrinsic), catch: (cause) => new ChopsticksExtrinsicError({ cause, operation: "submit", extrinsic }) }), dryRunExtrinsic: (extrinsic, at) => Effect3.tryPromise({ try: async () => { const result = await chain.dryRunExtrinsic(extrinsic, at); const isOk = result.outcome.isOk; return { success: isOk, storageDiff: result.storageDiff, error: isOk ? void 0 : result.outcome.asErr?.toString() }; }, catch: (cause) => new ChopsticksExtrinsicError({ cause, operation: "dryRun", extrinsic: typeof extrinsic === "string" ? extrinsic : extrinsic.call }) }), getBlock: (hashOrNumber) => Effect3.tryPromise({ try: async () => { const block = hashOrNumber === void 0 ? chain.head : typeof hashOrNumber === "number" ? await chain.getBlockAt(hashOrNumber) : await chain.getBlock(hashOrNumber); if (!block) return void 0; return { hash: block.hash, number: block.number }; }, catch: (cause) => new ChopsticksBlockError({ cause, operation: "getBlock", blockIdentifier: hashOrNumber }) }), setHead: (hashOrNumber) => Effect3.tryPromise({ try: async () => { const block = typeof hashOrNumber === "number" ? await chain.getBlockAt(hashOrNumber) : await chain.getBlock(hashOrNumber); if (!block) { throw new Error(`Block not found: ${hashOrNumber}`); } await chain.setHead(block); }, catch: (cause) => new ChopsticksBlockError({ cause, operation: "setHead", blockIdentifier: hashOrNumber }) }), submitUpwardMessages: (paraId, messages) => Effect3.try({ try: () => chain.submitUpwardMessages(paraId, messages), catch: (cause) => new ChopsticksXcmError({ cause, messageType: "ump", paraId }) }), submitDownwardMessages: (messages) => Effect3.try({ try: () => chain.submitDownwardMessages(messages), catch: (cause) => new ChopsticksXcmError({ cause, messageType: "dmp" }) }), submitHorizontalMessages: (paraId, messages) => Effect3.try({ try: () => chain.submitHorizontalMessages(paraId, messages), catch: (cause) => new ChopsticksXcmError({ cause, messageType: "hrmp", paraId }) }) }); launchChopsticksEffectProgram = (config) => Effect3.gen(function* () { const chopsticksModules = yield* Effect3.promise(() => getChopsticksModules("silent")); const args = prepareConfigForSetup(config); const context = yield* Effect3.tryPromise({ try: () => chopsticksModules.setupWithServer(args), catch: (cause) => new ChopsticksSetupError({ cause, endpoint: getEndpointString(config.endpoint), block: config.block ?? void 0 }) }); const port = Number.parseInt(context.addr.split(":")[1], 10); const cleanup = Effect3.tryPromise({ try: () => context.close(), catch: (cause) => new ChopsticksCleanupError({ cause }) }).pipe( Effect3.catchAll( (error) => Effect3.sync(() => { logger2.error(`Failed to cleanly close chopsticks: ${error}`); }) ) ); const serviceMethods = createServiceMethods(context.chain); const service = { chain: context.chain, addr: context.addr, port, ...serviceMethods }; return { result: service, cleanup }; }); acquireChopsticks = (config) => Effect3.acquireRelease( // Acquire: Setup chopsticks (first get modules, then setup) Effect3.promise(() => getChopsticksModules("silent")).pipe( Effect3.flatMap( (modules) => Effect3.tryPromise({ try: () => modules.setupWithServer(prepareConfigForSetup(config)), catch: (cause) => new ChopsticksSetupError({ cause, endpoint: getEndpointString(config.endpoint), block: config.block ?? void 0 }) }) ), Effect3.tap( (context) => Effect3.sync(() => logger2.debug(`Chopsticks started at ${context.addr}`)) ) ), // Release: Cleanup chopsticks (context) => Effect3.tryPromise({ try: async () => { logger2.debug("Closing chopsticks..."); await context.close(); logger2.debug("Chopsticks closed"); }, catch: (cause) => new ChopsticksCleanupError({ cause }) }).pipe( Effect3.catchAll( (error) => Effect3.sync(() => { logger2.error(`Failed to cleanly close chopsticks: ${error}`); }) ) ) ); ChopsticksServiceLayer = (config) => Layer.scoped( ChopsticksService, Effect3.gen(function* () { const context = yield* acquireChopsticks(config); const port = Number.parseInt(context.addr.split(":")[1], 10); const serviceMethods = createServiceMethods(context.chain); return { chain: context.chain, addr: context.addr, port, ...serviceMethods }; }) ); ChopsticksServiceLive = Layer.scoped( ChopsticksService, Effect3.gen(function* () { const config = yield* ChopsticksConfigTag; const context = yield* acquireChopsticks(config); const port = Number.parseInt(context.addr.split(":")[1], 10); const serviceMethods = createServiceMethods(context.chain); return { chain: context.chain, addr: context.addr, port, ...serviceMethods }; }) ); } }); // src/internal/effect/__tests__/launchChopsticksEffect.test.ts init_ChopsticksService(); import { describe, it, expect } from "vitest"; import { Effect as Effect4, Layer as Layer2 } from "effect"; import { BuildBlockMode } from "@acala-network/chopsticks"; describe("launchChopsticksEffect - Phase 2: Module Structure", () => { describe("Module Exports", () => { it("should export launchChopsticksEffect function", async () => { const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); expect(module.launchChopsticksEffect).toBeDefined(); expect(typeof module.launchChopsticksEffect).toBe("function"); }); it("should export launchChopsticksEffectProgram function", async () => { const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); expect(module.launchChopsticksEffectProgram).toBeDefined(); expect(typeof module.launchChopsticksEffectProgram).toBe("function"); }); it("should export ChopsticksLaunchResult type", async () => { const _typeCheck = { chain: {}, addr: "127.0.0.1:8000", port: 8e3 }; expect(_typeCheck.port).toBe(8e3); }); it("should export ChopsticksServiceImpl type", async () => { const _typeCheck = { chain: {}, addr: "127.0.0.1:8000", port: 8e3, createBlock: () => Effect4.succeed({ block: { hash: "0x123", number: 1 } }), setStorage: () => Effect4.void, submitExtrinsic: () => Effect4.succeed("0x"), dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }), getBlock: () => Effect4.succeed({ hash: "0x123", number: 1 }), setHead: () => Effect4.void, submitUpwardMessages: () => Effect4.void, submitDownwardMessages: () => Effect4.void, submitHorizontalMessages: () => Effect4.void }; expect(_typeCheck.addr).toBe("127.0.0.1:8000"); }); }); describe("ChopsticksServiceImpl Interface", () => { it("should have all required properties", () => { const mockService = { chain: {}, addr: "127.0.0.1:9000", port: 9e3, createBlock: () => Effect4.succeed({ block: { hash: "0xabc", number: 42 } }), setStorage: () => Effect4.void, submitExtrinsic: () => Effect4.succeed("0xhash"), dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }), getBlock: () => Effect4.succeed({ hash: "0xdef", number: 100 }), setHead: () => Effect4.void, submitUpwardMessages: () => Effect4.void, submitDownwardMessages: () => Effect4.void, submitHorizontalMessages: () => Effect4.void }; expect(mockService.addr).toBe("127.0.0.1:9000"); expect(mockService.port).toBe(9e3); expect(mockService.chain).toBeDefined(); }); it("should allow createBlock to return BlockCreationResult", async () => { const mockService = { chain: {}, addr: "127.0.0.1:8000", port: 8e3, createBlock: () => Effect4.succeed({ block: { hash: "0x123456", number: 999 } }), setStorage: () => Effect4.void, submitExtrinsic: () => Effect4.succeed("0x"), dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }), getBlock: () => Effect4.succeed(void 0), setHead: () => Effect4.void, submitUpwardMessages: () => Effect4.void, submitDownwardMessages: () => Effect4.void, submitHorizontalMessages: () => Effect4.void }; const result = await Effect4.runPromise(mockService.createBlock()); expect(result.block.hash).toBe("0x123456"); expect(result.block.number).toBe(999); }); it("should allow createBlock to fail with ChopsticksBlockError", async () => { const mockService = { chain: {}, addr: "127.0.0.1:8000", port: 8e3, createBlock: () => Effect4.fail( new ChopsticksBlockError({ cause: new Error("Block creation failed"), operation: "newBlock" }) ), setStorage: () => Effect4.void, submitExtrinsic: () => Effect4.succeed("0x"), dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }), getBlock: () => Effect4.succeed(void 0), setHead: () => Effect4.void, submitUpwardMessages: () => Effect4.void, submitDownwardMessages: () => Effect4.void, submitHorizontalMessages: () => Effect4.void }; const result = await Effect4.runPromise( mockService.createBlock().pipe( Effect4.catchTag( "ChopsticksBlockError", (error) => Effect4.succeed({ caught: true, operation: error.operation }) ) ) ); expect(result).toEqual({ caught: true, operation: "newBlock" }); }); it("should allow setStorage to fail with ChopsticksStorageError", async () => { const mockService = { chain: {}, addr: "127.0.0.1:8000", port: 8e3, createBlock: () => Effect4.succeed({ block: { hash: "0x", number: 1 } }), setStorage: () => Effect4.fail( new ChopsticksStorageError({ cause: new Error("Storage write failed"), module: "System", method: "Account" }) ), submitExtrinsic: () => Effect4.succeed("0x"), dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }), getBlock: () => Effect4.succeed(void 0), setHead: () => Effect4.void, submitUpwardMessages: () => Effect4.void, submitDownwardMessages: () => Effect4.void, submitHorizontalMessages: () => Effect4.void }; const result = await Effect4.runPromise( mockService.setStorage({ module: "System", method: "Account", params: [] }).pipe( Effect4.catchTag( "ChopsticksStorageError", (error) => Effect4.succeed({ caught: true, module: error.module, method: error.method }) ) ) ); expect(result).toEqual({ caught: true, module: "System", method: "Account" }); }); }); describe("Config Conversion (kebab-case)", () => { it("should accept config with required fields", () => { const config = { endpoint: "wss://rpc.polkadot.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }; expect(config.endpoint).toBe("wss://rpc.polkadot.io"); expect(config.port).toBe(8e3); expect(config["build-block-mode"]).toBe(BuildBlockMode.Manual); }); it("should accept full config with all options using kebab-case", () => { const config = { endpoint: "wss://rpc.polkadot.io", block: 12345, port: 9e3, host: "0.0.0.0", "build-block-mode": BuildBlockMode.Manual, "wasm-override": "/path/to/wasm", "allow-unresolved-imports": true, "mock-signature-host": true, db: "./chopsticks.db", "import-storage": { System: { Account: {} } }, "runtime-log-level": 3, "rpc-timeout": 3e4 // New field supported via chopsticks type }; expect(config.endpoint).toBe("wss://rpc.polkadot.io"); expect(config.block).toBe(12345); expect(config.port).toBe(9e3); expect(config.host).toBe("0.0.0.0"); expect(config["build-block-mode"]).toBe(BuildBlockMode.Manual); expect(config["wasm-override"]).toBe("/path/to/wasm"); expect(config["allow-unresolved-imports"]).toBe(true); expect(config["mock-signature-host"]).toBe(true); expect(config.db).toBe("./chopsticks.db"); expect(config["runtime-log-level"]).toBe(3); expect(config["rpc-timeout"]).toBe(3e4); }); it("should accept config with block as hash string", () => { const config = { endpoint: "wss://rpc.polkadot.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual, block: "0x1234567890abcdef" }; expect(config.block).toBe("0x1234567890abcdef"); }); it("should accept config with block as null for latest", () => { const config = { endpoint: "wss://rpc.polkadot.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual, block: null }; expect(config.block).toBeNull(); }); }); describe("launchChopsticksEffectProgram Effect Type", () => { it("should return an Effect that requires no context when config is provided inline", async () => { const { launchChopsticksEffectProgram: launchChopsticksEffectProgram2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const program = launchChopsticksEffectProgram2({ endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); expect(typeof program.pipe).toBe("function"); }); it("should produce ChopsticksSetupError on failure", async () => { const { launchChopsticksEffectProgram: launchChopsticksEffectProgram2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const program = launchChopsticksEffectProgram2({ endpoint: "wss://nonexistent.invalid", port: 8e3, "build-block-mode": BuildBlockMode.Manual }).pipe( Effect4.catchTag( "ChopsticksSetupError", (error) => Effect4.succeed({ caught: true, endpoint: error.endpoint }) ) ); expect(typeof program.pipe).toBe("function"); }); }); describe("Return Value Structure", () => { it("should return object with result and cleanup when successful", () => { const mockReturn = { result: { chain: {}, addr: "127.0.0.1:8000", port: 8e3, createBlock: () => Effect4.succeed({ block: { hash: "0x", number: 1 } }), setStorage: () => Effect4.void, submitExtrinsic: () => Effect4.succeed("0x"), dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }), getBlock: () => Effect4.succeed(void 0), setHead: () => Effect4.void, submitUpwardMessages: () => Effect4.void, submitDownwardMessages: () => Effect4.void, submitHorizontalMessages: () => Effect4.void }, cleanup: async () => { } }; expect(mockReturn.result).toBeDefined(); expect(mockReturn.cleanup).toBeDefined(); expect(typeof mockReturn.cleanup).toBe("function"); }); }); }); describe("ChopsticksServiceLayer - Phase 3: Layer.scoped", () => { describe("Module Exports", () => { it("should export ChopsticksServiceLayer function", async () => { const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); expect(module.ChopsticksServiceLayer).toBeDefined(); expect(typeof module.ChopsticksServiceLayer).toBe("function"); }); it("should export ChopsticksServiceLive Layer", async () => { const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); expect(module.ChopsticksServiceLive).toBeDefined(); }); }); describe("ChopsticksServiceLayer Type", () => { it("should return a Layer when called with config", async () => { const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const layer = ChopsticksServiceLayer2({ endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); expect(layer).toBeDefined(); }); it("should create Layer that provides ChopsticksService", async () => { const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { ChopsticksService: ChopsticksService2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports)); const layer = ChopsticksServiceLayer2({ endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); const program = Effect4.gen(function* () { const service = yield* ChopsticksService2; return service.addr; }); const providedProgram = program.pipe(Effect4.provide(layer)); expect(typeof providedProgram.pipe).toBe("function"); }); it("should accept all config options using kebab-case", async () => { const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { BuildBlockMode: BuildBlockMode2 } = await import("@acala-network/chopsticks"); const layer = ChopsticksServiceLayer2({ endpoint: "wss://rpc.polkadot.io", block: 12345, port: 9e3, host: "0.0.0.0", "build-block-mode": BuildBlockMode2.Manual, "wasm-override": "/path/to/wasm", "allow-unresolved-imports": true, "mock-signature-host": true, db: "./chopsticks.db", "runtime-log-level": 3, "rpc-timeout": 3e4 }); expect(layer).toBeDefined(); }); }); describe("ChopsticksServiceLive Type", () => { it("should require ChopsticksConfigTag in context", async () => { const { ChopsticksServiceLive: ChopsticksServiceLive2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { ChopsticksService: ChopsticksService2, ChopsticksConfigTag: ChopsticksConfigTag2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports)); const configLayer = Layer2.succeed(ChopsticksConfigTag2, { endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); const program = Effect4.gen(function* () { const service = yield* ChopsticksService2; return service.addr; }); const fullLayer = ChopsticksServiceLive2.pipe(Layer2.provide(configLayer)); const providedProgram = program.pipe(Effect4.provide(fullLayer)); expect(typeof providedProgram.pipe).toBe("function"); }); it("should allow Layer composition patterns", async () => { const { ChopsticksServiceLive: ChopsticksServiceLive2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { ChopsticksConfigTag: ChopsticksConfigTag2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports)); const configLayer = Layer2.succeed(ChopsticksConfigTag2, { endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); const serviceLayer = ChopsticksServiceLive2.pipe(Layer2.provide(configLayer)); expect(serviceLayer).toBeDefined(); }); }); describe("Layer Error Handling", () => { it("should produce ChopsticksSetupError on failure via ChopsticksServiceLayer", async () => { const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { ChopsticksService: ChopsticksService2, ChopsticksSetupError: ChopsticksSetupError3 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports)); const layer = ChopsticksServiceLayer2({ endpoint: "wss://nonexistent.invalid", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); const program = Effect4.gen(function* () { const service = yield* ChopsticksService2; return service.addr; }).pipe( Effect4.provide(layer), Effect4.catchTag( "ChopsticksSetupError", (error) => Effect4.succeed({ caught: true, endpoint: error.endpoint }) ) ); expect(typeof program.pipe).toBe("function"); }); it("should produce ChopsticksSetupError on failure via ChopsticksServiceLive", async () => { const { ChopsticksServiceLive: ChopsticksServiceLive2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { ChopsticksService: ChopsticksService2, ChopsticksConfigTag: ChopsticksConfigTag2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports)); const configLayer = Layer2.succeed(ChopsticksConfigTag2, { endpoint: "wss://nonexistent.invalid", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); const fullLayer = ChopsticksServiceLive2.pipe(Layer2.provide(configLayer)); const program = Effect4.gen(function* () { const service = yield* ChopsticksService2; return service.addr; }).pipe( Effect4.provide(fullLayer), Effect4.catchTag( "ChopsticksSetupError", (error) => Effect4.succeed({ caught: true, endpoint: error.endpoint }) ) ); expect(typeof program.pipe).toBe("function"); }); }); describe("Scope Management", () => { it("should be usable with Effect.scoped for manual scope control", async () => { const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { ChopsticksService: ChopsticksService2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports)); const layer = ChopsticksServiceLayer2({ endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); const program = Effect4.scoped( Effect4.gen(function* () { const service = yield* ChopsticksService2; return service.addr; }).pipe(Effect4.provide(layer)) ); expect(typeof program.pipe).toBe("function"); }); }); }); describe.skip("launchChopsticksEffect - Integration Tests", () => { it("should launch chopsticks and return a working service", async () => { const { launchChopsticksEffect: launchChopsticksEffect2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { result, cleanup } = await launchChopsticksEffect2({ endpoint: "wss://rpc.polkadot.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }); try { expect(result.addr).toMatch(/127\.0\.0\.1:\d+/); expect(result.port).toBe(8e3); expect(result.chain).toBeDefined(); } finally { await cleanup(); } }); it("should allow creating blocks after launch", async () => { const { launchChopsticksEffect: launchChopsticksEffect2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports)); const { result, cleanup } = await launchChopsticksEffect2({ endpoint: "wss://rpc.polkadot.io", port: 8001, "build-block-mode": BuildBlockMode.Manual }); try { const blockResult = await Effect4.runPromise(result.createBlock()); expect(blockResult.block.number).toBeGreaterThan(0); expect(blockResult.block.hash).toMatch(/^0x/); } finally { await cleanup(); } }); });