import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import fs from "fs"; import path from "path"; import os from "os"; import { parseSemver, computeNextStableVersion, computePrereleaseVersion, bumpPackages, } from "./versions.js"; let tmpDir: string; // Mock config — ROOT points to a temp dir for bumpPackages tests vi.mock("./config.js", async () => { return { get ROOT() { return tmpDir || "/mock"; }, loadConfig: () => ({ prereleaseTag: "canary", scopes: { monorepo: { packages: ["@copilotkit/shared", "@copilotkit/react-core"], versionSource: "@copilotkit/react-core", sharedVersion: true, }, angular: { packages: ["@copilotkitnext/angular"], versionSource: "@copilotkitnext/angular", sharedVersion: false, }, }, }), getScopeConfig: (scope: string) => { const scopes: Record = { monorepo: { packages: ["@copilotkit/shared", "@copilotkit/react-core"], versionSource: "@copilotkit/react-core", sharedVersion: true, }, angular: { packages: ["@copilotkitnext/angular"], versionSource: "@copilotkitnext/angular", sharedVersion: false, }, }; return scopes[scope]; }, }; }); describe("parseSemver", () => { it("parses a stable version", () => { expect(parseSemver("1.55.2")).toEqual({ major: 1, minor: 55, patch: 2, prerelease: null, }); }); it("parses a prerelease version", () => { expect(parseSemver("1.55.2-canary.1744382400")).toEqual({ major: 1, minor: 55, patch: 2, prerelease: "canary.1744382400", }); }); it("parses a version with text prerelease", () => { expect(parseSemver("2.0.0-canary.fix-user-issue")).toEqual({ major: 2, minor: 0, patch: 0, prerelease: "canary.fix-user-issue", }); }); it("throws on invalid version", () => { expect(() => parseSemver("not-a-version")).toThrow("Invalid semver"); }); it("throws on empty string", () => { expect(() => parseSemver("")).toThrow("Invalid semver"); }); }); describe("computeNextStableVersion", () => { it("bumps patch", () => { expect(computeNextStableVersion("1.55.2", "patch")).toBe("1.55.3"); }); it("bumps minor", () => { expect(computeNextStableVersion("1.55.2", "minor")).toBe("1.56.0"); }); it("bumps major", () => { expect(computeNextStableVersion("1.55.2", "major")).toBe("2.0.0"); }); it("strips prerelease suffix on any bump", () => { expect(computeNextStableVersion("1.56.0-canary.123", "patch")).toBe( "1.56.0", ); }); it("strips prerelease suffix regardless of bump level", () => { expect(computeNextStableVersion("2.0.0-canary.123", "major")).toBe("2.0.0"); }); it("handles 0.x versions", () => { expect(computeNextStableVersion("0.1.0", "patch")).toBe("0.1.1"); expect(computeNextStableVersion("0.1.0", "minor")).toBe("0.2.0"); expect(computeNextStableVersion("0.1.0", "major")).toBe("1.0.0"); }); }); describe("computePrereleaseVersion", () => { it("appends canary tag with timestamp when no suffix given", () => { const result = computePrereleaseVersion("1.55.2"); expect(result).toMatch(/^1\.55\.2-canary\.\d+$/); }); it("appends canary tag with custom suffix", () => { expect(computePrereleaseVersion("1.55.2", "fix-user-issue")).toBe( "1.55.2-canary.fix-user-issue", ); }); it("uses the base version as-is (no bump)", () => { expect(computePrereleaseVersion("1.55.2", "test")).toBe( "1.55.2-canary.test", ); expect(computePrereleaseVersion("2.0.0", "test")).toBe("2.0.0-canary.test"); }); it("strips existing prerelease before appending", () => { expect(computePrereleaseVersion("1.55.2-canary.old", "new")).toBe( "1.55.2-canary.new", ); }); }); describe("bumpPackages", () => { beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "release-test-")); const packagesDir = path.join(tmpDir, "packages"); // Create shared package const sharedDir = path.join(packagesDir, "shared"); fs.mkdirSync(sharedDir, { recursive: true }); fs.writeFileSync( path.join(sharedDir, "package.json"), JSON.stringify({ name: "@copilotkit/shared", version: "1.55.2", }), ); // Create react-core with workspace:* dep on shared const reactCoreDir = path.join(packagesDir, "react-core"); fs.mkdirSync(reactCoreDir, { recursive: true }); fs.writeFileSync( path.join(reactCoreDir, "package.json"), JSON.stringify({ name: "@copilotkit/react-core", version: "1.55.2", dependencies: { "@copilotkit/shared": "workspace:*", }, }), ); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("bumps version fields", () => { const result = bumpPackages("monorepo", "1.55.3"); expect(result).toHaveLength(2); expect(result[0].newVersion).toBe("1.55.3"); const pkg = JSON.parse( fs.readFileSync( path.join(tmpDir, "packages/shared/package.json"), "utf8", ), ); expect(pkg.version).toBe("1.55.3"); }); it("preserves workspace:* protocol in dependencies", () => { bumpPackages("monorepo", "1.55.3"); const pkg = JSON.parse( fs.readFileSync( path.join(tmpDir, "packages/react-core/package.json"), "utf8", ), ); expect(pkg.dependencies["@copilotkit/shared"]).toBe("workspace:*"); }); it("updates exact version deps (not workspace protocol)", () => { // Write react-core with an exact version dep instead of workspace:* fs.writeFileSync( path.join(tmpDir, "packages/react-core/package.json"), JSON.stringify({ name: "@copilotkit/react-core", version: "1.55.2", dependencies: { "@copilotkit/shared": "1.55.2", }, }), ); bumpPackages("monorepo", "1.55.3"); const pkg = JSON.parse( fs.readFileSync( path.join(tmpDir, "packages/react-core/package.json"), "utf8", ), ); expect(pkg.dependencies["@copilotkit/shared"]).toBe("1.55.3"); }); });