test: Investigated R102 symlink dedup: canonicalizePath already exists…

SF-Task: S01/T07
This commit is contained in:
Mikael Hugo 2026-05-02 12:00:56 +02:00
parent 3915dfda3a
commit e0fd2076d3
8 changed files with 68 additions and 22 deletions

View file

@ -483,6 +483,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
if (!eventFilter || eventFilter.has("cost_update")) {
output(costUpdate);
}
if (process.env.PI_TOKEN_TELEMETRY === "1") {
rawStderrWrite(
`[PI_TOKEN] input=${stats.tokens.input} output=${stats.tokens.output} cache_read=${stats.tokens.cacheRead} cache_write=${stats.tokens.cacheWrite} cost=$${stats.cost.toFixed(4)}\n`,
);
}
}
// execution_complete on agent_end

View file

@ -262,26 +262,36 @@ describe("git-service", async () => {
assert.deepStrictEqual(
RUNTIME_EXCLUSION_PATHS.length,
15,
"exactly 15 runtime exclusion paths",
25,
"exactly 25 runtime exclusion paths",
);
const expectedPaths = [
".sf/activity/",
".sf/audit/",
".sf/exec/",
".sf/forensics/",
".sf/journal/",
".sf/model-benchmarks/",
".sf/parallel/",
".sf/reports/",
".sf/runtime/",
".sf/worktrees/",
".sf/parallel/",
".sf/auto.lock",
".sf/metrics.json",
".sf/completed-units*.json",
".sf/state-manifest.json",
".sf/STATE.md",
".sf/sf.db*",
".sf/journal/",
".sf/doctor-history.jsonl",
".sf/event-log.jsonl",
".sf/notifications.jsonl",
".sf/routing-history.json",
".sf/self-feedback.jsonl",
".sf/repo-meta.json",
".sf/DISCUSSION-MANIFEST.json",
".sf/milestones/**/*-CONTINUE.md",
".sf/milestones/**/continue.md",
];
assert.deepStrictEqual(
@ -1602,25 +1612,40 @@ describe("git-service", async () => {
rmSync(repo, { recursive: true, force: true });
});
// ─── ensureGitignore: always adds .sf to gitignore ──────────────────
// ─── ensureGitignore: excludes symlinked .sf per-clone ───────────────
test("ensureGitignore: adds .sf entry", async () => {
test("ensureGitignore: adds symlinked .sf to git/info/exclude", async () => {
const { ensureGitignore } = await import("../../gitignore.ts");
const repo = mkdtempSync(join(tmpdir(), "sf-gitignore-external-state-"));
const externalState = mkdtempSync(join(tmpdir(), "sf-gitignore-state-"));
// Should add .sf to gitignore (external state dir is a symlink)
runGit(repo, ["init", "-b", "main"]);
symlinkSync(externalState, join(repo, ".sf"));
// Symlinked external state must be ignored per-clone so sf does not dirty
// a user-managed .gitignore while still hiding the opaque .sf symlink.
const modified = ensureGitignore(repo);
assert.ok(modified, "ensureGitignore: gitignore was modified");
assert.ok(modified, "ensureGitignore: ignore files were modified");
const { readFileSync } = await import("node:fs");
const content = readFileSync(join(repo, ".gitignore"), "utf-8");
const lines = content
const exclude = readFileSync(join(repo, ".git", "info", "exclude"), "utf-8");
const excludeLines = exclude
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"));
assert.ok(
lines.includes(".sf"),
"ensureGitignore: .gitignore contains .sf",
excludeLines.includes(".sf"),
"ensureGitignore: git/info/exclude contains .sf",
);
const gitignore = readFileSync(join(repo, ".gitignore"), "utf-8");
const gitignoreLines = gitignore
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"));
assert.ok(
!gitignoreLines.includes(".sf"),
"ensureGitignore: committed .gitignore does not contain .sf",
);
// Idempotent — calling again doesn't add duplicates
@ -1628,6 +1653,7 @@ describe("git-service", async () => {
assert.ok(!modified2, "ensureGitignore: second call is idempotent");
rmSync(repo, { recursive: true, force: true });
rmSync(externalState, { recursive: true, force: true });
});
// ─── nativeAddAllWithExclusions: symlinked .sf fallback ───────────────

View file

@ -24,7 +24,7 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe } from 'vitest';
import { describe, test } from "vitest";
import { createAutoWorktree } from "../auto-worktree.ts";
import {
closeDatabase,
@ -56,7 +56,8 @@ function createTempRepo(): string {
return dir;
}
describe("worktree-db-integration", async () => {
describe("worktree-db-integration", () => {
test("copies and reconciles worktree database state", async () => {
const savedCwd = process.cwd();
const tempDirs: string[] = [];
@ -245,4 +246,5 @@ describe("worktree-db-integration", async () => {
}
}
}
});
});

View file

@ -24,7 +24,7 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe } from 'vitest';
import { describe, test } from "vitest";
import { syncProjectRootToWorktree } from "../auto-worktree.ts";
function createBase(name: string): string {
@ -37,7 +37,8 @@ function cleanup(base: string): void {
rmSync(base, { recursive: true, force: true });
}
describe("worktree-db-respawn-truncation (#2815)", async () => {
describe("worktree-db-respawn-truncation (#2815)", () => {
test("preserves populated worktree databases while syncing artifacts", async () => {
// ─── 1. Non-empty worktree sf.db preserved after sync ───────────────
console.log(
"\n=== 1. non-empty worktree sf.db preserved after sync (#2815) ===",
@ -257,4 +258,5 @@ describe("worktree-db-respawn-truncation (#2815)", async () => {
cleanup(wtBase);
}
}
});
});

View file

@ -16,7 +16,7 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe } from 'vitest';
import { describe, test } from "vitest";
import {
formatWorktreeStatusLine,
getWorktreeHealth,
@ -43,7 +43,8 @@ function createBaseRepo(): string {
return dir;
}
describe("worktree-health", async () => {
describe("worktree-health", () => {
test("classifies worktree health states and formats status lines", async () => {
// Skip all tests on Windows — git worktree path resolution issues
if (process.platform === "win32") {
console.log("(all worktree-health tests skipped on Windows)");
@ -228,4 +229,5 @@ describe("worktree-health", async () => {
}
}
}
});
});

View file

@ -20,7 +20,7 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe } from 'vitest';
import { describe, test } from "vitest";
import { _clearSfRootCache } from "../paths.ts";
import { deriveState } from "../state.ts";
import {
@ -87,7 +87,8 @@ writeFileSync(
run("git add .", base);
run('git commit -m "chore: init"', base);
describe("worktree-integration", async () => {
describe("worktree-integration", () => {
test("runs the full worktree lifecycle with namespaced slice branches", async () => {
// Isolate from user's global preferences (which may have git.main_branch set).
// Reset caches so getService() creates a fresh instance with empty preferences.
const originalHome = process.env.HOME;
@ -310,4 +311,5 @@ describe("worktree-integration", async () => {
_clearSfRootCache();
_resetServiceCache();
rmSync(fakeHome, { recursive: true, force: true });
});
});

View file

@ -24,7 +24,7 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe } from 'vitest';
import { describe, test } from "vitest";
import {
createWorktree,
listWorktrees,
@ -69,7 +69,8 @@ writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
run("git add .", base);
run('git commit -m "init"', base);
describe("worktree-symlink-removal", async () => {
describe("worktree-symlink-removal", () => {
test("removes the git-registered worktree when .sf is a symlink", async () => {
console.log("\n=== #1852: removeWorktree with symlinked .sf/ ===");
// Create a worktree — git will resolve the symlink and register
@ -146,4 +147,5 @@ describe("worktree-symlink-removal", async () => {
// Cleanup
rmSync(base, { recursive: true, force: true });
rmSync(externalState, { recursive: true, force: true });
});
});

View file

@ -26,6 +26,11 @@ const manageRoute = await import(
const gitRoute = await import("../../../web/app/api/git/route.ts");
const { AuthStorage } = await import("@singularity-forge/pi-coding-agent");
afterEach(async () => {
await bridge.resetBridgeServiceForTests();
onboarding.resetOnboardingServiceForTests();
});
class FakeRpcChild extends EventEmitter {
stdin = new PassThrough();
stdout = new PassThrough();