diff --git a/services/api/src/sandbox/orchestrator.ts b/services/api/src/sandbox/orchestrator.ts index f4b653da..eeffe3f5 100644 --- a/services/api/src/sandbox/orchestrator.ts +++ b/services/api/src/sandbox/orchestrator.ts @@ -1728,7 +1728,12 @@ async function buildRunBundle( ); const checkCmds = resolveCheckCmds(profile, repo?.renderAnswers, project?.settings); const provisionSummary = [packageInstallCmd, provisionCmd].filter(Boolean).join(" && ") || null; - const contract = renderRunContract(rawContract, provisionSummary, checkCmds); + const contract = renderRunContract( + rawContract, + provisionSummary, + checkCmds, + repo?.defaultBranch ?? null, + ); const githubBranch = typeof runGh.branch === "string" ? runGh.branch : null; const checkoutBranch = githubPullRequestMode(run.mode) && githubBranch ? githubBranch : null; const expectedHeadSha = repairExpectedHeadSha(run.mode, run.trigger); @@ -2539,13 +2544,15 @@ export function renderRunContract( contract: string, provisionCmd: string | null, checkCmds: string[], + defaultBranch: string | null, ) { return contract .replaceAll("{{PROVISION_CMD}}", provisionCmd ?? "No provision command is configured.") .replaceAll( "{{CHECKS_INLINE}}", checkCmds.length ? checkCmds.join(" && ") : "No checks configured.", - ); + ) + .replaceAll("{{DEFAULT_BRANCH}}", defaultBranch || "main"); } function command(value: unknown) { diff --git a/services/api/test/orchestrator-checks.test.ts b/services/api/test/orchestrator-checks.test.ts index 46510db0..fc6f6b23 100644 --- a/services/api/test/orchestrator-checks.test.ts +++ b/services/api/test/orchestrator-checks.test.ts @@ -1,3 +1,6 @@ +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { harnessFragmentForBundle } from "../src/harness.js"; import { @@ -186,11 +189,41 @@ describe("platform run repository setup", () => { it("renders repository setup and gates into platform contracts", () => { expect( - renderRunContract("Provision: {{PROVISION_CMD}}\nChecks: {{CHECKS_INLINE}}", "pnpm install", [ - "pnpm test", - "pnpm lint", - ]), - ).toBe("Provision: pnpm install\nChecks: pnpm test && pnpm lint"); + renderRunContract( + "Provision: {{PROVISION_CMD}}\nChecks: {{CHECKS_INLINE}}\nBase: origin/{{DEFAULT_BRANCH}}", + "pnpm install", + ["pnpm test", "pnpm lint"], + "trunk", + ), + ).toBe("Provision: pnpm install\nChecks: pnpm test && pnpm lint\nBase: origin/trunk"); + }); + + it("falls back to main when the repository has no recorded default branch", () => { + expect(renderRunContract("origin/{{DEFAULT_BRANCH}}", null, [], null)).toBe("origin/main"); + }); + + // The repository lane renders these templates through packages/core/src/render.ts, + // which understands every placeholder. renderRunContract is a hand-written subset, + // so a placeholder added to a shared prompt reaches a platform agent literally + // unless this test fails first. + it("leaves no placeholder unrendered in any shared prompt template", async () => { + const promptsDir = fileURLToPath( + new URL("../../../packages/cli/templates/prompts/", import.meta.url), + ); + const names = (await readdir(promptsDir)).filter((name) => name.endsWith(".md")); + expect(names.length).toBeGreaterThan(0); + + const leftovers = new Set(); + for (const name of names) { + const rendered = renderRunContract( + await readFile(join(promptsDir, name), "utf8"), + "pnpm install", + ["pnpm test"], + "main", + ); + for (const match of rendered.match(/\{\{[A-Z_]+\}\}/g) ?? []) leftovers.add(match); + } + expect([...leftovers]).toEqual([]); }); });