Files
PaperClipAI/server/src/__tests__/git-workspace-restoration-diagnostic.test.ts
T
131b566ca4 fix(workspaces): report safe Git restore diagnostics (#19736)
Return the restore stage and failure category with the existing HTTP409 rejection. Preserve the unsaved comment and editable draft. Keep nested saved-commit and clone-timeout diagnostics through preparation wrappers, and distinguish local Git file errors from authentication failures. Retain only safe process flags privately; no raw command cause is exposed.

All seven affected local suites pass (288 tests) and the layered server typecheck passes on supported Node25.6.1. Reviewed head 022eb7c2eb has Greptile Apex5/5, zero unresolved findings and all applicable CI passing (55 successful checks and4 intentional skips).

Co-Authored-By: Sentry Bot <sentry-bot@paperclip.invalid>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 21:33:59 -08:00

60 lines
4.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { classifyWorkspaceRestorationError, GitCacheRestorationFailure } from "../services/git-workspace-restoration.js";
import { workspaceReopenDiagnostic } from "../services/execution-workspaces.js";
import { GitConnectionFailureError } from "../services/git-connection-failure.js";
const gitError = (stderr: string, extra: Record<string, unknown> = {}) =>
Object.assign(new Error(`Command failed: git fetch\n${stderr}`), { code: 128, stderr, ...extra });
describe("workspace restoration diagnostic", () => {
it.each([
["fatal: Authentication failed for 'https://user:sk-secret@example.test/repo.git/'", "auth_rejected"],
["fatal: could not read Username for 'https://github.com': terminal prompts disabled", "auth_rejected"],
["fatal: remote error: upload-pack: not our ref 9a204a56", "commit_unavailable"],
["fatal: couldn't find remote ref refs/heads/gone", "commit_unavailable"],
["fatal: unable to access 'https://example.test/repo.git/': Could not resolve host: example.test", "remote_unavailable"],
["fatal: '/srv/repo.git' does not appear to be a git repository", "remote_unavailable"],
["fatal: bad revision", "git_command"],
["error: cannot open '.git/FETCH_HEAD': Permission denied", "filesystem"],
["fatal: unable to access '/private/repository/.git/config': Permission denied", "filesystem"],
["git@example.test: Permission denied (publickey).", "auth_rejected"],
["git@example.test: Permission denied (publickey).\nerror: cannot open '.git/FETCH_HEAD': Permission denied", "filesystem"],
])("classifies %s", (stderr, kind) => {
expect(classifyWorkspaceRestorationError(gitError(stderr))).toBe(kind);
});
it("classifies timeouts, typed connection failures, filesystem errors and checks", () => {
expect(classifyWorkspaceRestorationError(gitError("", { killed: true, signal: "SIGTERM" }))).toBe("timeout");
expect(classifyWorkspaceRestorationError(new GitConnectionFailureError("clone failed",
{ schemaVersion: 1, provider: "git", operation: "clone", reason: "authentication_failed" }))).toBe("auth_rejected");
expect(classifyWorkspaceRestorationError(new GitConnectionFailureError("clone failed",
{ schemaVersion: 1, provider: "git", operation: "clone", reason: "dns_failure" }))).toBe("remote_unavailable");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("EACCES: permission denied, open '/x'"), { code: "EACCES" })))
.toBe("filesystem");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("ENOSPC: no space left"), { code: "ENOSPC" }))).toBe("filesystem");
expect(classifyWorkspaceRestorationError(new GitCacheRestorationFailure())).toBe("verification");
expect(classifyWorkspaceRestorationError("not an error")).toBe("unknown");
});
it("keeps the innermost restoration step and never carries Git output", () => {
const inner = new GitCacheRestorationFailure({ step: "saved_commit", kind: "commit_unavailable" });
expect(workspaceReopenDiagnostic("source_verify", inner)).toEqual({ step: "saved_commit", kind: "commit_unavailable" });
const diagnostic = workspaceReopenDiagnostic("managed_base", gitError("fatal: Authentication failed for 'https://user:sk-secret@example.test/r.git/'"));
expect(diagnostic).toEqual({ step: "managed_base", kind: "auth_rejected" });
expect(JSON.stringify(diagnostic)).not.toContain("sk-secret");
expect(workspaceReopenDiagnostic("publish", undefined)).toEqual({ step: "publish", kind: "unknown" });
expect(inner.message).toBe("The saved workspace commit could not be restored. Check repository access and retry.");
});
it("bounds command text and distinguishes network failures from filesystem failures", () => {
const text = "x".repeat(16_384) + "Authentication failed for a private repository";
expect(classifyWorkspaceRestorationError(gitError(text))).toBe("git_command");
expect(classifyWorkspaceRestorationError(gitError("", { stderr: Buffer.from(text) }))).toBe("git_command");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("network failure"), { code: "ECONNRESET" }))).toBe("remote_unavailable");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("resolver failure"), { code: "EAI_AGAIN" }))).toBe("remote_unavailable");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("timeout"), { name: "TimeoutError" }))).toBe("timeout");
expect(classifyWorkspaceRestorationError(gitError("", { code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER", killed: true }))).toBe("git_command");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("unknown"), { code: "EUNRECOGNIZED" }))).toBe("unknown");
});
});