fix(host-service): classify a missing project directory as NOT_FOUND in workspaces.create

Merged
SA
3mo ago
+93-14

Problem

Creating a workspace for a project whose repository folder no longer exists at its registered path (moved or deleted outside the app) fails with simple-git's construct error ("Cannot use simple-git on a directory that does not exist") thrown from the create mutation's ctx.git(localProject.repoPath) call. It escapes as INTERNAL_SERVER_ERROR, so the user sees an opaque internal error instead of an actionable "project directory is gone" message, and every retry is a fresh 500 — the latest events show one user retrying repeatedly within minutes against a condition that is deterministic and permanent until they re-point or restore the folder. 13 events in the last 24h, 233 lifetime, regressed on 1.23.0.

User-visible harm: a user whose project folder was moved/deleted outside Superset gets a raw internal error on every workspace-create attempt, with no hint that the project path is the problem.

Reach: the create path is unchanged on main; this ships in the next host-service/desktop release and reaches every user creating workspaces on it. The condition is a routine lifecycle state, so it will keep firing until classified.

Why code (vs. archiving / filtering): this issue already regressed once — #6495 classified the same construct error on the git.getStatus path, and the issue came back through the uncovered create path. Archiving would just repeat that cycle, and Sentry-side suppression is banned by the repo's classification contract (#6164). The classification helper for exactly this state already exists (requireProjectRepoPath, added in #6316); the create mutation was simply never given it.

Root cause

PR #6316 added requireProjectRepoPath — an ENOENT-safe statSync(..., { throwIfNoEntry: false })?.isDirectory() precheck that throws TRPCError NOT_FOUND with cause: { kind: "PROJECT_DIR_MISSING" } — and applied it to search-branches, list-project-worktrees and adopt, but not to workspaces.create or createEnqueued. The reporting middleware reports on INTERNAL_SERVER_ERROR alone, and createEnqueued drives the real create through a server-side caller whose middleware chain reports the escaping construct error even though the enqueue wrapper catches it afterwards.

Fix

Structural precheck only — no message matcher, matching the shape #6316 and #6634 established:

  • In workspaces.create, resolve the path through requireProjectRepoPath(localProject) once, immediately after requireLocalProject, and use the validated path for everything downstream that consumed localProject.repoPath (ensureMainWorkspace, ctx.git, the worker base-ref fetcher, fetchPrMetadata, both listBranchNames calls, applyGeneratedWorkspaceNames).
  • In createEnqueued's cheap-validation layer, the same one-line guard, so a missing directory rejects synchronously with NOT_FOUND instead of enqueueing a create that is guaranteed to fail and settling with an error event. That layer already throws synchronous non-500s today (BAD_REQUEST for a missing id, PRECONDITION_FAILED for an unregistered project), so this adds no new client contract.

Typed cause: reuses the helper's existing PROJECT_DIR_MISSING as-is; nothing new is invented and no new reader is added.

What still reports, deliberately:

  • A directory deleted between the precheck and the git call still surfaces as a 500 — that race is genuinely unexpected.
  • A permission-walled path (EACCES/EPERM) still throws unclassified and keeps reporting: throwIfNoEntry: false swallows only ENOENT, and the new negative test pins that a permission error is NOT converted to NOT_FOUND (the over-match #6634's merge gate had to fix).

Adjacent sites left alone: workspaces.generateBranchName (and aiRename) also touch project.repoPath outside the create mutation; they have not appeared in this issue and are out of scope.

Verification

New tests written first and confirmed failing before the fix: the two NOT_FOUND tests failed (both paths returned 500s), and the EACCES negative pin passed pre-fix (stays INTERNAL_SERVER_ERROR before and after).

bunx biome check on the three changed files:

Checked 3 files in 17ms. No fixes applied.

cd packages/host-service && bun run typecheck:

$ tsc --noEmit --emitDeclarationOnly false

(no errors)

Full bun test test in packages/host-service:

 1158 pass
 8 todo
 0 fail
 2709 expect() calls
Ran 1166 tests across 113 files. [178.84s]

Co-located router suites (bun test src/trpc/router/workspaces src/trpc/router/workspace-creation):

 167 pass
 0 fail
Ran 167 tests across 14 files. [3.37s]

Refs HOST-SERVICE-W

https://claude.ai/code/session_01PWSqv5bU8VzRDHP8Z4iWqi


Summary by cubic

Classifies a missing project repo directory as NOT_FOUND during workspace creation to replace opaque 500s. Previously, workspaces.create and workspaces.createEnqueued threw INTERNAL_SERVER_ERROR from simple-git when the repo path was gone; now both precheck and return NOT_FOUND with cause PROJECT_DIR_MISSING, and the enqueued path rejects before scheduling.

  • Validates the repo path once via requireProjectRepoPath after requireLocalProject in workspaces.create, then uses the validated path for all downstream calls (main workspace ensure, git, base-ref fetcher, PR metadata, branch listing, name application).
  • Adds the same guard to createEnqueued so a missing directory rejects synchronously and is not enqueued.
  • Keeps intentional reporting: a directory deleted after the precheck still surfaces as a 500; permission errors (EACCES/EPERM) remain unclassified and report.
  • Adds integration tests for NOT_FOUND on missing directories in both create paths and a negative pin that permission-walled paths stay INTERNAL_SERVER_ERROR.
  • No API shape or schema changes; clients see deterministic NOT_FOUND instead of 500 on this condition. Addresses the classification gap tracked in HOST-SERVICE-W.

Summary by CodeRabbit

  • Bug Fixes
    • Workspace creation now validates that the project repository path exists before starting operations.
    • Missing project directories return a clear “not found” error.
    • Permission-related access failures are reported accurately instead of being treated as missing directories.
    • Enqueued workspace creation no longer starts when the project directory is unavailable.