/** * Integration verification: parallel directory writes go to the correct .sf * * This verifies that after the fix, when code resolves paths inside a worktree * with symlinked .sf, writes target the project-level .sf (through symlink) * rather than the user-level ~/.sf. * * Covers: * 1. resolveProjectRoot() returns the real project, not ~ * 2. sfRoot() from the resolved project root finds project .sf, not ~/.sf * 3. The parallel/ directory would be created under project .sf * 4. session-status writes target the correct location * 5. orchestrator.json would be written to project .sf * 6. assertSafeDirectory blocks ~ as a project root */ import { execSync } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, symlinkSync, writeFileSync, } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join, resolve } from "node:path"; // ── Fixed functions (from worktree.ts after fix) ───────────────────────── function findWorktreeSegment(normalizedPath) { const directMarker = "/.sf/worktrees/"; const idx = normalizedPath.indexOf(directMarker); if (idx !== -1) { return { sfIdx: idx, afterWorktrees: idx + directMarker.length }; } const symlinkRe = /\/\.sf\/projects\/[a-f0-9]+\/worktrees\//; const match = normalizedPath.match(symlinkRe); if (match && match.index !== undefined) { return { sfIdx: match.index, afterWorktrees: match.index + match[0].length, }; } return null; } function resolveProjectRootFromGitFile(worktreePath) { try { let dir = worktreePath; for (let i = 0; i < 10; i++) { const gitPath = join(dir, ".git"); if (existsSync(gitPath)) { const content = readFileSync(gitPath, "utf8").trim(); if (content.startsWith("gitdir: ")) { const gitDir = resolve(dir, content.slice(8)); const dotGitDir = resolve(gitDir, "..", ".."); if ( dotGitDir.endsWith(".git") || dotGitDir.endsWith(".git/") || dotGitDir.endsWith(".git\\") ) { return resolve(dotGitDir, ".."); } const commonDirPath = join(gitDir, "commondir"); if (existsSync(commonDirPath)) { const commonDir = readFileSync(commonDirPath, "utf8").trim(); const resolvedCommonDir = resolve(gitDir, commonDir); return resolve(resolvedCommonDir, ".."); } } break; } const parent = resolve(dir, ".."); if (parent === dir) break; dir = parent; } } catch {} return null; } function normalizePathForCompare(path) { let normalized; try { normalized = realpathSync(path); } catch { normalized = resolve(path); } const slashed = normalized.replaceAll("\\", "/"); const trimmed = slashed.replace(/\/+$/, ""); return trimmed || "/"; } function resolveProjectRoot(basePath) { // Layer 1: If the coordinator passed the real project root, use it. if (process.env.SF_PROJECT_ROOT || process.env.SF_PROJECT_ROOT) { return process.env.SF_PROJECT_ROOT || process.env.SF_PROJECT_ROOT; } const normalizedPath = basePath.replaceAll("\\", "/"); const seg = findWorktreeSegment(normalizedPath); if (!seg) return basePath; const sepChar = basePath.includes("\\") ? "\\" : "/"; const sfMarker = `${sepChar}.sf${sepChar}`; const sfIdx = basePath.indexOf(sfMarker); const candidate = sfIdx !== -1 ? basePath.slice(0, sfIdx) : basePath.slice(0, seg.sfIdx); const sfHome = normalizePathForCompare( process.env.SF_HOME || process.env.SF_HOME || join(homedir(), ".sf"), ); const candidateSfPath = normalizePathForCompare(join(candidate, ".sf")); if (candidateSfPath === sfHome || candidateSfPath.startsWith(sfHome + "/")) { const realRoot = resolveProjectRootFromGitFile(basePath); if (realRoot) return realRoot; return basePath; } return candidate; } // Simplified sfRoot — matches paths.ts probeSfRoot logic function sfRoot(basePath) { const local = join(basePath, ".sf"); if (existsSync(local)) return local; return local; // fallback } // Simplified validateDirectory — matches validate-directory.ts function validateDirectory(dirPath) { let resolved; try { resolved = realpathSync(resolve(dirPath)); } catch { resolved = resolve(dirPath); } let normalized = resolved.replace(/[/\\]+$/, ""); if (normalized === "") normalized = "/"; let resolvedHome; try { resolvedHome = realpathSync(resolve(homedir())).replace(/[/\\]+$/, ""); } catch { resolvedHome = resolve(homedir()).replace(/[/\\]+$/, ""); } if (normalized === resolvedHome) { return { safe: false, severity: "blocked", reason: `Refusing to run in home directory: ${normalized}`, }; } return { safe: true, severity: "ok" }; } // ── Setup ──────────────────────────────────────────────────────────────── const HASH = "abc123def456"; const TEST_ROOT = mkdtempSync(join(tmpdir(), "sf-verify-integration-")); const USER_SF = process.env.SF_HOME || process.env.SF_HOME || join(TEST_ROOT, ".sf"); const USER_HOME = homedir(); const PROJECT_SF_STORAGE = `${USER_SF}/projects/${HASH}`; const PROJECT_DIR = mkdtempSync(join(tmpdir(), "myproject-")); const PROJECT_SF_LINK = `${PROJECT_DIR}/.sf`; const PROJECT_REAL = normalizePathForCompare(PROJECT_DIR); let PROJECT_STORAGE_REAL = ""; process.env.SF_HOME = USER_SF; process.env.SF_HOME = USER_SF; console.log("=== Setup ===\n"); mkdirSync(`${PROJECT_SF_STORAGE}/worktrees`, { recursive: true }); mkdirSync(`${PROJECT_SF_STORAGE}/milestones`, { recursive: true }); mkdirSync(PROJECT_DIR, { recursive: true }); symlinkSync(PROJECT_SF_STORAGE, PROJECT_SF_LINK); PROJECT_STORAGE_REAL = normalizePathForCompare(PROJECT_SF_STORAGE); execSync("git init -b main", { cwd: PROJECT_DIR, stdio: "pipe" }); execSync('git config user.name "Test"', { cwd: PROJECT_DIR, stdio: "pipe" }); execSync('git config user.email "test@test.com"', { cwd: PROJECT_DIR, stdio: "pipe", }); writeFileSync(join(PROJECT_DIR, "README.md"), "hello\n"); execSync("git add -A && git commit -m init", { cwd: PROJECT_DIR, stdio: "pipe", }); execSync("git worktree add .sf/worktrees/M001 -b worktree/M001", { cwd: PROJECT_DIR, stdio: "pipe", }); console.log("Created project with symlinked .sf and real git worktree\n"); let passed = 0; let failed = 0; function test(name, actual, expected) { if (actual === expected) { console.log(` ✅ ${name}`); passed++; } else { console.log( ` ❌ ${name}\n Expected: ${expected}\n Got: ${actual}`, ); failed++; } } // ── Simulate worker environment ────────────────────────────────────────── process.chdir(`${PROJECT_DIR}/.sf/worktrees/M001`); const workerCwd = process.cwd(); // Resolves symlinks → /root/.sf/projects/.../worktrees/M001 console.log("=== Test 1: resolveProjectRoot returns real project ===\n"); console.log(` Worker cwd (resolved): ${workerCwd}`); const projectRoot = resolveProjectRoot(workerCwd); console.log(` Resolved project root: ${projectRoot}`); test("resolveProjectRoot returns real project root", projectRoot, PROJECT_REAL); test( "resolveProjectRoot does NOT return home dir", projectRoot !== USER_HOME, true, ); console.log("\n=== Test 2: sfRoot finds project .sf ===\n"); const sfDir = sfRoot(projectRoot); console.log(` sfRoot result: ${sfDir}`); test("sfRoot points to project .sf", sfDir, `${PROJECT_REAL}/.sf`); // Verify it's a symlink to the right place const sfDirReal = realpathSync(sfDir); console.log(` sfRoot resolves to: ${sfDirReal}`); test("sfRoot resolves to project storage", sfDirReal, PROJECT_STORAGE_REAL); test( "sfRoot does NOT resolve to user-level ~/.sf", sfDirReal !== USER_SF, true, ); console.log("\n=== Test 3: parallel/ directory targets project .sf ===\n"); const parallelDir = join(sfDir, "parallel"); console.log(` Parallel dir would be: ${parallelDir}`); const parallelReal = join(sfDirReal, "parallel"); console.log(` Resolves physically to: ${parallelReal}`); test( "parallel dir is under project .sf", parallelDir.startsWith(PROJECT_REAL), true, ); test( "parallel dir is NOT under ~/.sf root", !parallelDir.startsWith(USER_SF) || parallelDir.startsWith(`${USER_SF}/projects/`), true, ); // Actually create it and verify mkdirSync(parallelDir, { recursive: true }); test("parallel dir was created", existsSync(parallelDir), true); test( "parallel dir physically exists in project storage", existsSync(parallelReal), true, ); // Write a session status file const statusFile = join(parallelDir, "M001.status.json"); writeFileSync( statusFile, JSON.stringify({ milestoneId: "M001", pid: 12345, state: "running" }), ); test( "session status file written to project parallel/", existsSync(statusFile), true, ); console.log("\n=== Test 4: orchestrator.json targets project .sf ===\n"); const orchestratorPath = join(sfDir, "orchestrator.json"); console.log(` orchestrator.json would be at: ${orchestratorPath}`); writeFileSync(orchestratorPath, JSON.stringify({ active: true })); test( "orchestrator.json written to project .sf", existsSync(orchestratorPath), true, ); // Verify nothing leaked to user-level ~/.sf root const userParallelDir = join(USER_SF, "parallel"); const userOrchestratorPath = join(USER_SF, "orchestrator.json"); test( "NO parallel/ dir at user-level ~/.sf root", !existsSync(userParallelDir), true, ); test( "NO orchestrator.json at user-level ~/.sf root", !existsSync(userOrchestratorPath), true, ); console.log("\n=== Test 5: validateDirectory blocks ~ as project root ===\n"); const homeValidation = validateDirectory(USER_HOME); test("validateDirectory blocks home dir", homeValidation.safe, false); test( "validateDirectory blocks with 'blocked' severity", homeValidation.severity, "blocked", ); const projectValidation = validateDirectory(PROJECT_DIR); test("validateDirectory allows project dir", projectValidation.safe, true); console.log("\n=== Test 6: SF_PROJECT_ROOT env var path ===\n"); process.env.SF_PROJECT_ROOT = PROJECT_DIR; const envResult = resolveProjectRoot(workerCwd); test("SF_PROJECT_ROOT short-circuits resolution", envResult, PROJECT_DIR); delete process.env.SF_PROJECT_ROOT; delete process.env.SF_PROJECT_ROOT; console.log("\n=== Test 7: Non-worktree paths unaffected ===\n"); test( "Regular project path unchanged", resolveProjectRoot("/some/project"), "/some/project", ); test( "Direct worktree layout still works", resolveProjectRoot("/foo/.sf/worktrees/M001"), "/foo", ); // ── Summary ────────────────────────────────────────────────────────────── console.log(`\n${"=".repeat(60)}`); console.log(`\nResults: ${passed} passed, ${failed} failed`); if (failed > 0) { console.log("\n🔴 INTEGRATION VERIFICATION FAILED"); process.exit(1); } else { console.log("\n✅ ALL INTEGRATION TESTS PASSED"); console.log(" - resolveProjectRoot returns real project, not ~"); console.log(" - sfRoot finds project .sf through symlink"); console.log(" - parallel/ dir created in project .sf, not ~/.sf"); console.log(" - session status writes land in correct location"); console.log(" - orchestrator.json lands in correct location"); console.log(" - validateDirectory blocks ~ as fallback safety net"); console.log(" - SF_PROJECT_ROOT env var works as primary layer"); console.log(" - Non-worktree paths are unaffected by the fix"); process.exit(0); }