diff --git a/server/__tests__/lane-profile.test.js b/server/__tests__/lane-profile.test.js index 73949c7..1d7fe54 100644 --- a/server/__tests__/lane-profile.test.js +++ b/server/__tests__/lane-profile.test.js @@ -111,3 +111,32 @@ describe("resolveProfile — E2 declarations", () => { assert.deepEqual(profile.generatedMergePaths, ["api/openapi.json", "api/client.ts"]); }); }); + +describe("isIntegrationEnabled", () => { + it("returns false when integrations.env doesn't exist at all", () => { + const lane = makeLane(); + writeProfile(lane.cwd, "PORTS=api\n"); + assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "tracker"), false); + }); + + it("returns true when _ENABLED=1 is set", () => { + const lane = makeLane(); + writeProfile(lane.cwd, "PORTS=api\n"); + fsMod.writeFileSync( + pathMod.join(lane.cwd, ".ccam", "profile", "integrations.env"), + "TRACKER_ENABLED=1\nTRACKER_PROJECT=demo\n" + ); + assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "tracker"), true); + }); + + it("returns false when the flag is 0 or absent from an existing file", () => { + const lane = makeLane(); + writeProfile(lane.cwd, "PORTS=api\n"); + fsMod.writeFileSync( + pathMod.join(lane.cwd, ".ccam", "profile", "integrations.env"), + "DEV_QC_ENABLED=0\n" + ); + assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "dev_qc"), false); + assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "ci_wait"), false); + }); +}); diff --git a/server/lib/lane-profile.js b/server/lib/lane-profile.js index e930e8b..4a7c78a 100644 --- a/server/lib/lane-profile.js +++ b/server/lib/lane-profile.js @@ -195,6 +195,34 @@ function profileSearchPaths(lane) { .map((root) => path.join(root, PROFILE_SUBDIR)); } +/** + * Whether a named integration is turned on for this lane — reads + * .ccam/profile/integrations.env (same two-location search as profile.env: + * the lane's own working copy first, the source repo second) and checks + * _ENABLED=1. A missing file or missing key is off, never an error — + * same off-by-default shape every other optional declaration follows. + * + * @param {object} lane - Lane row (`cwd`, `source_repo`). + * @param {string} name - Lowercase integration name, e.g. "tracker", "dev_qc", "ci_wait". + * @returns {boolean} + */ +function isIntegrationEnabled(lane, name) { + const candidates = [lane.cwd, lane.source_repo].filter(Boolean); + const key = `${name.toUpperCase()}_ENABLED`; + for (const root of candidates) { + const filePath = path.join(root, PROFILE_SUBDIR, "integrations.env"); + if (!fs.existsSync(filePath)) continue; + let declared; + try { + declared = parseEnvFile(fs.readFileSync(filePath, "utf8")); + } catch { + continue; + } + return declared[key] === "1"; + } + return false; +} + /** * A `harness_spawn` shell function, injected into every hook. * @@ -428,6 +456,7 @@ module.exports = { parseQcBootEnv, resolveProfile, profileSearchPaths, + isIntegrationEnabled, hookEnv, runHook, };