/** * 工作樹路徑的推導。 * * 這是一個純函式,而且是整套工作樹機制的地基:任何流程都要能從「這是哪顆工作包」 * 算出「它的工作樹在哪」,不查表、不讀狀態檔,換一台機器算出來也要一樣。 * 算錯的代價是找不到既有的工作樹而重建一棵,或兩顆工作包撞進同一個目錄。 * * 這一支直接 import lib,是本專案第二個這麼做的測試:大小寫與空白的變體沒辦法 * 穿過 CLI 的 kebab 驗證送進去,而表格驅動正是這種純規則最划算的驗法。 * 最後一則測試把它與 CLI 實際印出的路徑釘在一起,避免兩邊各自為政。 */ import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { worktreePath } from '../scripts/lib.js'; import { runScript, tmpRoot } from './helpers/run-script.js'; import { makeTempRepoWithRemote } from './helpers/temp-repo.js'; /** 推導只看輸入,不看環境;家目錄則由 TEA_SDLC_HOME 決定,測試固定一個值好比對 */ const HOME = '/home/tester/.tea-sdlc'; const derive = (repo, branch) => { process.env.TEA_SDLC_HOME = HOME; try { return worktreePath(repo, branch); } finally { delete process.env.TEA_SDLC_HOME; } }; test('路徑長在固定的家底下,目錄名是 12 碼十六進位', () => { const path = derive('plugins/tea-sdlc', 'feat/worktree-branch-prep/main'); assert.match(path, new RegExp(`^${HOME}/worktrees/[0-9a-f]{12}$`)); }); // ── 同一顆工作包只能推導出一條路徑 ───────────────────────────────── const SAME = [ { name: '大小寫不同', a: ['plugins/tea-sdlc', 'feat/mine/main'], b: ['Plugins/Tea-SDLC', 'Feat/Mine/Main'], }, { name: '前後有空白', a: ['plugins/tea-sdlc', 'feat/mine/main'], b: [' plugins/tea-sdlc ', ' feat/mine/main '], }, { name: '斜線兩側有空白', a: ['plugins/tea-sdlc', 'feat/mine/main'], b: ['plugins / tea-sdlc', 'feat / mine / main'], }, ]; for (const { name, a, b } of SAME) { test(`同一顆工作包:${name}的變體推導出同一條路徑`, () => { assert.equal(derive(...a), derive(...b), '正規化沒做,同一棵工作樹會被推導成兩個目錄'); }); } // ── 不同的工作包不能撞在一起 ─────────────────────────────────────── const DIFFERENT = [ { name: '分支名的斜線位置不同:feat/a-b/main 與 feat/a/b/main', a: ['plugins/tea-sdlc', 'feat/a-b/main'], b: ['plugins/tea-sdlc', 'feat/a/b/main'], }, { name: '同名分支在不同的 repo 上', a: ['plugins/tea-sdlc', 'feat/mine/main'], b: ['plugins/別的專案', 'feat/mine/main'], }, { name: '同名 repo 在不同的 owner 底下', a: ['plugins/tea-sdlc', 'feat/mine/main'], b: ['someone/tea-sdlc', 'feat/mine/main'], }, { name: '同一個 repo 的不同分支', a: ['plugins/tea-sdlc', 'feat/mine/main'], b: ['plugins/tea-sdlc', 'feat/yours/main'], }, ]; for (const { name, a, b } of DIFFERENT) { test(`不同的工作包:${name},不得撞名`, () => { assert.notEqual(derive(...a), derive(...b)); }); } test('分支名含斜線時路徑仍是單層目錄,不會長出巢狀結構', () => { const path = derive('plugins/tea-sdlc', 'feat/a/b/c/main'); assert.equal(path.startsWith(`${HOME}/worktrees/`), true); assert.equal(path.slice(`${HOME}/worktrees/`.length).includes('/'), false); }); test('換一台機器只有家目錄會變,目錄名不變', () => { process.env.TEA_SDLC_HOME = '/somewhere/else/.tea-sdlc'; const elsewhere = worktreePath('plugins/tea-sdlc', 'feat/mine/main'); delete process.env.TEA_SDLC_HOME; const here = derive('plugins/tea-sdlc', 'feat/mine/main'); assert.equal(elsewhere.split('/').at(-1), here.split('/').at(-1)); }); // ── 與 CLI 釘在一起 ─────────────────────────────────────────────── test('branch-prep 印出的工作樹路徑就是這個函式算出來的那一條', async (t) => { const repo = makeTempRepoWithRemote(); t.after(() => repo.cleanup()); mkdirSync(tmpRoot, { recursive: true }); const home = mkdtempSync(join(tmpRoot, 'home-')); t.after(() => rmSync(home, { recursive: true, force: true })); const { json } = await runScript( 'branch-prep.js', ['--repo', 'plugins/tea-sdlc', '--path', repo.dir, '--source', 'master', '--type', 'feat', '--slug', 'mine', '--dry-run'], { env: { TEA_SDLC_HOME: home } }, ); process.env.TEA_SDLC_HOME = home; const expected = worktreePath('plugins/tea-sdlc', 'feat/mine/main'); delete process.env.TEA_SDLC_HOME; assert.equal(json.data.worktree, expected); });