feat(lanes): scaffold .ccam/profile/ from detected Node facts (A3)

This commit is contained in:
2026-08-04 09:20:22 +07:00
parent c4b11799dc
commit 9f9879cc3d
2 changed files with 222 additions and 0 deletions
+91
View File
@@ -171,3 +171,94 @@ describe("detectNode: database + Redis + env wiring", () => {
assert.equal(detect.detectNode(repo).database, null);
});
});
describe("scaffoldProfile", () => {
it("writes a single-service profile with no database: bootstrap/boot/health only, zero TODOs", () => {
const repo = makeRepo();
writePkg(repo, { start: "node index.js" });
const facts = detect.detectNode(repo);
const result = detect.scaffoldProfile(repo, facts);
const profileDir = path.join(repo, ".ccam", "profile");
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
assert.match(env, /PORTS="app"/);
assert.match(env, /PORT_BASE_app=3000/);
assert.doesNotMatch(env, /DB_PREFIX/);
assert.doesNotMatch(env, /TODO/);
for (const hook of ["bootstrap.sh", "boot.sh", "health.sh"]) {
const hookPath = path.join(profileDir, "hooks", hook);
assert.ok(fs.existsSync(hookPath), hook);
assert.ok(fs.statSync(hookPath).mode & 0o111, `${hook} must be executable`);
}
assert.match(
fs.readFileSync(path.join(profileDir, "hooks", "boot.sh"), "utf8"),
/npm run start/
);
assert.equal(result.todos.length, 0);
});
it("writes a monorepo profile with a database: DB_PREFIX, ENV_REWRITE, copied db hooks, TODO migrate/seed", () => {
const repo = makeRepo();
writePkg(path.join(repo, "backend"), { start: "node server.js" });
writePkg(path.join(repo, "frontend"), { dev: "vite" }); // no preview -> exercises dev fallback
writeCompose(repo, " postgres:\n image: postgres:16\n redis:\n image: redis:7\n");
fs.writeFileSync(path.join(repo, "backend", ".env.example"), "FOO=bar\n");
const facts = detect.detectNode(repo);
const result = detect.scaffoldProfile(repo, facts);
const profileDir = path.join(repo, ".ccam", "profile");
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
assert.match(env, /DB_PREFIX=/);
assert.match(env, /REDIS=1/);
assert.match(env, /ENV_FILES="backend\/\.env"/);
assert.match(env, /ENV_REWRITE="DATABASE_URL REDIS_URL"/);
for (const hook of ["db-create.sh", "db-drop.sh", "migrate.sh", "seed.sh"]) {
assert.ok(fs.existsSync(path.join(profileDir, "hooks", hook)), hook);
}
assert.match(fs.readFileSync(path.join(profileDir, "hooks", "migrate.sh"), "utf8"), /exit 0/);
assert.match(
fs.readFileSync(path.join(profileDir, "hooks", "boot.sh"), "utf8"),
/npm run dev/ // frontend fallback, no preview script
);
assert.ok(result.todos.length >= 2, "migrate and seed are both TODO");
});
it("writes a TODO boot line (never a guess) when no start/dev script was found", () => {
const repo = makeRepo();
writePkg(repo, { test: "jest" }); // no start, no dev
const facts = {
layout: "single-service",
services: { app: { dir: ".", script: null } },
database: null,
redis: false,
env: null,
};
const result = detect.scaffoldProfile(repo, facts);
const boot = fs.readFileSync(path.join(repo, ".ccam", "profile", "hooks", "boot.sh"), "utf8");
assert.match(boot, /# TODO: no start\/dev script found/);
assert.equal(result.todos.length, 1);
});
it("refuses to overwrite an existing profile without force", () => {
const repo = makeRepo();
writePkg(repo, { start: "node index.js" });
const facts = detect.detectNode(repo);
detect.scaffoldProfile(repo, facts);
assert.throws(
() => detect.scaffoldProfile(repo, facts),
(err) => err.code === "EPROFILEEXISTS"
);
});
it("overwrites when force is true", () => {
const repo = makeRepo();
writePkg(repo, { start: "node index.js" });
const facts = detect.detectNode(repo);
detect.scaffoldProfile(repo, facts);
assert.doesNotThrow(() => detect.scaffoldProfile(repo, facts, { force: true }));
});
});