From e0fd2076d3aea76ffdd0aa429b7d9636786aa3d6 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 12:00:56 +0200 Subject: [PATCH] =?UTF-8?q?test:=20Investigated=20R102=20symlink=20dedup:?= =?UTF-8?q?=20canonicalizePath=20already=20exists=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SF-Task: S01/T07 --- .../pi-coding-agent/src/modes/rpc/rpc-mode.ts | 5 ++ .../sf/tests/integration/git-service.test.ts | 50 ++++++++++++++----- .../sf/tests/worktree-db-integration.test.ts | 6 ++- .../worktree-db-respawn-truncation.test.ts | 6 ++- .../sf/tests/worktree-health.test.ts | 6 ++- .../sf/tests/worktree-integration.test.ts | 6 ++- .../sf/tests/worktree-symlink-removal.test.ts | 6 ++- .../web-session-parity-contract.test.ts | 5 ++ 8 files changed, 68 insertions(+), 22 deletions(-) diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts index c01d04088..3c138ca70 100644 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts @@ -483,6 +483,11 @@ export async function runRpcMode(session: AgentSession): Promise { 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 diff --git a/src/resources/extensions/sf/tests/integration/git-service.test.ts b/src/resources/extensions/sf/tests/integration/git-service.test.ts index aa0262229..4bcdc92aa 100644 --- a/src/resources/extensions/sf/tests/integration/git-service.test.ts +++ b/src/resources/extensions/sf/tests/integration/git-service.test.ts @@ -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 ─────────────── diff --git a/src/resources/extensions/sf/tests/worktree-db-integration.test.ts b/src/resources/extensions/sf/tests/worktree-db-integration.test.ts index b334e077c..d9ea4ddbe 100644 --- a/src/resources/extensions/sf/tests/worktree-db-integration.test.ts +++ b/src/resources/extensions/sf/tests/worktree-db-integration.test.ts @@ -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 () => { } } } + }); }); diff --git a/src/resources/extensions/sf/tests/worktree-db-respawn-truncation.test.ts b/src/resources/extensions/sf/tests/worktree-db-respawn-truncation.test.ts index 320e73d13..55820c694 100644 --- a/src/resources/extensions/sf/tests/worktree-db-respawn-truncation.test.ts +++ b/src/resources/extensions/sf/tests/worktree-db-respawn-truncation.test.ts @@ -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); } } + }); }); diff --git a/src/resources/extensions/sf/tests/worktree-health.test.ts b/src/resources/extensions/sf/tests/worktree-health.test.ts index 814964b0d..399455cdc 100644 --- a/src/resources/extensions/sf/tests/worktree-health.test.ts +++ b/src/resources/extensions/sf/tests/worktree-health.test.ts @@ -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 () => { } } } + }); }); diff --git a/src/resources/extensions/sf/tests/worktree-integration.test.ts b/src/resources/extensions/sf/tests/worktree-integration.test.ts index 4996f7b9b..820411218 100644 --- a/src/resources/extensions/sf/tests/worktree-integration.test.ts +++ b/src/resources/extensions/sf/tests/worktree-integration.test.ts @@ -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 }); + }); }); diff --git a/src/resources/extensions/sf/tests/worktree-symlink-removal.test.ts b/src/resources/extensions/sf/tests/worktree-symlink-removal.test.ts index a0017d22a..4258c9fdb 100644 --- a/src/resources/extensions/sf/tests/worktree-symlink-removal.test.ts +++ b/src/resources/extensions/sf/tests/worktree-symlink-removal.test.ts @@ -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 }); + }); }); diff --git a/src/tests/integration/web-session-parity-contract.test.ts b/src/tests/integration/web-session-parity-contract.test.ts index 83951a69e..a2a4fb503 100644 --- a/src/tests/integration/web-session-parity-contract.test.ts +++ b/src/tests/integration/web-session-parity-contract.test.ts @@ -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();