import { describe, test, expect } from "vitest"; import { mkdir, writeFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { env, unknownEnvError, isProdLikeEnvironment, ENV_VAR } from "./env"; import { discover } from "./discovery/index"; const withEnv = async (value: string | undefined, fn: () => void | Promise): Promise => { const prev = process.env[ENV_VAR]; try { if (value === undefined) delete process.env[ENV_VAR]; else process.env[ENV_VAR] = value; await fn(); } finally { if (prev === undefined) delete process.env[ENV_VAR]; else process.env[ENV_VAR] = prev; } }; describe("env()", () => { test("reads CHANT_ENV, with a fallback", async () => { await withEnv(undefined, () => { expect(env()).toBeUndefined(); expect(env("dev")).toBe("dev"); }); await withEnv("prod", () => { expect(env()).toBe("prod"); expect(env("dev")).toBe("prod"); }); }); }); describe("unknownEnvError", () => { test("accepts a declared env, and any env when none are declared", () => { expect(unknownEnvError("prod", ["dev", "prod"])).toBeUndefined(); expect(unknownEnvError("prod", undefined)).toBeUndefined(); expect(unknownEnvError("prod", [])).toBeUndefined(); expect(unknownEnvError(undefined, ["dev"])).toBeUndefined(); }); test("rejects an undeclared env with a clear message", () => { expect(unknownEnvError("stage", ["dev", "prod"])).toMatch(/Unknown environment "stage".*dev, prod/); }); // #1166 — `environments` entries may now be `{ name, endpoint }`; validation // reduces to names either way, unaffected by an object entry's endpoint. test("accepts an object-form declared env, mixed with bare strings", () => { expect(unknownEnvError("floci", ["prod", { name: "floci", endpoint: "http://localhost:4566" }])).toBeUndefined(); }); test("rejects an undeclared env against a mixed string/object list, naming both", () => { expect( unknownEnvError("stage", ["prod", { name: "floci", endpoint: "http://localhost:4566" }]), ).toMatch(/Unknown environment "stage".*prod, floci/); }); // #1221 — an entry containing `*` is a glob pattern, so an unbounded family // (per-PR environments, per-suite test environments) is declarable without // listing each name. test("accepts an env covered by a declared glob pattern (#1221)", () => { expect(unknownEnvError("pr-42", ["dev", "prod", "pr-*"])).toBeUndefined(); expect(unknownEnvError("test-suite-a", ["prod", { name: "test-*", endpoint: "http://localhost:4566" }])).toBeUndefined(); }); test("rejects an env no pattern covers, listing the patterns among the declared entries (#1221)", () => { expect(unknownEnvError("stage", ["dev", "prod", "pr-*"])).toMatch(/Unknown environment "stage".*dev, prod, pr-\*/); expect(unknownEnvError("pr", ["pr-*"])).toMatch(/Unknown environment "pr"/); // prefix alone is not covered }); test("a literal entry still matches by equality, never as a pattern (#1221)", () => { // `prod` contains no `*`: nothing about pattern support changes it. expect(unknownEnvError("prod", ["prod", "pr-*"])).toBeUndefined(); expect(unknownEnvError("production", ["prod"])).toMatch(/Unknown environment "production"/); }); }); describe("env-aware discovery (#505)", () => { // Env is a build-context switch: the same source, discovered under a different // CHANT_ENV, yields a different entity set (here a prod-only resource). const src = ` export const base = { entityType: "Base", [Symbol.for("chant.declarable")]: true }; export const prodOnly = process.env.CHANT_ENV === "prod" ? { entityType: "ProdOnly", [Symbol.for("chant.declarable")]: true } : undefined; `; test("re-evaluates the project under the active environment", async () => { // Distinct dirs → distinct module URLs → fresh import per env (import() caches by path). const prodDir = join(tmpdir(), `chant-env-prod-${Date.now()}-${Math.random()}`); const devDir = join(tmpdir(), `chant-env-dev-${Date.now()}-${Math.random()}`); await mkdir(prodDir, { recursive: true }); await mkdir(devDir, { recursive: true }); await writeFile(join(prodDir, "app.ts"), src); await writeFile(join(devDir, "app.ts"), src); try { let prod!: Awaited>; let dev!: Awaited>; await withEnv("prod", async () => { prod = await discover(prodDir); }); await withEnv("dev", async () => { dev = await discover(devDir); }); expect(prod.entities.has("base")).toBe(true); expect(dev.entities.has("base")).toBe(true); expect(prod.entities.has("prodOnly")).toBe(true); // present under prod expect(dev.entities.has("prodOnly")).toBe(false); // absent under dev } finally { await rm(prodDir, { recursive: true, force: true }); await rm(devDir, { recursive: true, force: true }); } }); }); describe("isProdLikeEnvironment (#1222)", () => { test("matches prod, production, and separator-delimited variants", () => { for (const name of ["prod", "production", "Prod", "PRODUCTION", "prod-eu", "us-prod", "prod2", "production-east", "eu_prod"]) { expect(isProdLikeEnvironment(name)).toBe(true); } }); test("does not match names that merely contain the letters", () => { for (const name of ["dev", "staging", "preprod", "product-demo", "reproduction", "pr-123"]) { expect(isProdLikeEnvironment(name)).toBe(false); } }); });