Skip to content

[Bug]: Windows project path casing creates duplicate projects #35667

Closed as not planned

Description

@coygeek

Summary

On a case-insensitive Windows filesystem, adding an existing project folder with different path letter case can create another project for the same directory. Starting the web server from that differently spelled directory can also bootstrap a second project instead of reusing the first. A focused test against the project store reproduced the lookup miss, and the server and CLI paths from the supported entry points were traced in source; a Windows end-to-end run was not performed.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. In a checkout at the Version below with dependencies installed, save the script below as repro.sh outside the checkout and run sh repro.sh <checkout-path>.
  2. The script writes one temporary test named ProjectStore.x01-01.test.ts, creates an in-memory project with a Windows drive path, checks that the exact stored spelling resolves, then looks up a spelling with different letter case. It removes the temporary file and scratch state before exit.

The one focused run found the control project and missed the case variant. On a normal case-insensitive Windows volume, C:\Work\Repo and c:\work\repo refer to the same directory. Source-only end-to-end steps: register a folder with one spelling, then run t3 project add with the other spelling in the same environment; the CLI's existing-project guard and the server's conflict guard both miss it. The same miss applies to web startup bootstrap when its cwd spells an existing project root differently (apps/server/src/serverRuntimeStartup.ts#L271-L279, apps/server/src/project/ProjectService.ts#L439-L444).

The script requires Node.js, vp, and the checkout's installed dependencies. It takes the checkout path as its sole argument. Exit 1 with RESULT: defect reproduced means the exact-path control passed and the case variant failed; exit 0 with RESULT: expected behavior means the test passed; exit 2 with RESULT: inconclusive means setup or the control failed. It points HOME, TMPDIR, and T3CODE_HOME at disposable scratch directories.

Reproduction script: repro.sh
#!/bin/sh
set -eu

checkout=${1:?pass the T3 Code checkout path}
test_file="$checkout/apps/server/src/orchestration-v2/ProjectStore.x01-01.test.ts"
scratch=$(mktemp -d)
created=0
cleanup() {
  if [ "$created" -eq 1 ]; then rm -f "$test_file"; fi
  rm -rf "$scratch"
}
trap cleanup EXIT HUP INT TERM

if [ -e "$test_file" ]; then
  printf '%s\n' 'RESULT: inconclusive'
  exit 2
fi

export HOME="$scratch/home" TMPDIR="$scratch/tmp" T3CODE_HOME="$scratch/t3"
mkdir -p "$HOME" "$TMPDIR" "$T3CODE_HOME"
cd "$checkout"
node --version
vp --version

cat > "$test_file" <<'TEST'
import { assert, it } from "@effect/vitest";
import { EventId, ProjectId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import * as SqlitePersistence from "../persistence/Sqlite.ts";
import * as ProjectStore from "./ProjectStore.ts";

it.layer(ProjectStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)))(
  "Windows workspace identity",
  (it) => {
    it.effect("finds a stored project with different path letter case", () =>
      Effect.gen(function* () {
        const projects = yield* ProjectStore.ProjectStoreV2;
        const projectId = ProjectId.make("project-case-identity");
        yield* projects.apply({
          sequence: 1,
          eventId: EventId.make("event-case-identity"),
          aggregateKind: "project",
          aggregateId: projectId,
          occurredAt: "2026-01-01T00:00:00.000Z",
          commandId: null,
          causationEventId: null,
          correlationId: null,
          metadata: {},
          type: "project.created",
          payload: {
            projectId,
            title: "Case fixture",
            workspaceRoot: "C:\\Work\\Repo",
            defaultModelSelection: null,
            scripts: [],
            createdAt: "2026-01-01T00:00:00.000Z",
            updatedAt: "2026-01-01T00:00:00.000Z",
          },
        });

        const control = Option.isSome(yield* projects.findActiveByWorkspaceRoot("C:\\Work\\Repo"));
        const variant = Option.isSome(yield* projects.findActiveByWorkspaceRoot("c:\\work\\repo"));
        console.log(`EVIDENCE control=${control ? "present" : "missing"} caseVariant=${variant ? "present" : "missing"}`);
        assert.isTrue(control);
        assert.isTrue(variant);
      }),
    );
  },
);
TEST
created=1

if vp test run apps/server/src/orchestration-v2/ProjectStore.x01-01.test.ts --reporter=verbose > "$scratch/test.out" 2>&1; then
  cat "$scratch/test.out"
  printf '%s\n' 'RESULT: expected behavior'
  exit 0
fi

cat "$scratch/test.out"
if grep -Fq 'EVIDENCE control=present caseVariant=missing' "$scratch/test.out"; then
  printf '%s\n' 'RESULT: defect reproduced'
  exit 1
fi

printf '%s\n' 'RESULT: inconclusive'
exit 2

Expected behavior

