teqo/scripts/lib/worktree-env.mjs
Francisco Solla 7785c1088c fix(worktree): provisiona PLAYWRIGHT_BASE_URL no .env.test.local — e2e usa porta do slot (OPS20)
O .env.test.local (que o playwright.config.ts lê) passa a carregar
PLAYWRIGHT_BASE_URL na porta do slot (3100+slot); antes só o .env.local
(que o config não lê) o tinha, então dois test:e2e em worktrees paralelos
subiam dev servers na porta 3000. Causa-raiz tratada: os dois blocos env
inline do provision viraram um builder puro único (worktreeEnvFileContents
em scripts/lib/worktree-env.mjs) que deriva NEXT_PUBLIC_SITE_URL +
PLAYWRIGHT_BASE_URL do mesmo devPort para ambos os arquivos — paridade
estrutural, drift impossível + unit tests de paridade. playwright.config.ts
intocado (a variável certa já existia).
2026-08-09 02:19:26 -03:00

117 lines
4.5 KiB
JavaScript

/**
* Pure helpers for the per-worktree environment provisioning in
* `scripts/worktree.mjs` — deterministic port + database names derived from
* the branch, so parallel agents (each in their own worktree) never fight
* over port 3000, the shared `teqo_test` database, or a second Postgres
* container on 5432.
*
* Contract: `slot` is the single collision domain. Port and BOTH database
* names derive from the final slot, so a bumped slot frees all three at
* once. Deterministic for a given (branch, code, live taken-slots set) —
* the same "depends on live state" determinism the claim-queue branch picker
* already has.
*/
/** Dev-server ports start here: slot 15 → http://localhost:3115. */
export const DEV_PORT_BASE = 3100
/** Marker line identifying a generated env file (never overwrite manual ones). */
export const GENERATED_ENV_MARKER = '# teqo worktree env (generated by pnpm worktree next)'
/** Slot cap — ports above 3100 + 999 would leave the usual ephemeral range. */
const MAX_SLOT = 999
/** First run of digits in the issue code (`C15` → 15, `B164` → 164); null when none. */
export const numericSlotOfCode = (code) => {
const match = /([0-9]+)/.exec(code ?? '')
return match ? Number(match[1]) : null
}
/** Stable 32-bit FNV-1a hash → slot in [0, MAX_SLOT] for codes without digits. */
export const hashSlotOf = (value) => {
let hash = 0x811c9dc5
for (const byte of Buffer.from(String(value), 'utf8')) {
hash ^= byte
hash = Math.imul(hash, 0x01000193) >>> 0
}
return hash % (MAX_SLOT + 1)
}
/** The slot a branch prefers before collision resolution. */
const preferredSlotForBranch = (branch, code) => {
const numeric = numericSlotOfCode(code)
return numeric !== null && numeric <= MAX_SLOT ? numeric : hashSlotOf(branch)
}
export const devPortForSlot = (slot) => DEV_PORT_BASE + slot
export const devDatabaseForSlot = (slot) => `teqo_wt${slot}`
export const testDatabaseForSlot = (slot) => `teqo_wt${slot}_test`
/** DB name charset guard — derived names are fed into `CREATE DATABASE` SQL. */
export const isGeneratedDatabaseName = (name) => /^teqo_wt[0-9]+(_test)?$/.test(name ?? '')
/**
* Final environment for a branch. `takenSlots` = slots already claimed by
* OTHER live worktrees; bumps +1 until free (bounded — live slot sets are
* tiny). Every value a consumer needs derives from the one final slot.
*/
export const worktreeEnvironment = ({ branch, code, takenSlots = new Set() }) => {
let slot = preferredSlotForBranch(branch, code)
while (takenSlots.has(slot)) slot += 1
return {
slot,
devPort: devPortForSlot(slot),
devDatabase: devDatabaseForSlot(slot),
testDatabase: testDatabaseForSlot(slot),
}
}
/**
* Line arrays for `.env.local` (dev) and `.env.test.local` (test) of a
* provisioned worktree. `NEXT_PUBLIC_SITE_URL` and `PLAYWRIGHT_BASE_URL` must
* point BOTH files at the SAME slot port — the parity is what keeps a parallel
* e2e run in another worktree from falling back to port 3000 (OPS20: they had
* drifted — the dev file carried PLAYWRIGHT_BASE_URL, the test file didn't, and
* playwright.config.ts only reads `.env.test.local`). Building both from one
* `url` makes the drift structurally impossible.
*
* @param {{ branch: string, issueLabel?: string, generatedBy: string, env: { slot: number, devPort: number, devDatabase: string, testDatabase: string }, payloadSecret: string, copiedLines?: string[] }} options
* @returns {{ dev: string[], test: string[] }}
*/
export const worktreeEnvFileContents = ({
branch,
issueLabel = '',
generatedBy,
env,
payloadSecret,
copiedLines = [],
}) => {
// Both DATABASE_URLs derive from the same slot as the port (module contract).
const devUrl = `postgresql://teqo:teqo@localhost:5432/${env.devDatabase}`
const testUrl = `postgresql://teqo:teqo@localhost:5432/${env.testDatabase}`
const url = `http://localhost:${env.devPort}`
const header = [
GENERATED_ENV_MARKER,
`# branch ${branch}${issueLabel} · slot ${env.slot} — ${generatedBy}`,
]
return {
dev: [
...header,
`DATABASE_URL=${devUrl}`,
`PORT=${env.devPort}`,
`NEXT_PUBLIC_SITE_URL=${url}`,
`PLAYWRIGHT_BASE_URL=${url}`,
`PAYLOAD_SECRET=${payloadSecret}`,
...copiedLines,
'',
],
test: [
...header,
'# Lido DEPOIS de .env.test (vitest.setup.ts / playwright.config.ts) — suites isoladas.',
`DATABASE_URL=${testUrl}`,
`NEXT_PUBLIC_SITE_URL=${url}`,
`PLAYWRIGHT_BASE_URL=${url}`,
'',
],
}
}