Closes #1713
This commit is contained in:
parent
049d432c3c
commit
305b426f5f
4 changed files with 87 additions and 4 deletions
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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 ===");
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue