From c399b918c3f75c907c4dd82f176549e382358e7f Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 8 Aug 2026 16:00:27 -0300 Subject: [PATCH] =?UTF-8?q?feat(worktree):=20comando=20plan=20=E2=80=94=20?= =?UTF-8?q?worktree=20dedicado=20ao=20/plan-issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novo subcomando `pnpm worktree plan [--go] [--no-migrate]` cria/reutiliza o worktree de planejamento no branch fixo `plans/plan-issue` (`PLAN_WORKTREE_BRANCH`), sem vínculo com a fila de claim: um `next` posterior da próxima Issue nunca colide em branch nem em slot (slot derivado por hash do branch, bumpado entre worktrees vivos). Mesmo provisionamento isolado do `next` — porta 3100+slot, bancos teqo_wt/teqo_wt_test, migrations. - scripts/worktree.mjs: cmdPlan + provision() com purpose/issue opcionais - scripts/lib/worktree.mjs: constante PLAN_WORKTREE_BRANCH - tests/unit/worktree.unit.spec.ts: derivação de slot do plano - docs: .opencode/commands/worktree.md, shell, skills, AGENTS.md, changelog --- .agents/shell/worktree.sh | 3 + .agents/skills/local-database/SKILL.md | 2 +- .agents/skills/worktree-next-issue/SKILL.md | 3 + .opencode/commands/worktree.md | 3 +- AGENTS.md | 2 +- docs/CHANGELOG-AGENTS.md | 2 + scripts/lib/worktree.mjs | 7 ++ scripts/worktree.mjs | 82 +++++++++++++++++++-- tests/unit/worktree.unit.spec.ts | 23 +++++- 9 files changed, 116 insertions(+), 11 deletions(-) diff --git a/.agents/shell/worktree.sh b/.agents/shell/worktree.sh index 0958b001..16172767 100644 --- a/.agents/shell/worktree.sh +++ b/.agents/shell/worktree.sh @@ -11,6 +11,9 @@ # Uso (terminal interativo): # worktree next [--go] cria/reutiliza o worktree da próxima Issue claimável; # com --go, cd para dentro dele +# worktree plan [--go] cria/reutiliza o worktree de planejamento do /plan-issue +# (branch fixo plans/plan-issue, sem vínculo com a fila); +# com --go, cd para dentro dele # worktree kill [--force] # # Read-only no GitHub — claim continua sendo `pnpm agent:claim`. diff --git a/.agents/skills/local-database/SKILL.md b/.agents/skills/local-database/SKILL.md index d0d5971c..33ec8bc4 100644 --- a/.agents/skills/local-database/SKILL.md +++ b/.agents/skills/local-database/SKILL.md @@ -30,7 +30,7 @@ Parallel agents must not fight over port 3000, the shared `teqo_test`, or a seco - `slot` = numeric part of the issue code (`C15` → 15, `B164` → 164; hash fallback without digits; bumped on collision among live worktrees). - Dev server port `3100+slot`; databases `teqo_wt` (dev) and `teqo_wt_test` (tests), created on the **one shared container** with the full migration chain applied (one-time, ~30 s each). -- Writes `.env.local` (DATABASE_URL, PORT, NEXT_PUBLIC_SITE_URL, PLAYWRIGHT_BASE_URL, PAYLOAD_SECRET + Blob/VAPID copied from the main repo) and `.env.test.local` (test DATABASE_URL). Both carry the marker `# teqo worktree env (generated by pnpm worktree next)`; **manual env files are never overwritten** — remove them and re-run `pnpm worktree next` to regenerate. +- Writes `.env.local` (DATABASE_URL, PORT, NEXT_PUBLIC_SITE_URL, PLAYWRIGHT_BASE_URL, PAYLOAD_SECRET + Blob/VAPID copied from the main repo) and `.env.test.local` (test DATABASE_URL). Both carry the marker `# teqo worktree env (generated by pnpm worktree next)`; **manual env files are never overwritten** — remove them and re-run `pnpm worktree next` (or `pnpm worktree plan` for the /plan-issue worktree) to regenerate. - `--no-migrate` skips the migration runs; `pnpm worktree kill` drops the worktree's generated databases (best-effort, `DROP DATABASE … WITH (FORCE)`). - Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning and no port override. - Pure derivation: `scripts/lib/worktree-env.mjs` (unit-tested in `tests/unit/worktree.unit.spec.ts`). diff --git a/.agents/skills/worktree-next-issue/SKILL.md b/.agents/skills/worktree-next-issue/SKILL.md index cff6bda2..06209215 100644 --- a/.agents/skills/worktree-next-issue/SKILL.md +++ b/.agents/skills/worktree-next-issue/SKILL.md @@ -20,11 +20,14 @@ A lógica determinística (fila → código+slug → branch → worktree) vive e ```bash pnpm worktree next [--go] [--no-migrate] # cria worktree de origin/main + provisiona o ambiente +pnpm worktree plan [--go] [--no-migrate] # worktree de PLANEJAMENTO (/plan-issue), branch fixo plans/plan-issue pnpm worktree kill [--force] # destrói o worktree atual + remove seus bancos gerados ``` No opencode, isso é o comando **`/worktree next`** / **`/worktree kill`** (`.opencode/commands/worktree.md`), que só repassa `$ARGUMENTS` para o script. +`plan` é o primo do `next`: worktree de **planejamento** (branch fixo `plans/plan-issue`) para rodar a skill `/plan-issue` sem ocupar o main. Não é nomeado por Issue nenhuma — proposital, para um `next` posterior da próxima Issue nunca colidir em branch nem em slot. O fluxo desta skill (fila → próxima Issue → implementação) é só `next`. + No terminal interativo, para `--go` trocar de diretório de verdade, use a função `worktree()` de **`.agents/shell/worktree.sh`** (uma linha de `source` no profile): o script imprime `cd ` e a função o aplica no shell que te chamou — node não consegue mudar o cwd do shell pai. ## Fluxo quando invocado como skill (agentes que não têm opencode) diff --git a/.opencode/commands/worktree.md b/.opencode/commands/worktree.md index 29893697..64c2df91 100644 --- a/.opencode/commands/worktree.md +++ b/.opencode/commands/worktree.md @@ -1,10 +1,11 @@ --- -description: Cria o worktree da próxima Issue claimável (next [--go]) ou destrói o worktree atual (kill [--force]) +description: Cria o worktree da próxima Issue claimável (next [--go]), o worktree de planejamento do /plan-issue (plan [--go]) ou destrói o worktree atual (kill [--force]) --- Rode o script determinístico abaixo (não improvise a lógica) e reporte o resultado de forma concisa. - `next [--go]`: cria (ou reutiliza) o worktree da próxima Issue claimável. Com `--go`, o script imprime `cd ` na última linha — aplique-o na sessão de shell atual (`cd `) e reporte o novo diretório. +- `plan [--go]`: cria (ou reutiliza) o worktree de planejamento (branch fixo `plans/plan-issue`, sem vínculo com a fila) para rodar `/plan-issue` sem ocupar o main; nunca conflita com `next`. - `kill [--force]`: destrói o worktree em que você está. !`node scripts/worktree.mjs $ARGUMENTS` diff --git a/AGENTS.md b/AGENTS.md index 06418c46..4cf6640c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ These are product/architecture constraints — not a ban on editing existing cod - `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit. - To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data). - `pnpm db:doctor` diagnoses local database connectivity for the dev AND test targets: it names who holds the port, points at exited/crash-looping containers, and flags Postgres containers sharing one data volume (two postmasters on one PGDATA corrupt the WAL — this destroyed the local cluster's checkpoint on 2026-07-25; the canonical setup is ONE compose container on 5432 hosting `teqo` + `teqo_test`). `pnpm dev` runs the same preflight via `guard-dev-db` and refuses to start with a named remedy instead of a later ECONNREFUSED. `db:start`/`db:stop` pin the compose project with `-p teqo`, so they always target that ONE container even from a worktree directory. -- **Per-worktree environments (`pnpm worktree next`, 2026-08-08):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--go]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt` (dev) and `teqo_wt_test` (tests) created on the shared container with the full migration chain applied; writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo__test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning. +- **Per-worktree environments (`pnpm worktree next`, 2026-08-08):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--go]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt` (dev) and `teqo_wt_test` (tests) created on the shared container with the full migration chain applied; writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. `worktree plan [--go]` (2026-08-08) is the sibling for the `/plan-issue` skill: same provisioning on the fixed branch `plans/plan-issue` (`PLAN_WORKTREE_BRANCH`, `scripts/lib/worktree.mjs`), deliberately **not** named after any Issue so a later `next` never collides in branch or slot. `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo__test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning. - To (re)populate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" below. - To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard). Since the 2026-07-23 hardening, elections-tab reads are cached under the `election-tse` tag — after re-seeding a database the deployed app reads, bust it via `POST /api/revalidate?tag=election-tse`. See "Election baseline data (TSE 2022)" below. diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index b43f6bb9..7a140e42 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-08):** **`pnpm worktree plan [--go] [--no-migrate]`** — worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main: branch fixo `plans/plan-issue` (`PLAN_WORKTREE_BRANCH`, `scripts/lib/worktree.mjs`), deliberadamente **não** nomeado por Issue nenhuma — um `next` posterior da próxima Issue nunca colide em branch nem em slot (slot do plano = hash do branch, registrado via marcador e bumpado pelos vivos). Mesmo provisionamento isolado do `next` (porta `3100+slot`, bancos `teqo_wt`/`teqo_wt_test`, migrations; sem Docker degrada ao compartilhado). `kill` cobre; comment das envs distingue `gerado por pnpm worktree plan`. Sem migration. Constant unit-testada. + **Recently resolved (2026-08-08):** **B168** (Issue #422) — desfeito o **encadeamento automático** das ações rápidas do wizard (B98 era o precedente que este item inverte; evidência de campo: o coordenador rejeitou a tela avançando sozinha). Ao salvar **qualquer** ação de ajuste (votos, tendência, atualização, liderança), o usuário volta à origem (`from` allowlist B110 mantido; Início sem `from`) com toast — nunca avança para outro wizard. Removidos a fila/pipeline `wizardActionChain.ts` (fundido `wizardReturnHref` + `wizardPreviousHref` só com passos internos em `campaignActionRoutes.ts`), o param `entry`, os skip resolvers/`Pular` (`WizardSkipTrailing`/`WizardTrendSkipTrailing` deletados, chrome mobile sempre “X”) e o prefill de nota sem produtor (`notePrefill`/`voteFrom`/`voteTo`). Tendência: pós-save o RSC refresha com status já aplicado e o **client** é o único dono da navegação (toast + retorno) — o redirect servidor engolia o toast (B97 corrigido no caminho). Liderança: botão flutuante pós-save passou de “Continuar” (avançava) para **“Concluir”** (retorna à origem). Sem migration. Intenção [`desfazer-encadeamento-acoes-wizard.md`](plans/desfazer-encadeamento-acoes-wizard.md); impl [`desfazer-encadeamento-acoes-wizard-impl.md`](plans/desfazer-encadeamento-acoes-wizard-impl.md). **Recently resolved (2026-08-08):** **worktree next provisiona ambiente isolado por worktree** — agentes em paralelo não disputam mais porta 3000, o `teqo_test` compartilhado nem um segundo Postgres no 5432: `pnpm worktree next [--go] [--no-migrate]` deriva deterministicamente slot do código da Issue (C15→15; hash sem dígitos; bump em colisão entre worktrees vivos) → dev server `3100+slot`, bancos `teqo_wt`/`teqo_wt_test` criados no container compartilhado com migrations aplicadas; escreve `.env.local` + `.env.test.local` (gitignored, marcador gerado; envs manuais nunca sobrescritos) e copia PAYLOAD*SECRET/Blob/VAPID do main. `kill` dropa os bancos gerados (best-effort, WITH FORCE). Suites leem `.env.test.local` depois de `.env.test` (vitest.setup/playwright/db-doctor/gate-ci espelham o layering); `assertTestDatabase` aceita `teqo_test` ou `teqo*\_test`em localhost só (fail-closed preservado).`db:start`/`db:stop`fixam`-p teqo`(um container, qualquer diretório); max_connections 200→400. Sem Docker (Cursor Cloud): degrada ao compartilhado com aviso. Derivação pura`scripts/lib/worktree-env.mjs` (unit-tested). diff --git a/scripts/lib/worktree.mjs b/scripts/lib/worktree.mjs index 18a4646d..5a205bd6 100644 --- a/scripts/lib/worktree.mjs +++ b/scripts/lib/worktree.mjs @@ -8,6 +8,13 @@ import { slugify } from '../../src/lib/slug.ts' +/** + * Generic branch of the /plan-issue planning worktree (`pnpm worktree plan`). + * Deliberately NOT tied to any Issue: it must never collide with the + * `-` branch `worktree next` derives for the next claimable Issue. + */ +export const PLAN_WORKTREE_BRANCH = 'plans/plan-issue' + /** Strip the leading ` — ` (or any dash variant) off a title. */ const stripCodePrefix = (title, code) => { const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') diff --git a/scripts/worktree.mjs b/scripts/worktree.mjs index 986009fc..e1089c42 100644 --- a/scripts/worktree.mjs +++ b/scripts/worktree.mjs @@ -17,6 +17,14 @@ * paralelo não disputarem porta 3000 nem o * `teqo_test` compartilhado. `--no-migrate` pula a * aplicação das migrations nos bancos novos. + * pnpm worktree plan [--go] [--no-migrate] + * cria/reutiliza o worktree de PLANEJAMENTO + * (branch fixo `plans/plan-issue`) para rodar a + * skill /plan-issue sem ocupar o main; NÃO é + * nomeado nem derivado de Issue nenhuma, então um + * `next` posterior da próxima Issue nunca colide + * (nem no branch, nem no slot). Mesmo provisionamento + * isolado do `next`. * pnpm worktree kill [--force] destrói o worktree em que o shell atual está * (recusa worktree sujo sem `--force`) e remove os * bancos gerados do worktree (best-effort) @@ -41,7 +49,7 @@ import { isGeneratedDatabaseName, worktreeEnvironment, } from './lib/worktree-env.mjs' -import { branchNameForIssue } from './lib/worktree.mjs' +import { branchNameForIssue, PLAN_WORKTREE_BRANCH } from './lib/worktree.mjs' const die = dieAgent('worktree') const WORKTREES_ROOT = join(homedir(), '.cursor', 'worktrees', 'teqo') @@ -197,8 +205,10 @@ const writeFallbackEnv = (dir, branch) => { * Deterministic per-worktree environment: shared container + own databases + * own dev-server port + env files + migrations. Skips everything when a * manual `.env.local`/`.env.test.local` (no marker) exists — never clobbers. + * `purpose` labels the env comment (`next` | `plan`); `issue` is optional and + * fills the `issue #N` part of the comment only when present. */ -const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot }) => { +const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpose = 'next' }) => { const manual = (existsSync(join(dir, '.env.local')) && !envFileIsGenerated(dir, '.env.local')) || (existsSync(join(dir, '.env.test.local')) && !envFileIsGenerated(dir, '.env.test.local')) @@ -231,9 +241,12 @@ const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot }) => const payloadSecret = mainEnv.PAYLOAD_SECRET ?? randomBytes(24).toString('hex') const copy = (key) => (mainEnv[key] ? [`${key}=${mainEnv[key]}`] : []) + const issueLabel = issue ? ` · issue #${issue.number}` : '' + const generatedBy = `gerado por pnpm worktree ${purpose}` + const devEnv = [ GENERATED_ENV_MARKER, - `# branch ${branch} · issue #${issue.number} · slot ${env.slot} — gerado por pnpm worktree next`, + `# branch ${branch}${issueLabel} · slot ${env.slot} — ${generatedBy}`, `DATABASE_URL=${devUrl}`, `PORT=${env.devPort}`, `NEXT_PUBLIC_SITE_URL=http://localhost:${env.devPort}`, @@ -254,7 +267,7 @@ const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot }) => const testEnv = [ GENERATED_ENV_MARKER, - `# branch ${branch} · issue #${issue.number} · slot ${env.slot} — gerado por pnpm worktree next`, + `# branch ${branch}${issueLabel} · slot ${env.slot} — ${generatedBy}`, '# Lido DEPOIS de .env.test (vitest.setup.ts / playwright.config.ts) — suites isoladas.', `DATABASE_URL=${testUrl}`, `NEXT_PUBLIC_SITE_URL=http://localhost:${env.devPort}`, @@ -321,6 +334,54 @@ const cmdNext = async (go, skipMigrate) => { if (go) console.log(`cd ${dir}`) } +/** + * `plan` — the /plan-issue planning worktree. Deliberately NOT tied to the + * claim queue and NOT named after any Issue: `PLAN_WORKTREE_BRANCH` is a fixed + * generic branch, so a later `pnpm worktree next` for the next claimable Issue + * never collides with it (branch name nor slot — the plan env registers its + * hashed slot via the marker and `next` bumps around it). Same isolated env + * provisioning as `next`. + */ +const cmdPlan = async (go, skipMigrate) => { + const branch = PLAN_WORKTREE_BRANCH + if (git(['check-ref-format', '--allow-onelevel', branch], { okIfFails: true }) === null) { + die(`Branch plan inválido para refname: ${branch}`) + } + const dir = join(WORKTREES_ROOT, branch) + + git(['fetch', 'origin']) + + const entries = parseWorktreeList(git(['worktree', 'list', '--porcelain'])) + if (entries.some((entry) => resolve(entry.path) === resolve(dir))) { + console.log(`Worktree de planejamento já existe em ${dir} — reutilizando, sem duplicar.`) + } else { + const branchExists = + git(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { okIfFails: true }) !== null + const flag = branchExists ? '-B' : '-b' + git(['worktree', 'add', flag, branch, dir, 'origin/main']) + + console.log('Worktree de planejamento criado:') + console.log(` branch: ${branch}`) + console.log(` path: ${dir}`) + console.log(' origem: origin/main') + } + + const mainRoot = entries[0]?.path + const env = worktreeEnvironment({ + branch, + code: null, + takenSlots: readLiveSlots(entries, dir), + }) + await provision({ dir, branch, env, skipMigrate, mainRoot, purpose: 'plan' }) + + console.log(`\nAmbiente isolado do worktree de planejamento (slot ${env.slot}):`) + console.log(` dev server: http://localhost:${env.devPort} (pnpm dev)`) + console.log(` banco dev: postgresql://teqo:teqo@localhost:5432/${env.devDatabase}`) + console.log(` banco test: postgresql://teqo:teqo@localhost:5432/${env.testDatabase}`) + + if (go) console.log(`cd ${dir}`) +} + /** Generated database names referenced by a worktree's own env files. */ const worktreeDatabaseNamesOf = (dir) => { const names = [] @@ -405,12 +466,20 @@ const { flags, positional } = parseArgs(process.argv.slice(2), new Set()) const subcommand = positional[0] if (!subcommand) { - console.log('Uso: pnpm worktree next [--go] [--no-migrate] | kill [--force]') + console.log( + 'Uso: pnpm worktree next [--go] [--no-migrate] | plan [--go] [--no-migrate] | kill [--force]', + ) console.log(' next [--go] [--no-migrate]') console.log(' cria worktree da próxima Issue claimável (branch -) e provisiona o') console.log(' ambiente isolado: porta de dev + bancos próprios (determinístico do branch);') console.log(' com --go imprime `cd ` no fim (quem aplica o cd: opencode command, ou a') console.log(' função `worktree()` de .agents/shell/worktree.sh); --no-migrate pula migrations') + console.log(' plan [--go] [--no-migrate]') + console.log( + ' cria/reutiliza o worktree de planejamento (branch plans/plan-issue) para rodar a', + ) + console.log(' skill /plan-issue sem ocupar o main nem travar na fila; provisiona o mesmo') + console.log(' ambiente isolado de `next`; nunca colide com o branch - de `next`') console.log(' kill [--force] destrói o worktree em que você está (recusa sujo sem --force) e') console.log(' remove os bancos gerados do worktree (best-effort)') process.exit(1) @@ -418,10 +487,11 @@ if (!subcommand) { try { if (subcommand === 'next') await cmdNext(Boolean(flags.go), Boolean(flags['no-migrate'])) + else if (subcommand === 'plan') await cmdPlan(Boolean(flags.go), Boolean(flags['no-migrate'])) else if (subcommand === 'kill') { if (flags.go) die('`--go` só faz sentido com `next`.') await cmdKill(Boolean(flags.force)) - } else die(`subcomando desconhecido: ${subcommand} (esperado: next | kill)`) + } else die(`subcomando desconhecido: ${subcommand} (esperado: next | plan | kill)`) } catch (error) { if (error?.stderr) die(error.stderr.toString().trim()) die(error?.message ?? String(error)) diff --git a/tests/unit/worktree.unit.spec.ts b/tests/unit/worktree.unit.spec.ts index f11087c5..f5bbbc20 100644 --- a/tests/unit/worktree.unit.spec.ts +++ b/tests/unit/worktree.unit.spec.ts @@ -3,16 +3,20 @@ import { describe, expect, it } from 'vitest' import { - GENERATED_ENV_MARKER, devDatabaseForSlot, devPortForSlot, + GENERATED_ENV_MARKER, hashSlotOf, isGeneratedDatabaseName, numericSlotOfCode, testDatabaseForSlot, worktreeEnvironment, } from '../../scripts/lib/worktree-env.mjs' -import { branchNameForIssue, issueCodeAndSubject } from '../../scripts/lib/worktree.mjs' +import { + branchNameForIssue, + issueCodeAndSubject, + PLAN_WORKTREE_BRANCH, +} from '../../scripts/lib/worktree.mjs' type TestIssue = { number: number @@ -125,6 +129,21 @@ describe('worktreeEnvironment (per-worktree ports and databases)', () => { expect(testDatabaseForSlot(env.slot)).toBe(env.testDatabase) }) + it('plan worktree derives a stable hashed slot and never collides with `next`', () => { + expect(PLAN_WORKTREE_BRANCH).toBe('plans/plan-issue') + expect(PLAN_WORKTREE_BRANCH).not.toMatch(/^[A-Z][A-Za-z0-9]*-/) + const env = worktreeEnvironment({ branch: PLAN_WORKTREE_BRANCH, code: null }) + expect(env.slot).toBe(hashSlotOf(PLAN_WORKTREE_BRANCH)) + expect(numericSlotOfCode('')).toBeNull() + expect(env).toEqual(worktreeEnvironment({ branch: PLAN_WORKTREE_BRANCH, code: null })) + const bumped = worktreeEnvironment({ + branch: PLAN_WORKTREE_BRANCH, + code: null, + takenSlots: new Set([env.slot]), + }) + expect(bumped.slot).toBe(env.slot + 1) + }) + it('caps huge codes into the hash range instead of absurd ports', () => { const env = worktreeEnvironment({ branch: 'C999999-foo', code: 'C999999' }) expect(env.slot).toBeLessThanOrEqual(999)