Repository navigation
Description
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
- In a checkout at the Version below with dependencies installed, save the script below as
repro.shoutside the checkout and runsh repro.sh <checkout-path>. - 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 2Expected 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 reproducedScreenshots, 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
juliusmarminge commentedon Nov 12, 2026
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.tsfails 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 thatt3 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.