Adding a folder already registered as an active project should report that project rather than create another one, regardless of letter case on a case-insensitive Windows volume. The CLI defines ProjectAlreadyExistsError for that case (apps/server/src/cli/project.ts#L148-L157); the project command rejects a workspace owned by another active project (apps/server/src/orchestration-v2/ProjectCommands.ts#L125-L132). The existing bootstrap test requires an already registered workspace to be reused (apps/server/src/project/ProjectService.test.ts#L360-L379), and the client comparison helper explicitly lowercases Windows drive and UNC paths (packages/shared/src/path.ts#L55-L60).

Actual behavior

The actual ProjectStoreV2.findActiveByWorkspaceRoot lookup returns the stored project for C:\Work\Repo and no project for c:\work\repo in the focused run below. Source trace: t3 project add passes the normalized path into a case-sensitive snapshot check and, on a miss, dispatches project.create (apps/server/src/cli/project.ts#L488-L511); the server uses the same store lookup for its conflict check (apps/server/src/project/ProjectService.ts#L251-L260). A Windows CLI run was not performed.

Impact and scope

Impact: P2 - a case variation in a Windows folder path can create a persistent second project entry for the same directory, separating that folder's threads and project settings by project ID; consistently using the original spelling avoids the failure.

The store lookup failure was reproduced in an in-memory SQLite database on macOS. The user-facing duplicate creation follows from the source-traced CLI and web bootstrap paths on a case-insensitive Windows volume; it was not observed on a Windows host. A Windows directory configured for case-sensitive behavior is outside this claim. The lookup also affects CLI project subcommands that resolve an identifier by path (apps/server/src/cli/project.ts#L293-L309).

Earliest failure stage

State or storage

Analysis or suspected cause

Source trace: workspace normalization uses path.resolve and a directory stat, without canonicalizing letter case (apps/server/src/workspace/WorkspacePaths.ts#L133-L173). A separate Node v26.12.1 run showed path.win32.resolve retains both spellings (C:\Work\Repo and c:\work\repo). The project store's query uses workspace_root = ? (apps/server/src/orchestration-v2/ProjectStore.ts#L132-L140, apps/server/src/orchestration-v2/ProjectStore.ts#L218-L225); the projection column is plain TEXT NOT NULL (apps/server/src/persistence/Migrations/005_Projections.ts#L7-L16).

Additional context

Open #34755 reports a client lookup miss for forward-slash UNC input, not drive-path letter case in the server and CLI. Open #23500 changes mobile existing-project reuse, not these server or CLI lookups. The corresponding normalization, store lookup, project service, CLI check, and projection schema on main at a3cde98425e4d3fc962aafd2e3dc4f789dc67926 still use the same comparisons.

The focused lookup was reproduced with an in-memory SQLite test; the Windows CLI and web paths were traced in source, not run end to end on Windows. The regression check is for the case variant to resolve the same active project while the exact-path control and case-sensitive filesystem behavior remain valid. Evidence provenance: observed store test and source trace. Local verification: reproduced at the store boundary; Windows end-to-end not run. Reproduction completeness: incomplete for native Windows.

Impact

Minor bug or occasional failure

Version or commit

main @ 817b92f (full SHA 817b92f13a1d11922e76d6b79cac42861be972cb). The focused reproduction ran at c00a373491d20ebcef901b81170abfb9f5f757fe; the relevant source files were unchanged between the verifier’s main revision and this main revision.

Environment

Observed test host: macOS arm64; Node.js v26.12.1; Vite+ v1.5.1 (local v1.4.0); in-memory SQLite. No native Windows run was performed.

Logs or stack traces

The focused reproduction printed this output before exiting 1; the assertion summary is excerpted and contains no local paths:

v26.12.1
vp v1.5.1
EVIDENCE control=present caseVariant=missing
AssertionError: expected false to be true
Test Files  1 failed (1)
Tests  1 failed (1)
RESULT: defect reproduced

Screenshots, recordings, or supporting files

No response

Workaround

Use the original path letter case consistently when adding or opening the project on a case-insensitive Windows volume. This was inferred from the lookup control; no native Windows workaround run was performed.

References

  • Closed #13421 attempted to make Windows project lookup insensitive to path case in the V1 projection. The maintainer closed it after the V2 rewrite removed that file and explicitly did not assume the underlying request was resolved. This report reproduces the V2 store lookup miss.
  • Closed #17697 concerns symlink aliases for the same folder. Its filesystem identity and product decision differ from Windows letter-case equivalence.

Activity

  1. juliusmarminge commented on Nov 12, 2026

    @juliusmarminge
    Member

    I tried to reproduce this on the current build and could not.

    On origin/main b1c1e07 I added a case to the existing apps/server/src/orchestration-v2/ProjectStore.test.ts. It applies project.created events for workspace roots 'C:\Work\Repo' and '/tmp/Work/Repo' in the in-memory SQLite store, then calls ProjectStoreV2.findActiveByWorkspaceRoot('c:\work\repo') and findActiveByWorkspaceRoot('/tmp/work/repo'). vp test run apps/server/src/orchestration-v2/ProjectStore.test.ts fails with 'AssertionError: expected null to equal project-windows-case': the Windows spelling with different letter case finds no project. The two existing tests in the file pass. This is the lookup that t3 project add's existing-project check, the server's project.create conflict check and web startup bootstrap all use. I did not see a duplicate project appear in the CLI or web app because this needs a case-insensitive Windows volume, and this Linux VM's filesystem is case-sensitive.

    Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions