From 14b0db4e47d28b65e857ad0cec986c40f3f09ee4 Mon Sep 17 00:00:00 2001 From: Vagabond Date: Fri, 18 Sep 2026 00:51:42 +0700 Subject: [PATCH 1/5] test: cover init modules and pipeline --- .github/workflows/cli.yml | 3 + cli/test/modules/config/module.test.ts | 27 +++ cli/test/modules/deployGeneration/harness.ts | 2 + .../modules/diamondGeneration/module.test.ts | 49 ++++ cli/test/modules/init/module.test.ts | 76 ++++++ cli/test/modules/preflight/module.test.ts | 43 ++++ cli/test/modules/projectDir/module.test.ts | 55 +++++ cli/test/modules/scaffolding/module.test.ts | 44 ++++ .../modules/testGeneration/module.test.ts | 40 +++ cli/test/pipelines/initPipeline/harness.ts | 129 ++++++++++ .../initPipeline/initPipeline.test.ts | 227 +++++++++--------- 11 files changed, 583 insertions(+), 112 deletions(-) create mode 100644 cli/test/modules/config/module.test.ts create mode 100644 cli/test/modules/diamondGeneration/module.test.ts create mode 100644 cli/test/modules/init/module.test.ts create mode 100644 cli/test/modules/preflight/module.test.ts create mode 100644 cli/test/modules/projectDir/module.test.ts create mode 100644 cli/test/modules/scaffolding/module.test.ts create mode 100644 cli/test/modules/testGeneration/module.test.ts create mode 100644 cli/test/pipelines/initPipeline/harness.ts diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index c988f9c6..5d839c25 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -34,6 +34,9 @@ jobs: - name: Build TypeScript run: npm run cli@build + - name: Run tests + run: npm run cli@test + - name: Lint run: npm run cli@lint diff --git a/cli/test/modules/config/module.test.ts b/cli/test/modules/config/module.test.ts new file mode 100644 index 00000000..127d5514 --- /dev/null +++ b/cli/test/modules/config/module.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { Context } from "../../../src/context/context"; +import { ConfigModule } from "../../../src/modules/config/module"; +import { BasesCatalog } from "../../../src/modules/config/types"; + +/** Tests catalog loading independently from init selection and scaffolding. */ +describe("ConfigModule", () => { + it("loads and indexes the shipped bases catalog", async () => { + const ctx = Context.create(); + + const result = await ConfigModule.loadBasesCatalog(ctx); + const catalog = result.config.bases as BasesCatalog; + + expect(result).toBe(ctx); + expect(result.state.config?.success).toBe(true); + expect(catalog.globals.diamond?.required).toHaveProperty("DiamondInspectFacet"); + expect(catalog.globals.libraries?.optional).toHaveProperty("ERC165Facet"); + expect(Object.keys(catalog.features)).toEqual(expect.arrayContaining([ + "counter", + "erc-20", + "erc-721", + "owner", + "access-control", + ])); + expect(ConfigModule.getDiamondCompilerVersion(catalog)).toBe("0.8.30"); + }); +}); diff --git a/cli/test/modules/deployGeneration/harness.ts b/cli/test/modules/deployGeneration/harness.ts index 91869e7c..7d2b286d 100644 --- a/cli/test/modules/deployGeneration/harness.ts +++ b/cli/test/modules/deployGeneration/harness.ts @@ -37,6 +37,8 @@ const catalog: BasesCatalog = { globals: { diamond: { label: "Diamond", + pragma: ">=0.8.30", + compilerVersion: "0.8.30", required: { DiamondInspectFacet: facet(facetPaths.DiamondInspectFacet), }, diff --git a/cli/test/modules/diamondGeneration/module.test.ts b/cli/test/modules/diamondGeneration/module.test.ts new file mode 100644 index 00000000..7011a6bb --- /dev/null +++ b/cli/test/modules/diamondGeneration/module.test.ts @@ -0,0 +1,49 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { ModuleState } from "../../../src/context/types"; +import { BasesCatalog } from "../../../src/modules/config/types"; +import { DiamondGenerationModule } from "../../../src/modules/diamondGeneration/module"; +import { createDeployGenerationHarness } from "../deployGeneration/harness"; + +type DiamondGenerationResult = { + outputPath: string; + imports: string[]; + constructorEntries: unknown[]; +}; + +/** Tests Diamond.sol generation from the full ERC-20 fixture selection. */ +describe("DiamondGenerationModule", () => { + it("generates Diamond.sol with selected facet imports and constructors", async () => { + const harness = await createDeployGenerationHarness(); + const sourceRoot = path.join(harness.projectRoot, "src"); + const catalog = harness.ctx.config.bases as BasesCatalog; + catalog.features.owner.required.OwnerDataFacet = { + ...catalog.features.owner.required.OwnerDataFacet, + mod: "@perfect-abstractions/compose/access/Owner/Data/OwnerDataMod.sol", + constructor: [{ + comments: ["Set the initial contract owner."], + code: "OwnerDataMod.setContractOwner(msg.sender);", + }], + }; + + try { + const result = await DiamondGenerationModule.generateDiamondContract(harness.ctx, sourceRoot); + const state = result.state.generateDiamondContract as ModuleState; + const source = await fs.readFile(path.join(sourceRoot, "Diamond.sol"), "utf8"); + + expect(state.success).toBe(true); + expect(state.result?.outputPath).toBe(path.join(sourceRoot, "Diamond.sol")); + expect(state.result?.imports).toEqual(expect.arrayContaining([ + "@perfect-abstractions/compose/diamond/DiamondMod.sol", + "@perfect-abstractions/compose/access/Owner/Data/OwnerDataMod.sol", + ])); + expect(state.result?.constructorEntries.length).toBeGreaterThan(0); + expect(source).toContain("contract Diamond"); + expect(source).toContain("DiamondMod.addFacets(_facets);"); + expect(source).toContain("OwnerDataMod.setContractOwner(msg.sender);"); + } finally { + await harness.cleanup(); + } + }); +}); diff --git a/cli/test/modules/init/module.test.ts b/cli/test/modules/init/module.test.ts new file mode 100644 index 00000000..1dc5e433 --- /dev/null +++ b/cli/test/modules/init/module.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { Context } from "../../../src/context/context"; +import { ComposeContext } from "../../../src/context/types"; +import { ConfigModule } from "../../../src/modules/config/module"; +import { InitModule } from "../../../src/modules/init/module"; + +async function createContext(param: Record): Promise { + const ctx = Context.create(); + Object.assign(ctx.param, param); + await ConfigModule.loadBasesCatalog(ctx); + return ctx; +} + +/** Tests non-interactive init flag validation and normalized selection state. */ +describe("InitModule.runInitNonInteractive", () => { + let ctx: ComposeContext; + + beforeEach(async () => { + ctx = await createContext({ + yes: true, + framework: "foundry", + base: "erc-20", + projectName: "token-diamond", + }); + }); + + it("normalizes base, library, extension, ownership, and access selections", async () => { + Object.assign(ctx.param, { + libraries: "ERC165Facet", + extensions: "ERC20BurnFacet", + ownership: "owner", + ownershipExtensions: "OwnerRenounceFacet", + accessControl: "access-control", + accessControlExtensions: "AccessControlGrantBatchFacet", + }); + + const result = await InitModule.runInitNonInteractive(ctx); + + expect(result).toBe(ctx); + expect(result.param.libraries).toEqual(["ERC165Facet"]); + expect(result.param.extensions).toEqual(["ERC20BurnFacet"]); + expect(result.param.access).toEqual(["owner", "access-control"]); + expect(result.param.accessExtensions).toEqual([ + "OwnerRenounceFacet", + "AccessControlGrantBatchFacet", + ]); + expect(result.state.entry?.success).toBe(true); + expect(result.state.entry?.result).toEqual(expect.objectContaining({ + framework: "foundry", + selectedBaseKey: "erc-20", + selectedLibraries: ["ERC165Facet"], + selectedExtensions: ["ERC20BurnFacet"], + selectedAccess: ["owner", "access-control"], + })); + }); + + it("defaults Hardhat projects to the ethers toolbox", async () => { + ctx.param.framework = "hardhat"; + + await InitModule.runInitNonInteractive(ctx); + + expect(ctx.param.toolbox).toBe("ethers"); + }); + + it.each([ + [{ base: undefined }, "Missing required flag: --base"], + [{ base: "unknown" }, "Unknown base: unknown"], + [{ framework: "truffle" }, "Unsupported framework: truffle"], + [{ projectName: "invalid name" }, "Project name must not contain spaces"], + [{ ownership: "owner,owner-two-step" }, "--ownership accepts only one ownership base"], + ])("rejects invalid non-interactive parameters %#", async (override, message) => { + Object.assign(ctx.param, override); + + await expect(InitModule.runInitNonInteractive(ctx)).rejects.toThrow(message); + }); +}); diff --git a/cli/test/modules/preflight/module.test.ts b/cli/test/modules/preflight/module.test.ts new file mode 100644 index 00000000..2007c45a --- /dev/null +++ b/cli/test/modules/preflight/module.test.ts @@ -0,0 +1,43 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Context } from "../../../src/context/context"; + +const { isBinaryInPath } = vi.hoisted(() => ({ + isBinaryInPath: vi.fn(async () => undefined), +})); + +vi.mock("../../../src/utils/exec", () => ({ isBinaryInPath })); + +import { PreflightModule } from "../../../src/modules/preflight/module"; + +/** Tests framework toolchain requirements without reading the host PATH. */ +describe("PreflightModule", () => { + beforeEach(() => { + isBinaryInPath.mockClear(); + }); + + it("requires forge for Foundry", async () => { + const ctx = Context.create(); + ctx.param.framework = "foundry"; + + await expect(PreflightModule.check(ctx)).resolves.toBe(ctx); + expect(isBinaryInPath).toHaveBeenCalledWith("forge"); + }); + + it("requires npm when Hardhat dependencies will be installed", async () => { + const ctx = Context.create(); + ctx.param.framework = "hardhat"; + ctx.param.installDeps = true; + + await PreflightModule.check(ctx); + expect(isBinaryInPath).toHaveBeenCalledWith("npm"); + }); + + it("skips npm when Hardhat dependency installation is disabled", async () => { + const ctx = Context.create(); + ctx.param.framework = "hardhat"; + ctx.param.installDeps = false; + + await PreflightModule.check(ctx); + expect(isBinaryInPath).not.toHaveBeenCalled(); + }); +}); diff --git a/cli/test/modules/projectDir/module.test.ts b/cli/test/modules/projectDir/module.test.ts new file mode 100644 index 00000000..173cbc47 --- /dev/null +++ b/cli/test/modules/projectDir/module.test.ts @@ -0,0 +1,55 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { Context } from "../../../src/context/context"; +import { ProjectDirModule } from "../../../src/modules/projectDir/module"; + +/** Tests project path resolution and empty-directory protection. */ +describe("ProjectDirModule", () => { + it("resolves and creates a new project directory", async () => { + const outputRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-project-dir-")); + const ctx = Context.create(); + ctx.param.outDir = outputRoot; + ctx.param.projectName = "example"; + + try { + await ProjectDirModule.resolve(ctx); + await ProjectDirModule.validate(ctx); + + const expected = path.join(outputRoot, "example"); + expect(ctx.param.projectRoot).toBe(expected); + await expect(fs.access(expected)).resolves.toBeUndefined(); + } finally { + await fs.rm(outputRoot, { recursive: true, force: true }); + } + }); + + it("allows an existing directory containing only .git", async () => { + const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-project-dir-git-")); + const ctx = Context.create(); + ctx.param.projectRoot = projectRoot; + + try { + await fs.mkdir(path.join(projectRoot, ".git")); + await expect(ProjectDirModule.validate(ctx)).resolves.toBe(ctx); + } finally { + await fs.rm(projectRoot, { recursive: true, force: true }); + } + }); + + it("rejects a non-empty target directory", async () => { + const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-project-dir-full-")); + const ctx = Context.create(); + ctx.param.projectRoot = projectRoot; + + try { + await fs.writeFile(path.join(projectRoot, "existing.txt"), "occupied", "utf8"); + await expect(ProjectDirModule.validate(ctx)).rejects.toThrow( + `Target directory is not empty: ${projectRoot}`, + ); + } finally { + await fs.rm(projectRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/cli/test/modules/scaffolding/module.test.ts b/cli/test/modules/scaffolding/module.test.ts new file mode 100644 index 00000000..7df8d0e8 --- /dev/null +++ b/cli/test/modules/scaffolding/module.test.ts @@ -0,0 +1,44 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { ScaffoldingModule } from "../../../src/modules/scaffolding/module"; +import { createDeployGenerationHarness } from "../deployGeneration/harness"; + +/** Tests compose.json assembly and persistence from a prepared scaffold map. */ +describe("ScaffoldingModule", () => { + it("writes selected package facets to compose.json", async () => { + const harness = await createDeployGenerationHarness(); + const contractSourceRoot = path.join(harness.projectRoot, "src"); + + try { + ScaffoldingModule.buildComposeJson(harness.ctx, contractSourceRoot); + await ScaffoldingModule.validateLocalFacetFiles(harness.ctx); + await ScaffoldingModule.writeComposeConfig(harness.ctx); + + const composeJson = JSON.parse( + await fs.readFile(path.join(harness.projectRoot, "compose.json"), "utf8"), + ) as { + project: string; + framework: string; + diamonds: Record; + }>; + }; + const diamond = composeJson.diamonds["my-diamond"]; + + expect(composeJson.project).toBe("my-diamond"); + expect(composeJson.framework).toBe("foundry"); + expect(diamond.contract).toBe("src/Diamond.sol:Diamond"); + expect(Object.keys(diamond.facets)).toHaveLength(16); + expect(diamond.facets.ERC20DataFacet).toEqual({ + source: "package", + contract: "ERC20DataFacet", + package: "@perfect-abstractions/compose", + }); + expect(harness.ctx.state.facetFileValidation?.success).toBe(true); + } finally { + await harness.cleanup(); + } + }); +}); diff --git a/cli/test/modules/testGeneration/module.test.ts b/cli/test/modules/testGeneration/module.test.ts new file mode 100644 index 00000000..6cfcd47a --- /dev/null +++ b/cli/test/modules/testGeneration/module.test.ts @@ -0,0 +1,40 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { ModuleState } from "../../../src/context/types"; +import { DeployGenerationModule } from "../../../src/modules/deployGeneration/module"; +import { TestGenerationModule } from "../../../src/modules/testGeneration/module"; +import { createDeployGenerationHarness } from "../deployGeneration/harness"; + +type TestGenerationResult = { + outputPath: string; + facetCount: number; + framework: string; +}; + +/** Tests framework test generation using the deploy model as prerequisite state. */ +describe("TestGenerationModule", () => { + it("generates a Foundry Diamond test for every selected facet", async () => { + const harness = await createDeployGenerationHarness(); + const testRoot = path.join(harness.projectRoot, "test"); + + try { + await DeployGenerationModule.generateDeployScript(harness.ctx, harness.scriptRoot); + const result = await TestGenerationModule.generateTestFile(harness.ctx, testRoot); + const state = result.state.generateTestFile as ModuleState; + const source = await fs.readFile(path.join(testRoot, "Diamond.t.sol"), "utf8"); + + expect(state.success).toBe(true); + expect(state.result).toEqual(expect.objectContaining({ + outputPath: path.join(testRoot, "Diamond.t.sol"), + facetCount: 16, + framework: "foundry", + })); + expect(source).toContain("contract DiamondTest is Test"); + expect(source).toContain("new ERC20DataFacet()"); + expect(source).toContain("new DiamondUpgradeFacet()"); + } finally { + await harness.cleanup(); + } + }); +}); diff --git a/cli/test/pipelines/initPipeline/harness.ts b/cli/test/pipelines/initPipeline/harness.ts new file mode 100644 index 00000000..d117fdfb --- /dev/null +++ b/cli/test/pipelines/initPipeline/harness.ts @@ -0,0 +1,129 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { vi } from "vitest"; +import { + ConfigOptions, + IFrameworkAdapter, + SolidityAstSource, +} from "../../../src/adapters/interface/IFrameworkAdapter"; +import { Context } from "../../../src/context/context"; +import { ComposeContext } from "../../../src/context/types"; +import { + contractNameFromSourcePath, + resolveCatalogSourceForRead, +} from "../../../src/utils/soliditySources"; + +export type InitFramework = "foundry" | "hardhat"; + +export type InitPipelineHarness = { + adapter: IFrameworkAdapter; + ctx: ComposeContext; + outputRoot: string; + projectRoot: string; + cleanup(): Promise; +}; + +function contractAst(sourceName: string, contractName: string, id: number): SolidityAstSource { + return { + sourceName, + ast: { + id, + nodeType: "SourceUnit", + src: "0:0:0", + absolutePath: sourceName, + nodes: [{ + id: id + 1, + nodeType: "ContractDefinition", + name: contractName, + contractKind: "contract", + linearizedBaseContracts: [id + 1], + nodes: [], + src: "0:0:0", + }], + }, + }; +} + +/** Implements IFrameworkAdapter without invoking network or external toolchains. */ +function createFixtureAdapter(framework: InitFramework): IFrameworkAdapter { + const sourceDirectory = framework === "foundry" ? "src" : "contracts"; + const scriptDirectory = framework === "foundry" ? "script" : "scripts"; + const artifactDirectory = framework === "foundry" ? "out" : "artifacts"; + + return { + getContractSourceRoot: (projectRoot) => path.join(projectRoot, sourceDirectory), + getScriptRoot: (projectRoot) => path.join(projectRoot, scriptDirectory), + getTestRoot: (projectRoot) => path.join(projectRoot, "test"), + getArtifactDir: (projectRoot) => path.join(projectRoot, artifactDirectory), + compile: vi.fn(async () => undefined), + compileAst: vi.fn(async (_ctx, sourcePaths) => sourcePaths.map((sourcePath, index) => + contractAst(sourcePath, contractNameFromSourcePath(sourcePath), index * 10 + 1) + )), + resolveSoliditySourcePath: vi.fn(async (_ctx, sourcePath) => + resolveCatalogSourceForRead(sourcePath)), + initProject: vi.fn(async (ctx) => { + const projectRoot = String(ctx.param.projectRoot); + await Promise.all([ + fs.mkdir(path.join(projectRoot, sourceDirectory), { recursive: true }), + fs.mkdir(path.join(projectRoot, scriptDirectory), { recursive: true }), + fs.mkdir(path.join(projectRoot, "test"), { recursive: true }), + ]); + }), + writeConfig: vi.fn(async (ctx, options: ConfigOptions) => { + const projectRoot = String(ctx.param.projectRoot); + if (framework === "foundry") { + await fs.writeFile( + path.join(projectRoot, "foundry.toml"), + `[profile.default]\nsolc = "${options.compilerVersion}"\noptimizer = true\n`, + "utf8", + ); + return; + } + + await Promise.all([ + fs.writeFile( + path.join(projectRoot, "package.json"), + `${JSON.stringify({ name: options.projectName, private: true, type: "module" }, null, 2)}\n`, + "utf8", + ), + fs.writeFile( + path.join(projectRoot, "hardhat.config.ts"), + `export default { solidity: "${options.compilerVersion}" };\n`, + "utf8", + ), + ]); + }), + }; +} + +/** Creates a temporary project context for an InitPipeline integration test. */ +export async function createInitPipelineHarness( + framework: InitFramework, + base: string, + projectName: string, +): Promise { + const outputRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-init-pipeline-")); + const projectRoot = path.join(outputRoot, projectName); + const ctx = Context.create(); + const adapter = createFixtureAdapter(framework); + + Object.assign(ctx.param, { + command: "init", + yes: true, + framework, + toolbox: "ethers", + base, + projectName, + outDir: outputRoot, + installDeps: false, + }); + + return { + adapter, + ctx, + outputRoot, + projectRoot, + cleanup: () => fs.rm(outputRoot, { recursive: true, force: true }), + }; +} diff --git a/cli/test/pipelines/initPipeline/initPipeline.test.ts b/cli/test/pipelines/initPipeline/initPipeline.test.ts index 495fdb40..a37246db 100644 --- a/cli/test/pipelines/initPipeline/initPipeline.test.ts +++ b/cli/test/pipelines/initPipeline/initPipeline.test.ts @@ -1,132 +1,135 @@ -import { describe, expect, it, vi } from "vitest"; -import { - IFrameworkAdapter, - SolidityAstSource, -} from "../../../src/adapters/interface/IFrameworkAdapter"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { HashingAdapter } from "../../../src/adapters/hashingAdapter"; -import { Context } from "../../../src/context/context"; -import { ConfigModule } from "../../../src/modules/config/module"; -import { DeployGenerationModule } from "../../../src/modules/deployGeneration/module"; -import { DiamondGenerationModule } from "../../../src/modules/diamondGeneration/module"; import { InitModule } from "../../../src/modules/init/module"; import { PreflightModule } from "../../../src/modules/preflight/module"; -import { ProjectDirModule } from "../../../src/modules/projectDir/module"; -import { ScaffoldingModule } from "../../../src/modules/scaffolding/module"; -import { TestGenerationModule } from "../../../src/modules/testGeneration/module"; import { ValidationModule } from "../../../src/modules/validation/module"; import { InitPipeline } from "../../../src/pipelines/initPipeline"; import { DependencyResolver } from "../../../src/resolver/dependencyResolver"; +import { createInitPipelineHarness, InitPipelineHarness } from "./harness"; -function contractAst(sourceName: string, contractName: string, id: number): SolidityAstSource { - return { - sourceName, - ast: { - id, - nodeType: "SourceUnit", - src: "0:0:0", - absolutePath: sourceName, - nodes: [ - { - id: id + 1, - nodeType: "ContractDefinition", - name: contractName, - contractKind: "contract", - linearizedBaseContracts: [id + 1], - nodes: [], - src: "0:0:0", - }, - ], - }, - }; +type ComposeJson = { + project: string; + framework: string; + diamonds: Record; + }>; +}; + +async function expectFiles(projectRoot: string, files: string[]): Promise { + await Promise.all(files.map((file) => + expect(fs.access(path.join(projectRoot, file))).resolves.toBeUndefined() + )); } -describe("InitPipeline source validation", () => { - it("merges Compose package and project facet paths before compiling AST", async () => { - const ctx = Context.create(); - const packagePath = "@perfect-abstractions/compose/diamond/PackageFacet.sol"; - const resolvedPackagePath = "/tmp/compose-project/lib/Compose/src/diamond/PackageFacet.sol"; - const projectPath = "/tmp/compose-project/src/facets/ProjectFacet.sol"; - Object.assign(ctx.param, { - yes: true, - framework: "foundry", - projectRoot: "/tmp/compose-project", - projectName: "example", - installDeps: false, - base: "counter", - libraries: [], - extensions: [], - access: [], - accessExtensions: [], - }); +async function readComposeJson(projectRoot: string): Promise { + return JSON.parse( + await fs.readFile(path.join(projectRoot, "compose.json"), "utf8"), + ) as ComposeJson; +} - const compileAst = vi.fn(async () => [ - contractAst(resolvedPackagePath, "PackageFacet", 1), - contractAst(projectPath, "ProjectFacet", 10), - ]); - const adapter = { - getContractSourceRoot: vi.fn(() => "/tmp/compose-project/src"), - getScriptRoot: vi.fn(() => "/tmp/compose-project/script"), - getTestRoot: vi.fn(() => "/tmp/compose-project/test"), - resolveSoliditySourcePath: vi.fn(async (_ctx, sourcePath: string) => - sourcePath === packagePath ? resolvedPackagePath : sourcePath), - compileAst, - initProject: vi.fn(async () => undefined), - writeConfig: vi.fn(async () => undefined), - } as unknown as IFrameworkAdapter; +function useHarnessDependencies(harness: InitPipelineHarness): void { + vi.spyOn(InitModule, "showComposeHeader").mockImplementation(() => undefined); + vi.spyOn(InitModule, "showSuccess").mockImplementation(() => undefined); + vi.spyOn(PreflightModule, "check").mockImplementation(async (ctx) => ctx); + vi.spyOn(ValidationModule, "showReport").mockImplementation(async (ctx) => ctx); + vi.spyOn(DependencyResolver, "resolve").mockResolvedValue({ + [String(harness.ctx.param.framework)]: harness.adapter, + hashing: HashingAdapter, + }); +} - vi.spyOn(InitModule, "showComposeHeader").mockImplementation(() => undefined); - vi.spyOn(InitModule, "showSuccess").mockImplementation(() => undefined); - vi.spyOn(ConfigModule, "loadBasesCatalog").mockImplementation(async (parentCtx) => { - parentCtx.config.bases = {}; - return parentCtx; - }); - vi.spyOn(ConfigModule, "getDiamondCompilerVersion").mockReturnValue("0.8.30"); - vi.spyOn(InitModule, "runInitNonInteractive").mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(PreflightModule, "check").mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(ProjectDirModule, "resolve").mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(ProjectDirModule, "validate").mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(ScaffoldingModule, "copyFacets").mockResolvedValue([ - { - facetName: "package", - contractName: "PackageFacet", - targetPath: packagePath, - origin: "package", - }, - { - facetName: "project", - contractName: "ProjectFacet", - targetPath: projectPath, - origin: "local", - }, - ]); - vi.spyOn(ValidationModule, "showReport").mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(DiamondGenerationModule, "generateDiamondContract") - .mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(DeployGenerationModule, "generateDeployScript") - .mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(TestGenerationModule, "generateTestFile") - .mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(ScaffoldingModule, "buildComposeJson").mockImplementation((parentCtx) => parentCtx); - vi.spyOn(ScaffoldingModule, "validateLocalFacetFiles") - .mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(ScaffoldingModule, "writeComposeConfig") - .mockImplementation(async (parentCtx) => parentCtx); - vi.spyOn(DependencyResolver, "resolve").mockResolvedValue({ - foundry: adapter, - hashing: HashingAdapter, - }); +afterEach(() => { + vi.restoreAllMocks(); +}); + +/** + * Exercises the complete InitPipeline with real modules and filesystem output. + * + * Framework process and compiler behavior stay behind an IFrameworkAdapter + * fixture; their real implementations are covered by the adapter test suite. + */ +describe("InitPipeline", () => { + it.each([ + { + framework: "foundry" as const, + projectName: "bare-foundry", + contract: "src/Diamond.sol:Diamond", + files: [ + "compose.json", + "foundry.toml", + "src/Diamond.sol", + "script/Deploy.s.sol", + "test/Diamond.t.sol", + ], + }, + { + framework: "hardhat" as const, + projectName: "bare-hardhat", + contract: "contracts/Diamond.sol:Diamond", + files: [ + "compose.json", + "package.json", + "hardhat.config.ts", + "contracts/Diamond.sol", + "scripts/deploy.ts", + "test/Diamond.ts", + ], + }, + ])("scaffolds a bare $framework diamond", async ({ + framework, + projectName, + contract, + files, + }) => { + const harness = await createInitPipelineHarness(framework, "none", projectName); + useHarnessDependencies(harness); try { - const result = await InitPipeline.execute(ctx); + const result = await InitPipeline.execute(harness.ctx); + const composeJson = await readComposeJson(harness.projectRoot); + const diamond = composeJson.diamonds[projectName]; - expect(result).toBe(ctx); - expect(compileAst).toHaveBeenCalledWith(ctx, [resolvedPackagePath, projectPath]); - expect(result.state.validationComposeFacetSources?.success).toBe(true); - expect(result.state.validationProjectFacetSources?.success).toBe(true); + expect(result.status.success).toBe(true); expect(result.state.initValidation?.success).toBe(true); + expect(result.state.initPipeline?.success).toBe(true); + expect(composeJson.project).toBe(projectName); + expect(composeJson.framework).toBe(framework); + expect(diamond.contract).toBe(contract); + expect(Object.keys(diamond.facets)).toEqual(["DiamondInspectFacet"]); + await expectFiles(harness.projectRoot, files); + expect(harness.adapter.compileAst).toHaveBeenCalledOnce(); + } finally { + await harness.cleanup(); + } + }); + + it("scaffolds an ERC-20 diamond from non-interactive flags", async () => { + const harness = await createInitPipelineHarness("foundry", "erc-20", "erc20-foundry"); + useHarnessDependencies(harness); + + try { + const result = await InitPipeline.execute(harness.ctx); + const composeJson = await readComposeJson(harness.projectRoot); + const facetNames = Object.keys(composeJson.diamonds["erc20-foundry"].facets).sort(); + expect(result.status.success).toBe(true); + expect(result.state.initValidation?.success).toBe(true); + expect(facetNames).toEqual([ + "DiamondInspectFacet", + "ERC20ApproveFacet", + "ERC20DataFacet", + "ERC20TransferFacet", + ]); + expect(harness.adapter.compileAst).toHaveBeenCalledOnce(); } finally { - vi.restoreAllMocks(); + await harness.cleanup(); } }); }); From f558f422b4eda0fac4dfb78b0528e044b71fe857 Mon Sep 17 00:00:00 2001 From: Vagabond Date: Fri, 18 Sep 2026 01:42:20 +0700 Subject: [PATCH 2/5] fix(ci): run cli tests with foundry --- .github/workflows/cli.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 5d839c25..252f706c 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -34,9 +34,6 @@ jobs: - name: Build TypeScript run: npm run cli@build - - name: Run tests - run: npm run cli@test - - name: Lint run: npm run cli@lint @@ -71,6 +68,9 @@ jobs: - name: Build TypeScript run: npm run cli@build + - name: Run tests + run: npm run cli@test + - name: Smoke test Foundry counter base run: | set -euo pipefail From d0b6334ce554eb8c5a55fd98e7d0eaf98f7b72d0 Mon Sep 17 00:00:00 2001 From: Vagabond Date: Fri, 18 Sep 2026 01:46:05 +0700 Subject: [PATCH 3/5] test: pin foundry fixture evm version --- .../IFrameworkAdapter/foundryAdapter/project/foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/test/adapters/IFrameworkAdapter/foundryAdapter/project/foundry.toml b/cli/test/adapters/IFrameworkAdapter/foundryAdapter/project/foundry.toml index 53d42b50..72d305e7 100644 --- a/cli/test/adapters/IFrameworkAdapter/foundryAdapter/project/foundry.toml +++ b/cli/test/adapters/IFrameworkAdapter/foundryAdapter/project/foundry.toml @@ -2,3 +2,4 @@ src = "src" out = "out" solc = "0.8.30" +evm_version = "prague" From 6c712c2111967334150e4b04987ab5fb8e396a34 Mon Sep 17 00:00:00 2001 From: Vagabond Date: Sat, 19 Sep 2026 01:28:31 +0700 Subject: [PATCH 4/5] refactor(cli): organize adapters by interface --- .../foundryAdapter/adapter.ts} | 44 ++++--------------- .../foundryAdapter/helpers.ts | 29 ++++++++++++ .../hardhatAdapter/adapter.ts} | 40 ++++------------- .../hardhatAdapter/helpers.ts | 24 ++++++++++ .../interface.ts} | 0 .../adapter.ts} | 2 +- .../interface.ts} | 2 +- .../adapters/{rpc => IRPCAdapter}/adapter.ts | 2 +- .../adapters/{rpc => IRPCAdapter}/errors.ts | 0 .../interface.ts} | 0 .../adapters/{rpc => IRPCAdapter}/retry.ts | 0 .../adapters/{rpc => IRPCAdapter}/types.ts | 0 .../adapters/{rpc => IRPCAdapter}/utils.ts | 0 .../{rpc => IRPCAdapter}/validation.ts | 0 cli/src/modules/compile/module.ts | 2 +- cli/src/modules/inspect/module.ts | 2 +- cli/src/modules/lockFile/module.ts | 2 +- cli/src/modules/scaffolding/module.ts | 2 +- cli/src/modules/validation/astSelectors.ts | 2 +- cli/src/modules/validation/module.ts | 4 +- .../modules/validation/sourceResolution.ts | 2 +- cli/src/modules/validation/types.ts | 2 +- cli/src/modules/validation/validators.ts | 2 +- .../validation/virtualStorageLayout.ts | 2 +- cli/src/pipelines/buildPipeline.ts | 2 +- cli/src/pipelines/initPipeline.ts | 2 +- cli/src/pipelines/validatePipeline.ts | 2 +- cli/src/resolver/dependencyRegistry.ts | 14 +++--- cli/src/utils/chainConfig.ts | 2 +- cli/src/utils/solidityAst.ts | 2 +- .../IFrameworkAdapter/canonicalAst.ts | 2 +- .../foundryAdapter/foundryAdapter.test.ts | 2 +- .../hardhatAdapter/hardhatAdapter.test.ts | 6 +-- .../adapter.test.ts} | 2 +- .../adapter.test.ts} | 2 +- .../modules/lockFile/computeFacetHash.test.ts | 2 +- cli/test/modules/lockFile/module.test.ts | 2 +- .../modules/validation/astSelectors.test.ts | 4 +- .../modules/validation/selectorScopes.test.ts | 2 +- .../validation/virtualStorageLayout.test.ts | 2 +- cli/test/pipelines/initPipeline/harness.ts | 2 +- .../initPipeline/initPipeline.test.ts | 2 +- cli/test/utils/chainConfig.test.ts | 2 +- website/docs/contribution/cli/testing.mdx | 9 +++- 44 files changed, 119 insertions(+), 111 deletions(-) rename cli/src/adapters/{foundryAdapter.ts => IFrameworkAdapter/foundryAdapter/adapter.ts} (78%) create mode 100644 cli/src/adapters/IFrameworkAdapter/foundryAdapter/helpers.ts rename cli/src/adapters/{hardhatAdapter.ts => IFrameworkAdapter/hardhatAdapter/adapter.ts} (84%) create mode 100644 cli/src/adapters/IFrameworkAdapter/hardhatAdapter/helpers.ts rename cli/src/adapters/{interface/IFrameworkAdapter.ts => IFrameworkAdapter/interface.ts} (100%) rename cli/src/adapters/{hashingAdapter.ts => IHashingAdapter/adapter.ts} (82%) rename cli/src/adapters/{interface/IHashingAdapter.ts => IHashingAdapter/interface.ts} (99%) rename cli/src/adapters/{rpc => IRPCAdapter}/adapter.ts (97%) rename cli/src/adapters/{rpc => IRPCAdapter}/errors.ts (100%) rename cli/src/adapters/{interface/IRPCAdapter.ts => IRPCAdapter/interface.ts} (100%) rename cli/src/adapters/{rpc => IRPCAdapter}/retry.ts (100%) rename cli/src/adapters/{rpc => IRPCAdapter}/types.ts (100%) rename cli/src/adapters/{rpc => IRPCAdapter}/utils.ts (100%) rename cli/src/adapters/{rpc => IRPCAdapter}/validation.ts (100%) rename cli/test/adapters/{hashingAdapter.test.ts => IHashingAdapter/adapter.test.ts} (85%) rename cli/test/adapters/{rpcAdapter.test.ts => IRPCAdapter/adapter.test.ts} (98%) diff --git a/cli/src/adapters/foundryAdapter.ts b/cli/src/adapters/IFrameworkAdapter/foundryAdapter/adapter.ts similarity index 78% rename from cli/src/adapters/foundryAdapter.ts rename to cli/src/adapters/IFrameworkAdapter/foundryAdapter/adapter.ts index ab9b3e58..e28a2c9d 100644 --- a/cli/src/adapters/foundryAdapter.ts +++ b/cli/src/adapters/IFrameworkAdapter/foundryAdapter/adapter.ts @@ -1,48 +1,20 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { ComposeContext } from "../context/types"; +import { ComposeContext } from "../../../context/types"; import { ConfigOptions, IFrameworkAdapter, SolidityAstSource, -} from "./interface/IFrameworkAdapter"; -import { writeFileIfMissing } from "../utils/files"; -import { runCommand } from "../utils/exec"; +} from "../interface"; +import { writeFileIfMissing } from "../../../utils/files"; +import { runCommand } from "../../../utils/exec"; import { composePackageSubpath, isComposePackagePath, -} from "../utils/soliditySources"; -import { CLI_ROOT } from "../utils/cliRoot"; -import { isSourceUnitAst, listJsonFiles, uniqueAstSources } from "../utils/solidityAst"; - -function ensureTomlSectionSettings( - content: string, - section: string, - settings: Record, -): string { - const lines = content.replace(/\r\n/g, "\n").split("\n"); - if (lines.at(-1) === "") lines.pop(); - - let sectionIndex = lines.findIndex((line) => line.trim() === `[${section}]`); - if (sectionIndex === -1) { - if (lines.length > 0 && lines.at(-1)?.trim() !== "") lines.push(""); - sectionIndex = lines.length; - lines.push(`[${section}]`); - } - - let sectionEnd = lines.findIndex( - (line, index) => index > sectionIndex && /^\s*\[[^\]]+\]\s*$/.test(line), - ); - if (sectionEnd === -1) sectionEnd = lines.length; - - const sectionLines = lines.slice(sectionIndex + 1, sectionEnd); - const missingSettings = Object.entries(settings) - .filter(([key]) => !sectionLines.some((line) => new RegExp(`^\\s*${key}\\s*=`).test(line))) - .map(([key, value]) => `${key} = ${value}`); - - lines.splice(sectionEnd, 0, ...missingSettings); - return `${lines.join("\n")}\n`; -} +} from "../../../utils/soliditySources"; +import { CLI_ROOT } from "../../../utils/cliRoot"; +import { isSourceUnitAst, listJsonFiles, uniqueAstSources } from "../../../utils/solidityAst"; +import { ensureTomlSectionSettings } from "./helpers"; /** Framework adapter for Foundry-based Diamond projects. */ const adapter: IFrameworkAdapter = { diff --git a/cli/src/adapters/IFrameworkAdapter/foundryAdapter/helpers.ts b/cli/src/adapters/IFrameworkAdapter/foundryAdapter/helpers.ts new file mode 100644 index 00000000..658dcb9f --- /dev/null +++ b/cli/src/adapters/IFrameworkAdapter/foundryAdapter/helpers.ts @@ -0,0 +1,29 @@ +/** Adds missing settings to a TOML section without overwriting existing values. */ +export function ensureTomlSectionSettings( + content: string, + section: string, + settings: Record, +): string { + const lines = content.replace(/\r\n/g, "\n").split("\n"); + if (lines.at(-1) === "") lines.pop(); + + let sectionIndex = lines.findIndex((line) => line.trim() === `[${section}]`); + if (sectionIndex === -1) { + if (lines.length > 0 && lines.at(-1)?.trim() !== "") lines.push(""); + sectionIndex = lines.length; + lines.push(`[${section}]`); + } + + let sectionEnd = lines.findIndex( + (line, index) => index > sectionIndex && /^\s*\[[^\]]+\]\s*$/.test(line), + ); + if (sectionEnd === -1) sectionEnd = lines.length; + + const sectionLines = lines.slice(sectionIndex + 1, sectionEnd); + const missingSettings = Object.entries(settings) + .filter(([key]) => !sectionLines.some((line) => new RegExp(`^\\s*${key}\\s*=`).test(line))) + .map(([key, value]) => `${key} = ${value}`); + + lines.splice(sectionEnd, 0, ...missingSettings); + return `${lines.join("\n")}\n`; +} diff --git a/cli/src/adapters/hardhatAdapter.ts b/cli/src/adapters/IFrameworkAdapter/hardhatAdapter/adapter.ts similarity index 84% rename from cli/src/adapters/hardhatAdapter.ts rename to cli/src/adapters/IFrameworkAdapter/hardhatAdapter/adapter.ts index c5b22a2a..312bcdf0 100644 --- a/cli/src/adapters/hardhatAdapter.ts +++ b/cli/src/adapters/IFrameworkAdapter/hardhatAdapter/adapter.ts @@ -1,43 +1,21 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { ComposeContext } from "../context/types"; +import { ComposeContext } from "../../../context/types"; import { ConfigOptions, IFrameworkAdapter, SolidityAstSource, -} from "./interface/IFrameworkAdapter"; -import { writeFileIfMissing } from "../utils/files"; -import { runCommand } from "../utils/exec"; +} from "../interface"; +import { writeFileIfMissing } from "../../../utils/files"; +import { runCommand } from "../../../utils/exec"; import { composePackageSubpath, isComposePackagePath, -} from "../utils/soliditySources"; -import { ScaffoldingModule } from "../modules/scaffolding/module"; -import { CLI_ROOT } from "../utils/cliRoot"; -import { isSourceUnitAst, listJsonFiles, uniqueAstSources } from "../utils/solidityAst"; - -/** Converts a Hardhat compiler source name into its readable filesystem path. */ -export function resolveHardhatAstSourcePath(projectRoot: string, sourceName: string): string { - const segments = sourceName.replace(/\\/g, "/").split("/"); - - if (segments[0] === "project") { - return path.resolve(projectRoot, ...segments.slice(1)); - } - - if (segments[0] === "npm") { - const packageNameIndex = segments[1]?.startsWith("@") ? 2 : 1; - const versionedPackageName = segments[packageNameIndex] ?? ""; - const versionSeparator = versionedPackageName.lastIndexOf("@"); - if (versionSeparator > 0) { - segments[packageNameIndex] = versionedPackageName.slice(0, versionSeparator); - } - return path.resolve(projectRoot, "node_modules", ...segments.slice(1)); - } - - return path.isAbsolute(sourceName) - ? path.normalize(sourceName) - : path.resolve(projectRoot, sourceName); -} +} from "../../../utils/soliditySources"; +import { ScaffoldingModule } from "../../../modules/scaffolding/module"; +import { CLI_ROOT } from "../../../utils/cliRoot"; +import { isSourceUnitAst, listJsonFiles, uniqueAstSources } from "../../../utils/solidityAst"; +import { resolveHardhatAstSourcePath } from "./helpers"; /** Framework adapter for Hardhat-based Diamond projects. */ const adapter: IFrameworkAdapter = { diff --git a/cli/src/adapters/IFrameworkAdapter/hardhatAdapter/helpers.ts b/cli/src/adapters/IFrameworkAdapter/hardhatAdapter/helpers.ts new file mode 100644 index 00000000..3d42ab4b --- /dev/null +++ b/cli/src/adapters/IFrameworkAdapter/hardhatAdapter/helpers.ts @@ -0,0 +1,24 @@ +import path from "node:path"; + +/** Converts a Hardhat compiler source name into its readable filesystem path. */ +export function resolveHardhatAstSourcePath(projectRoot: string, sourceName: string): string { + const segments = sourceName.replace(/\\/g, "/").split("/"); + + if (segments[0] === "project") { + return path.resolve(projectRoot, ...segments.slice(1)); + } + + if (segments[0] === "npm") { + const packageNameIndex = segments[1]?.startsWith("@") ? 2 : 1; + const versionedPackageName = segments[packageNameIndex] ?? ""; + const versionSeparator = versionedPackageName.lastIndexOf("@"); + if (versionSeparator > 0) { + segments[packageNameIndex] = versionedPackageName.slice(0, versionSeparator); + } + return path.resolve(projectRoot, "node_modules", ...segments.slice(1)); + } + + return path.isAbsolute(sourceName) + ? path.normalize(sourceName) + : path.resolve(projectRoot, sourceName); +} diff --git a/cli/src/adapters/interface/IFrameworkAdapter.ts b/cli/src/adapters/IFrameworkAdapter/interface.ts similarity index 100% rename from cli/src/adapters/interface/IFrameworkAdapter.ts rename to cli/src/adapters/IFrameworkAdapter/interface.ts diff --git a/cli/src/adapters/hashingAdapter.ts b/cli/src/adapters/IHashingAdapter/adapter.ts similarity index 82% rename from cli/src/adapters/hashingAdapter.ts rename to cli/src/adapters/IHashingAdapter/adapter.ts index 3ca2626a..b401c1bb 100644 --- a/cli/src/adapters/hashingAdapter.ts +++ b/cli/src/adapters/IHashingAdapter/adapter.ts @@ -1,5 +1,5 @@ import { keccak256, stringToBytes, type Hex } from "viem"; -import { IHashingAdapter } from "./interface/IHashingAdapter"; +import { IHashingAdapter } from "./interface"; /** Adapter providing keccak256 hashing over UTF-8 strings, returning a hex digest. */ export const HashingAdapter: IHashingAdapter = { diff --git a/cli/src/adapters/interface/IHashingAdapter.ts b/cli/src/adapters/IHashingAdapter/interface.ts similarity index 99% rename from cli/src/adapters/interface/IHashingAdapter.ts rename to cli/src/adapters/IHashingAdapter/interface.ts index c3d997a7..5f32bc74 100644 --- a/cli/src/adapters/interface/IHashingAdapter.ts +++ b/cli/src/adapters/IHashingAdapter/interface.ts @@ -9,4 +9,4 @@ export interface IHashingAdapter { * @returns The keccak256 hex digest */ keccak256(value: string): Hex; -} \ No newline at end of file +} diff --git a/cli/src/adapters/rpc/adapter.ts b/cli/src/adapters/IRPCAdapter/adapter.ts similarity index 97% rename from cli/src/adapters/rpc/adapter.ts rename to cli/src/adapters/IRPCAdapter/adapter.ts index 58873a40..519c6404 100644 --- a/cli/src/adapters/rpc/adapter.ts +++ b/cli/src/adapters/IRPCAdapter/adapter.ts @@ -8,7 +8,7 @@ import { type Hex, type ReadContractParameters, } from "viem"; -import type { IRPCAdapter, RPCReadContractOptions } from "../interface/IRPCAdapter"; +import type { IRPCAdapter, RPCReadContractOptions } from "./interface"; import { requestError, RPCAdapterError } from "./errors"; import { retryRPC } from "./retry"; import type { RPCAdapterOptions } from "./types"; diff --git a/cli/src/adapters/rpc/errors.ts b/cli/src/adapters/IRPCAdapter/errors.ts similarity index 100% rename from cli/src/adapters/rpc/errors.ts rename to cli/src/adapters/IRPCAdapter/errors.ts diff --git a/cli/src/adapters/interface/IRPCAdapter.ts b/cli/src/adapters/IRPCAdapter/interface.ts similarity index 100% rename from cli/src/adapters/interface/IRPCAdapter.ts rename to cli/src/adapters/IRPCAdapter/interface.ts diff --git a/cli/src/adapters/rpc/retry.ts b/cli/src/adapters/IRPCAdapter/retry.ts similarity index 100% rename from cli/src/adapters/rpc/retry.ts rename to cli/src/adapters/IRPCAdapter/retry.ts diff --git a/cli/src/adapters/rpc/types.ts b/cli/src/adapters/IRPCAdapter/types.ts similarity index 100% rename from cli/src/adapters/rpc/types.ts rename to cli/src/adapters/IRPCAdapter/types.ts diff --git a/cli/src/adapters/rpc/utils.ts b/cli/src/adapters/IRPCAdapter/utils.ts similarity index 100% rename from cli/src/adapters/rpc/utils.ts rename to cli/src/adapters/IRPCAdapter/utils.ts diff --git a/cli/src/adapters/rpc/validation.ts b/cli/src/adapters/IRPCAdapter/validation.ts similarity index 100% rename from cli/src/adapters/rpc/validation.ts rename to cli/src/adapters/IRPCAdapter/validation.ts diff --git a/cli/src/modules/compile/module.ts b/cli/src/modules/compile/module.ts index 6f6d7017..0a874762 100644 --- a/cli/src/modules/compile/module.ts +++ b/cli/src/modules/compile/module.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import { IFrameworkAdapter } from "../../adapters/interface/IFrameworkAdapter"; +import { IFrameworkAdapter } from "../../adapters/IFrameworkAdapter/interface"; import { getNewestSourceMtime, getOldestArtifactMtime } from "./utils"; export const CompileModule = { diff --git a/cli/src/modules/inspect/module.ts b/cli/src/modules/inspect/module.ts index 6798c9cb..3168862a 100644 --- a/cli/src/modules/inspect/module.ts +++ b/cli/src/modules/inspect/module.ts @@ -5,7 +5,7 @@ import { DependencyKey } from "../../resolver/dependencyKey"; import { DependencyResolver } from "../../resolver/dependencyResolver"; import { resolveChainConfig } from "../../utils/chainConfig"; import { findFileAncestor } from "../../utils/files"; -import { RPCAdapterError } from "../../adapters/rpc/errors"; +import { RPCAdapterError } from "../../adapters/IRPCAdapter/errors"; import { showInspect } from "./output"; import { DIAMOND_LOUPE_ABI } from "./diamondLoupeAbi"; import { toFacetInfo } from "./facetFormatter"; diff --git a/cli/src/modules/lockFile/module.ts b/cli/src/modules/lockFile/module.ts index 135e1dd0..4588601e 100644 --- a/cli/src/modules/lockFile/module.ts +++ b/cli/src/modules/lockFile/module.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { type Hex } from "viem"; import { ComposeContext } from "../../context/types"; -import { IHashingAdapter } from "../../adapters/interface/IHashingAdapter"; +import { IHashingAdapter } from "../../adapters/IHashingAdapter/interface"; import { atomicWriteFile, findFileAncestor } from "../../utils/files"; import { ComposeLock, DiamondDeployment } from "./types"; import { validateLockFile } from "./validators"; diff --git a/cli/src/modules/scaffolding/module.ts b/cli/src/modules/scaffolding/module.ts index f76e0069..40e7c7b6 100644 --- a/cli/src/modules/scaffolding/module.ts +++ b/cli/src/modules/scaffolding/module.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { IFrameworkAdapter } from "../../adapters/interface/IFrameworkAdapter"; +import { IFrameworkAdapter } from "../../adapters/IFrameworkAdapter/interface"; import { ComposeContext, ModuleState } from "../../context/types"; import { VERSION } from "../../utils/metadata"; import { copyFileIfMissing, parsePackageName, resolveLocalSolidityImportClosure, toPosixPath } from "../../utils/files"; diff --git a/cli/src/modules/validation/astSelectors.ts b/cli/src/modules/validation/astSelectors.ts index 3ed3b0ab..18a29be8 100644 --- a/cli/src/modules/validation/astSelectors.ts +++ b/cli/src/modules/validation/astSelectors.ts @@ -1,4 +1,4 @@ -import { SolidityAstSource } from "../../adapters/interface/IFrameworkAdapter"; +import { SolidityAstSource } from "../../adapters/IFrameworkAdapter/interface"; import { matchesAstSource } from "./astIdentity"; import { FacetReference, FacetScanResult, FunctionInfo } from "./types"; diff --git a/cli/src/modules/validation/module.ts b/cli/src/modules/validation/module.ts index 847fbe2d..8566dc28 100644 --- a/cli/src/modules/validation/module.ts +++ b/cli/src/modules/validation/module.ts @@ -1,5 +1,5 @@ import { ComposeContext } from "../../context/types"; -import { SolidityAstSource } from "../../adapters/interface/IFrameworkAdapter"; +import { SolidityAstSource } from "../../adapters/IFrameworkAdapter/interface"; import { DiamondValidationScope, FacetReference, SelectorCollisionDeps } from "./types"; import { matchesAstSource } from "./astIdentity"; import { scanFacetSelectorsFromAst } from "./astSelectors"; @@ -22,7 +22,7 @@ import { } from "./state"; import { showReport, showSuccess } from "./output"; import { getResolvedFacetSources, resolveFacetSources } from "./sourceResolution"; -import { IFrameworkAdapter } from "../../adapters/interface/IFrameworkAdapter"; +import { IFrameworkAdapter } from "../../adapters/IFrameworkAdapter/interface"; /** * Validates facet scans for selector export guidance and collision-free layouts. diff --git a/cli/src/modules/validation/sourceResolution.ts b/cli/src/modules/validation/sourceResolution.ts index c7365ff3..62149271 100644 --- a/cli/src/modules/validation/sourceResolution.ts +++ b/cli/src/modules/validation/sourceResolution.ts @@ -1,4 +1,4 @@ -import { IFrameworkAdapter } from "../../adapters/interface/IFrameworkAdapter"; +import { IFrameworkAdapter } from "../../adapters/IFrameworkAdapter/interface"; import { ComposeContext, ModuleState } from "../../context/types"; import { ScaffoldMapEntry } from "../scaffolding/types"; import { ResolvedFacetSource, ResolvedFacetSourceResult } from "./types"; diff --git a/cli/src/modules/validation/types.ts b/cli/src/modules/validation/types.ts index 27cf295f..10c3c94c 100644 --- a/cli/src/modules/validation/types.ts +++ b/cli/src/modules/validation/types.ts @@ -1,4 +1,4 @@ -import { IHashingAdapter } from "../../adapters/interface/IHashingAdapter"; +import { IHashingAdapter } from "../../adapters/IHashingAdapter/interface"; export type FunctionInfo = { name: string; diff --git a/cli/src/modules/validation/validators.ts b/cli/src/modules/validation/validators.ts index d1e39e01..4f069f3f 100644 --- a/cli/src/modules/validation/validators.ts +++ b/cli/src/modules/validation/validators.ts @@ -1,4 +1,4 @@ -import { IHashingAdapter } from "../../adapters/interface/IHashingAdapter"; +import { IHashingAdapter } from "../../adapters/IHashingAdapter/interface"; import { isArrayPrefix } from "../../utils/arrays"; import { FacetScanResult, diff --git a/cli/src/modules/validation/virtualStorageLayout.ts b/cli/src/modules/validation/virtualStorageLayout.ts index d3e20bf2..d0da09e8 100644 --- a/cli/src/modules/validation/virtualStorageLayout.ts +++ b/cli/src/modules/validation/virtualStorageLayout.ts @@ -1,5 +1,5 @@ import { keccak256, stringToBytes, toHex } from "viem"; -import { SolidityAstSource } from "../../adapters/interface/IFrameworkAdapter"; +import { SolidityAstSource } from "../../adapters/IFrameworkAdapter/interface"; import { matchesAstSource } from "./astIdentity"; import { DiamondValidationScope, diff --git a/cli/src/pipelines/buildPipeline.ts b/cli/src/pipelines/buildPipeline.ts index dd55a9de..95648471 100644 --- a/cli/src/pipelines/buildPipeline.ts +++ b/cli/src/pipelines/buildPipeline.ts @@ -3,7 +3,7 @@ import { CompileModule } from "../modules/compile/module"; import { FrameworkModule } from "../modules/framework/module"; import { DependencyKey } from "../resolver/dependencyKey"; import { DependencyResolver } from "../resolver/dependencyResolver"; -import { IFrameworkAdapter } from "../adapters/interface/IFrameworkAdapter"; +import { IFrameworkAdapter } from "../adapters/IFrameworkAdapter/interface"; export const BuildPipeline = { async execute(ctx: ComposeContext): Promise { diff --git a/cli/src/pipelines/initPipeline.ts b/cli/src/pipelines/initPipeline.ts index 7df547fc..f9e13202 100644 --- a/cli/src/pipelines/initPipeline.ts +++ b/cli/src/pipelines/initPipeline.ts @@ -10,7 +10,7 @@ import { DeployGenerationModule } from "../modules/deployGeneration/module"; import { TestGenerationModule } from "../modules/testGeneration/module"; import { DependencyKey } from "../resolver/dependencyKey"; import { DependencyResolver } from "../resolver/dependencyResolver"; -import { IFrameworkAdapter } from "../adapters/interface/IFrameworkAdapter"; +import { IFrameworkAdapter } from "../adapters/IFrameworkAdapter/interface"; import { BasesCatalog } from "../modules/config/types"; /** diff --git a/cli/src/pipelines/validatePipeline.ts b/cli/src/pipelines/validatePipeline.ts index 27f77c33..308214e9 100644 --- a/cli/src/pipelines/validatePipeline.ts +++ b/cli/src/pipelines/validatePipeline.ts @@ -1,4 +1,4 @@ -import { IFrameworkAdapter } from "../adapters/interface/IFrameworkAdapter"; +import { IFrameworkAdapter } from "../adapters/IFrameworkAdapter/interface"; import { ComposeContext } from "../context/types"; import { ValidationModule } from "../modules/validation/module"; import { DependencyKey } from "../resolver/dependencyKey"; diff --git a/cli/src/resolver/dependencyRegistry.ts b/cli/src/resolver/dependencyRegistry.ts index 7c4b9ecd..4962cfa5 100644 --- a/cli/src/resolver/dependencyRegistry.ts +++ b/cli/src/resolver/dependencyRegistry.ts @@ -1,12 +1,12 @@ import { HashingAdapter, -} from "../adapters/hashingAdapter"; -import { IHashingAdapter } from "../adapters/interface/IHashingAdapter"; -import { IFrameworkAdapter } from "../adapters/interface/IFrameworkAdapter"; -import { foundryAdapter } from "../adapters/foundryAdapter"; -import { hardhatAdapter } from "../adapters/hardhatAdapter"; -import type { IRPCAdapter } from "../adapters/interface/IRPCAdapter"; -import { createRPCAdapter } from "../adapters/rpc/adapter"; +} from "../adapters/IHashingAdapter/adapter"; +import { IHashingAdapter } from "../adapters/IHashingAdapter/interface"; +import { IFrameworkAdapter } from "../adapters/IFrameworkAdapter/interface"; +import { foundryAdapter } from "../adapters/IFrameworkAdapter/foundryAdapter/adapter"; +import { hardhatAdapter } from "../adapters/IFrameworkAdapter/hardhatAdapter/adapter"; +import type { IRPCAdapter } from "../adapters/IRPCAdapter/interface"; +import { createRPCAdapter } from "../adapters/IRPCAdapter/adapter"; import { resolveChainConfig } from "../utils/chainConfig"; import { DependencyKey } from "./dependencyKey"; diff --git a/cli/src/utils/chainConfig.ts b/cli/src/utils/chainConfig.ts index f360686c..cd7bbb9c 100644 --- a/cli/src/utils/chainConfig.ts +++ b/cli/src/utils/chainConfig.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import { findFileAncestor } from "./files"; -import { RPCAdapterError } from "../adapters/rpc/errors"; +import { RPCAdapterError } from "../adapters/IRPCAdapter/errors"; export type ResolvedChainConfig = { /** Name used to select the chain in compose.json. */ diff --git a/cli/src/utils/solidityAst.ts b/cli/src/utils/solidityAst.ts index 566ae8e9..c965e8ea 100644 --- a/cli/src/utils/solidityAst.ts +++ b/cli/src/utils/solidityAst.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { SolidityAstSource, SoliditySourceUnitAst, -} from "../adapters/interface/IFrameworkAdapter"; +} from "../adapters/IFrameworkAdapter/interface"; /** Recursively lists JSON files below an adapter output directory. */ export async function listJsonFiles(root: string): Promise { diff --git a/cli/test/adapters/IFrameworkAdapter/canonicalAst.ts b/cli/test/adapters/IFrameworkAdapter/canonicalAst.ts index 9537c56e..babafc26 100644 --- a/cli/test/adapters/IFrameworkAdapter/canonicalAst.ts +++ b/cli/test/adapters/IFrameworkAdapter/canonicalAst.ts @@ -1,4 +1,4 @@ -import { SoliditySourceUnitAst } from "../../../src/adapters/interface/IFrameworkAdapter"; +import { SoliditySourceUnitAst } from "../../../src/adapters/IFrameworkAdapter/interface"; /** Sorts AST object keys recursively so expected fixtures are stable and reviewable. */ export function canonicalizeAst(ast: SoliditySourceUnitAst): unknown { diff --git a/cli/test/adapters/IFrameworkAdapter/foundryAdapter/foundryAdapter.test.ts b/cli/test/adapters/IFrameworkAdapter/foundryAdapter/foundryAdapter.test.ts index c5ce7ac2..fba7fd2c 100644 --- a/cli/test/adapters/IFrameworkAdapter/foundryAdapter/foundryAdapter.test.ts +++ b/cli/test/adapters/IFrameworkAdapter/foundryAdapter/foundryAdapter.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { foundryAdapter } from "../../../../src/adapters/foundryAdapter"; +import { foundryAdapter } from "../../../../src/adapters/IFrameworkAdapter/foundryAdapter/adapter"; import { canonicalizeAst } from "../canonicalAst"; import { createFoundryAdapterFixtureHarness } from "./harness"; diff --git a/cli/test/adapters/IFrameworkAdapter/hardhatAdapter/hardhatAdapter.test.ts b/cli/test/adapters/IFrameworkAdapter/hardhatAdapter/hardhatAdapter.test.ts index 0df1aa58..35752f46 100644 --- a/cli/test/adapters/IFrameworkAdapter/hardhatAdapter/hardhatAdapter.test.ts +++ b/cli/test/adapters/IFrameworkAdapter/hardhatAdapter/hardhatAdapter.test.ts @@ -1,10 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; -import { - hardhatAdapter, - resolveHardhatAstSourcePath, -} from "../../../../src/adapters/hardhatAdapter"; +import { hardhatAdapter } from "../../../../src/adapters/IFrameworkAdapter/hardhatAdapter/adapter"; +import { resolveHardhatAstSourcePath } from "../../../../src/adapters/IFrameworkAdapter/hardhatAdapter/helpers"; import { canonicalizeAst } from "../canonicalAst"; import { createHardhatAdapterFixtureHarness, diff --git a/cli/test/adapters/hashingAdapter.test.ts b/cli/test/adapters/IHashingAdapter/adapter.test.ts similarity index 85% rename from cli/test/adapters/hashingAdapter.test.ts rename to cli/test/adapters/IHashingAdapter/adapter.test.ts index b524677c..64384f18 100644 --- a/cli/test/adapters/hashingAdapter.test.ts +++ b/cli/test/adapters/IHashingAdapter/adapter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { HashingAdapter } from "../../src/adapters/hashingAdapter"; +import { HashingAdapter } from "../../../src/adapters/IHashingAdapter/adapter"; /** * Tests HashingAdapter against the full Keccak-256 digest of a Solidity signature. diff --git a/cli/test/adapters/rpcAdapter.test.ts b/cli/test/adapters/IRPCAdapter/adapter.test.ts similarity index 98% rename from cli/test/adapters/rpcAdapter.test.ts rename to cli/test/adapters/IRPCAdapter/adapter.test.ts index 9921982c..b0fb823b 100644 --- a/cli/test/adapters/rpcAdapter.test.ts +++ b/cli/test/adapters/IRPCAdapter/adapter.test.ts @@ -30,7 +30,7 @@ vi.mock("viem", () => ({ withRetry: mocks.withRetry, })); -import { createRPCAdapter } from "../../src/adapters/rpc/adapter"; +import { createRPCAdapter } from "../../../src/adapters/IRPCAdapter/adapter"; type MockClient = { chain: { id: number }; diff --git a/cli/test/modules/lockFile/computeFacetHash.test.ts b/cli/test/modules/lockFile/computeFacetHash.test.ts index 6c559111..39c48937 100644 --- a/cli/test/modules/lockFile/computeFacetHash.test.ts +++ b/cli/test/modules/lockFile/computeFacetHash.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { HashingAdapter } from "../../../src/adapters/hashingAdapter"; +import { HashingAdapter } from "../../../src/adapters/IHashingAdapter/adapter"; import { LockFileModule } from "../../../src/modules/lockFile/module"; describe("LockFileModule.computeFacetHash", () => { diff --git a/cli/test/modules/lockFile/module.test.ts b/cli/test/modules/lockFile/module.test.ts index b1dd594f..59fa5ee6 100644 --- a/cli/test/modules/lockFile/module.test.ts +++ b/cli/test/modules/lockFile/module.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { HashingAdapter } from "../../../src/adapters/hashingAdapter"; +import { HashingAdapter } from "../../../src/adapters/IHashingAdapter/adapter"; import { LockFileModule } from "../../../src/modules/lockFile/module"; import { ComposeLock, DiamondDeployment } from "../../../src/modules/lockFile/types"; diff --git a/cli/test/modules/validation/astSelectors.test.ts b/cli/test/modules/validation/astSelectors.test.ts index 6fe10cbc..003b2526 100644 --- a/cli/test/modules/validation/astSelectors.test.ts +++ b/cli/test/modules/validation/astSelectors.test.ts @@ -5,8 +5,8 @@ import path from "node:path"; import { IFrameworkAdapter, SolidityAstSource, -} from "../../../src/adapters/interface/IFrameworkAdapter"; -import { HashingAdapter } from "../../../src/adapters/hashingAdapter"; +} from "../../../src/adapters/IFrameworkAdapter/interface"; +import { HashingAdapter } from "../../../src/adapters/IHashingAdapter/adapter"; import { Context } from "../../../src/context/context"; import { ModuleState } from "../../../src/context/types"; import { ValidationModule } from "../../../src/modules/validation/module"; diff --git a/cli/test/modules/validation/selectorScopes.test.ts b/cli/test/modules/validation/selectorScopes.test.ts index 8093bfc6..358ddcd3 100644 --- a/cli/test/modules/validation/selectorScopes.test.ts +++ b/cli/test/modules/validation/selectorScopes.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { HashingAdapter } from "../../../src/adapters/hashingAdapter"; +import { HashingAdapter } from "../../../src/adapters/IHashingAdapter/adapter"; import { Context } from "../../../src/context/context"; import { ValidationModule } from "../../../src/modules/validation/module"; import { FacetScanResult } from "../../../src/modules/validation/types"; diff --git a/cli/test/modules/validation/virtualStorageLayout.test.ts b/cli/test/modules/validation/virtualStorageLayout.test.ts index 43b6161e..3ba221f0 100644 --- a/cli/test/modules/validation/virtualStorageLayout.test.ts +++ b/cli/test/modules/validation/virtualStorageLayout.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; -import { SolidityAstSource } from "../../../src/adapters/interface/IFrameworkAdapter"; +import { SolidityAstSource } from "../../../src/adapters/IFrameworkAdapter/interface"; import { FacetReference, VirtualStorageLayoutRecord, diff --git a/cli/test/pipelines/initPipeline/harness.ts b/cli/test/pipelines/initPipeline/harness.ts index d117fdfb..d0076cb2 100644 --- a/cli/test/pipelines/initPipeline/harness.ts +++ b/cli/test/pipelines/initPipeline/harness.ts @@ -6,7 +6,7 @@ import { ConfigOptions, IFrameworkAdapter, SolidityAstSource, -} from "../../../src/adapters/interface/IFrameworkAdapter"; +} from "../../../src/adapters/IFrameworkAdapter/interface"; import { Context } from "../../../src/context/context"; import { ComposeContext } from "../../../src/context/types"; import { diff --git a/cli/test/pipelines/initPipeline/initPipeline.test.ts b/cli/test/pipelines/initPipeline/initPipeline.test.ts index a37246db..65564642 100644 --- a/cli/test/pipelines/initPipeline/initPipeline.test.ts +++ b/cli/test/pipelines/initPipeline/initPipeline.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { HashingAdapter } from "../../../src/adapters/hashingAdapter"; +import { HashingAdapter } from "../../../src/adapters/IHashingAdapter/adapter"; import { InitModule } from "../../../src/modules/init/module"; import { PreflightModule } from "../../../src/modules/preflight/module"; import { ValidationModule } from "../../../src/modules/validation/module"; diff --git a/cli/test/utils/chainConfig.test.ts b/cli/test/utils/chainConfig.test.ts index 7ac1110a..cf2dbefd 100644 --- a/cli/test/utils/chainConfig.test.ts +++ b/cli/test/utils/chainConfig.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { RPCAdapterError } from "../../src/adapters/rpc/errors"; +import { RPCAdapterError } from "../../src/adapters/IRPCAdapter/errors"; import { resolveChainConfig } from "../../src/utils/chainConfig"; const temporaryDirectories: string[] = []; diff --git a/website/docs/contribution/cli/testing.mdx b/website/docs/contribution/cli/testing.mdx index ee0f5b06..c6b9c415 100644 --- a/website/docs/contribution/cli/testing.mdx +++ b/website/docs/contribution/cli/testing.mdx @@ -18,7 +18,14 @@ The `cli/test` directory mirrors `cli/src`. Keep each feature's test, harness, a ```text cli/test/ ├── adapters/ -│ └── hashingAdapter.test.ts +│ ├── IFrameworkAdapter/ +│ │ ├── fixtures/ +│ │ ├── foundryAdapter/ +│ │ └── hardhatAdapter/ +│ ├── IHashingAdapter/ +│ │ └── adapter.test.ts +│ └── IRPCAdapter/ +│ └── adapter.test.ts ├── modules/ │ └── deployGeneration/ │ ├── fixtures/ From 4862d576a8446c759f0ae6fca1d39f19f36c680a Mon Sep 17 00:00:00 2001 From: Vagabond Date: Sat, 19 Sep 2026 21:11:50 +0700 Subject: [PATCH 5/5] chore: add changeset --- .changeset/wild-readers-battle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wild-readers-battle.md diff --git a/.changeset/wild-readers-battle.md b/.changeset/wild-readers-battle.md new file mode 100644 index 00000000..aebc53b1 --- /dev/null +++ b/.changeset/wild-readers-battle.md @@ -0,0 +1,5 @@ +--- +"@perfect-abstractions/compose-cli": patch +--- + +Add compose init test coverage and reorganize adapters by interface.