fix: validate worktree .git file and fix metrics toolCall casing (#1713) (#1754)

Closes #1713
This commit is contained in:
TÂCHES 2026-03-21 09:06:25 -06:00 committed by GitHub
parent 049d432c3c
commit 305b426f5f
4 changed files with 87 additions and 4 deletions

View file

@ -164,7 +164,7 @@ export function snapshotUnitMetrics(
// Count tool calls in this message
if (msg.content && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_call") toolCalls++;
if (block.type === "toolCall") toolCalls++;
}
}
} else if (msg.role === "user") {

View file

@ -172,6 +172,29 @@ async function main(): Promise<void> {
teardownAutoWorktree(tempDir, "M005");
}
// ─── #1713: stale worktree directory recovery ─────────────────────
console.log("\n=== #1713: stale worktree directory without .git file ===");
{
// Simulate a crash leaving a stale directory with no .git file.
// createAutoWorktree should detect and remove the stale directory,
// then successfully create a fresh worktree.
const { worktreePath } = await import("../worktree-manager.ts");
const staleDir = worktreePath(tempDir, "M010");
mkdirSync(staleDir, { recursive: true });
// Write a dummy file to prove it's not an empty directory
writeFileSync(join(staleDir, "orphan.txt"), "stale leftover\n");
assertTrue(existsSync(staleDir), "stale directory exists before recovery");
assertTrue(!existsSync(join(staleDir, ".git")), "stale directory has no .git file");
// createAutoWorktree should remove the stale dir and create a real worktree
const recoveredPath = createAutoWorktree(tempDir, "M010");
assertTrue(existsSync(recoveredPath), "worktree created after stale dir recovery");
assertTrue(existsSync(join(recoveredPath, ".git")), "recovered worktree has .git file");
assertTrue(!existsSync(join(recoveredPath, "orphan.txt")), "stale file removed by recovery");
teardownAutoWorktree(tempDir, "M010");
}
// ─── #778: reconcile plan checkboxes on re-attach ─────────────────
console.log("\n=== #778: reconcile plan checkboxes on re-attach ===");
{

View file

@ -333,4 +333,52 @@ test("snapshotUnitMetrics handles simulated idle-watchdog duplicate pattern", ()
resetMetrics();
rmSync(tmpBase, { recursive: true, force: true });
}
});
});
// ── toolCall block counting ─────────────────────────────────────────────────
test("snapshotUnitMetrics counts toolCall blocks correctly (#1713)", () => {
const tmpBase = mkdtempSync(join(tmpdir(), "gsd-metrics-toolcall-"));
mkdirSync(join(tmpBase, ".gsd"), { recursive: true });
try {
resetMetrics();
initMetrics(tmpBase);
const ctx = mockCtx([
{ role: "user", content: "Do something" },
{
role: "assistant",
content: [
{ type: "text", text: "Let me help." },
{ type: "toolCall", name: "Read", input: { file: "foo.ts" } },
{ type: "toolCall", name: "Edit", input: { file: "bar.ts" } },
],
usage: {
input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, totalTokens: 1500,
cost: 0.01,
},
},
{
role: "assistant",
content: [
{ type: "toolCall", name: "Bash", input: { command: "ls" } },
{ type: "text", text: "All done." },
],
usage: {
input: 800, output: 300, cacheRead: 0, cacheWrite: 0, totalTokens: 1100,
cost: 0.008,
},
},
]);
const unit = snapshotUnitMetrics(ctx, "execute-task", "M001/S01/T01", Date.now() - 3000, "test-model");
assert.ok(unit);
assert.equal(unit!.toolCalls, 3, "should count 3 toolCall blocks across 2 assistant messages");
assert.equal(unit!.assistantMessages, 2);
assert.equal(unit!.userMessages, 1);
} finally {
resetMetrics();
rmSync(tmpBase, { recursive: true, force: true });
}
});

View file

@ -15,7 +15,7 @@
* 4. remove() git worktree remove + branch cleanup
*/
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
import { join, resolve, sep } from "node:path";
import { GSDError, GSD_PARSE_ERROR, GSD_STALE_STATE, GSD_LOCK_HELD, GSD_GIT_ERROR, GSD_MERGE_CONFLICT } from "./errors.js";
import {
@ -129,7 +129,19 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
const branch = opts.branch ?? worktreeBranchName(name);
if (existsSync(wtPath)) {
throw new GSDError(GSD_STALE_STATE, `Worktree "${name}" already exists at ${wtPath}`);
// A valid git worktree has a .git file (not directory) containing a
// "gitdir:" pointer. If the directory exists but has no .git file,
// it is a stale leftover from a prior crash — remove it so a fresh
// worktree can be created in its place.
const gitFilePath = join(wtPath, ".git");
if (!existsSync(gitFilePath)) {
console.error(
`[GSD] Removing stale worktree directory (no .git file): ${wtPath}`,
);
rmSync(wtPath, { recursive: true, force: true });
} else {
throw new GSDError(GSD_STALE_STATE, `Worktree "${name}" already exists at ${wtPath}`);
}
}
// Ensure the .gsd/worktrees/ directory exists