Files
agentic-dev/turbo/generators/__tests__/feature.e2e.test.ts
Danijel Martinek f69b53d73e test(generators): add feature generator composition e2e test
The core-package e2e tests cover each generator in isolation. None
exercised generators running in sequence — so the feature template
shipping without an integrations/cms/index.ts barrel went unnoticed
until `gen job` failed against a scaffolded feature.

This test scaffolds a feature, then runs `gen job` against it. `gen job`
throws at its `<gen:job-tasks>` anchor assertion if the barrel is
missing, so the composition gap now fails loudly.
2026-05-21 11:49:56 +02:00

77 lines
2.7 KiB
TypeScript

import { describe, it, expect, onTestFinished } from "vitest";
import { mkdtempSync, rmSync, cpSync, existsSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
// Repo root is 2 levels up from turbo/generators/__tests__
const REPO_ROOT = resolve(
fileURLToPath(import.meta.url),
"..",
"..",
"..",
"..",
);
/**
* Generator-composition e2e: scaffold a feature, then run `gen job` against
* the freshly-scaffolded feature.
*
* The `core-package` e2e tests verify each generator's output in isolation
* (byte-identical snapshots). This one verifies generators run *in sequence*:
* `gen job` asserts the consumer feature's `integrations/cms/index.ts` carries
* a `<gen:job-tasks>` anchor, so this test fails loudly if the `feature`
* template ever stops emitting that barrel — the regression that shipped once.
*/
describe("e2e: feature generator composition", () => {
it(
"scaffolds a feature that `gen job` can extend",
{ timeout: 180_000 },
() => {
const tmp = mkdtempSync(join(tmpdir(), "e2e-feature-"));
// Remove the temp clone when the test finishes so sequential runs
// don't accumulate and fill disk.
onTestFinished(() => rmSync(tmp, { recursive: true, force: true }));
cpSync(REPO_ROOT, tmp, {
recursive: true,
filter: (src) =>
!src.includes("node_modules") &&
!src.includes(".turbo") &&
!src.includes(".pnpm-store"),
});
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {
stdio: "ignore",
});
execSync(
`cd ${tmp} && pnpm turbo gen feature --args demo-feature Widget widgets`,
{ stdio: "ignore" },
);
// Regression guard: `gen job` throws at its anchor assertion if the
// feature template did not emit src/integrations/cms/index.ts.
execSync(
`cd ${tmp} && pnpm turbo gen job --args demo-feature send-demo-email void`,
{ stdio: "ignore" },
);
const featureRoot = join(tmp, "packages", "demo-feature");
const cmsIndex = join(featureRoot, "src/integrations/cms/index.ts");
expect(existsSync(cmsIndex)).toBe(true);
expect(readFileSync(cmsIndex, "utf8")).toContain(
'export { sendDemoEmailTask } from "./jobs/send-demo-email.task"',
);
expect(
existsSync(join(featureRoot, "src/jobs/send-demo-email.job.ts")),
).toBe(true);
const pkgJson = JSON.parse(
readFileSync(join(featureRoot, "package.json"), "utf8"),
) as { exports: Record<string, string> };
expect(pkgJson.exports["./cms"]).toBe("./src/integrations/cms/index.ts");
},
);
});