singularity-forge/tests/repro-worktree-bug/verify-integration.mjs
2026-05-05 14:46:18 +02:00

361 lines
11 KiB
JavaScript

/**
* 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);
}