diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index f95fd1bef7..a7ffa0f1ab 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -25,6 +25,8 @@ const defaultEnvironmentInput = { runningUnderArm64Translation: false, } satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; +const normalizePath = (value: string) => value.replaceAll("\\", "/").replace(/^[A-Z]:/i, ""); + type TestEnvironmentInput = Partial & { readonly env?: Record; }; @@ -138,7 +140,10 @@ describe("DesktopAppIdentity", () => { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; const userDataPath = yield* identity.resolveUserDataPath; - assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Alpha)"); + assert.equal( + normalizePath(userDataPath), + "/Users/alice/Library/Application Support/T3 Code (Alpha)", + ); }), { legacyPathExists: true }, ), diff --git a/apps/desktop/src/app/DesktopEnvironment.test.ts b/apps/desktop/src/app/DesktopEnvironment.test.ts index 427b884883..89229da2cf 100644 --- a/apps/desktop/src/app/DesktopEnvironment.test.ts +++ b/apps/desktop/src/app/DesktopEnvironment.test.ts @@ -19,6 +19,12 @@ const defaultInput = { runningUnderArm64Translation: false, } satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; +const normalizePath = (value: string) => value.replaceAll("\\", "/").replace(/^[A-Z]:/i, ""); + +const assertPathEqual = (actual: string, expected: string) => { + assert.equal(normalizePath(actual), expected); +}; + const makeEnvironmentLayer = ( overrides: Partial = {}, env: Record = {}, @@ -53,18 +59,21 @@ describe("DesktopEnvironment", () => { ); assert.equal(environment.isDevelopment, true); - assert.equal(environment.appDataDirectory, "/Users/alice/Library/Application Support"); + assertPathEqual(environment.appDataDirectory, "/Users/alice/Library/Application Support"); assert.equal(environment.baseDir, "/tmp/t3"); - assert.equal(environment.stateDir, "/tmp/t3/dev"); - assert.equal(environment.desktopSettingsPath, "/tmp/t3/dev/desktop-settings.json"); - assert.equal(environment.clientSettingsPath, "/tmp/t3/dev/client-settings.json"); - assert.equal(environment.savedEnvironmentRegistryPath, "/tmp/t3/dev/saved-environments.json"); - assert.equal(environment.serverSettingsPath, "/tmp/t3/dev/settings.json"); - assert.equal(environment.logDir, "/tmp/t3/dev/logs"); - assert.equal(environment.rootDir, "/repo"); - assert.equal(environment.appRoot, "/repo"); - assert.equal(environment.backendEntryPath, "/repo/apps/server/dist/bin.mjs"); - assert.equal(environment.backendCwd, "/repo"); + assertPathEqual(environment.stateDir, "/tmp/t3/dev"); + assertPathEqual(environment.desktopSettingsPath, "/tmp/t3/dev/desktop-settings.json"); + assertPathEqual(environment.clientSettingsPath, "/tmp/t3/dev/client-settings.json"); + assertPathEqual( + environment.savedEnvironmentRegistryPath, + "/tmp/t3/dev/saved-environments.json", + ); + assertPathEqual(environment.serverSettingsPath, "/tmp/t3/dev/settings.json"); + assertPathEqual(environment.logDir, "/tmp/t3/dev/logs"); + assertPathEqual(environment.rootDir, "/repo"); + assertPathEqual(environment.appRoot, "/repo"); + assertPathEqual(environment.backendEntryPath, "/repo/apps/server/dist/bin.mjs"); + assertPathEqual(environment.backendCwd, "/repo"); assert.equal(environment.appUserModelId, "com.t3tools.t3code.dev"); assert.equal(environment.linuxWmClass, "t3code-dev"); assert.deepEqual( @@ -89,9 +98,9 @@ describe("DesktopEnvironment", () => { ); assert.equal(environment.isDevelopment, false); - assert.equal(environment.stateDir, "/tmp/t3/userdata"); - assert.equal(environment.logDir, "/tmp/t3/userdata/logs"); - assert.equal(environment.serverSettingsPath, "/tmp/t3/userdata/settings.json"); + assertPathEqual(environment.stateDir, "/tmp/t3/userdata"); + assertPathEqual(environment.logDir, "/tmp/t3/userdata/logs"); + assertPathEqual(environment.serverSettingsPath, "/tmp/t3/userdata/settings.json"); }), ); @@ -109,7 +118,10 @@ describe("DesktopEnvironment", () => { Option.some("/Users/alice"), ); assert.deepEqual( - environment.resolvePickFolderDefaultPath({ initialPath: "~/project" }), + Option.map( + environment.resolvePickFolderDefaultPath({ initialPath: "~/project" }), + normalizePath, + ), Option.some("/Users/alice/project"), ); }), diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index c7f40de42d..946f592be6 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -160,8 +160,6 @@ const buildCmd = Command.make( cwd: serverDir, stdout: config.verbose ? "inherit" : "ignore", stderr: "inherit", - // Windows needs shell mode to resolve `.cmd` shims on PATH. - shell: process.platform === "win32", }), ); diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 82546621d3..61af89cad8 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -147,6 +147,19 @@ describe("buildTurnStartParams", () => { ], }); }); + + it("omits unsupported service tiers before sending to Codex", () => { + const params = Effect.runSync( + buildTurnStartParams({ + threadId: "provider-thread-1", + runtimeMode: "full-access", + prompt: "Run", + serviceTier: "priority", + }), + ); + + assert.equal("serviceTier" in params, false); + }); }); describe("isRecoverableThreadResumeError", () => { @@ -237,6 +250,44 @@ describe("openCodexThread", () => { ); }); + it("omits unsupported service tiers from thread/start", async () => { + const calls: Array<{ method: "thread/start" | "thread/resume"; payload: unknown }> = []; + const started = makeThreadOpenResponse("fresh-thread"); + const client = { + request: ( + method: M, + payload: CodexRpc.ClientRequestParamsByMethod[M], + ) => { + calls.push({ method, payload }); + return Effect.succeed(started as CodexRpc.ClientRequestResponsesByMethod[M]); + }, + }; + + await Effect.runPromise( + openCodexThread({ + client, + threadId: ThreadId.make("thread-1"), + runtimeMode: "full-access", + cwd: "/tmp/project", + requestedModel: "gpt-5.3-codex", + serviceTier: "priority", + resumeThreadId: undefined, + }), + ); + + assert.deepStrictEqual(calls, [ + { + method: "thread/start", + payload: { + cwd: "/tmp/project", + approvalPolicy: "never", + sandbox: "danger-full-access", + model: "gpt-5.3-codex", + }, + }, + ]); + }); + it("propagates non-recoverable resume failures", async () => { const client = { request: ( diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index a2b54fac21..c976843a00 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -87,6 +87,7 @@ const formatSchemaIssue = SchemaIssue.makeFormatterDefault(); export type CodexResumeCursor = typeof CodexResumeCursorSchema.Type; type CodexServiceTier = NonNullable; +type SupportedCodexServiceTier = "fast" | "flex"; type CodexThreadItem = | EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"][number]["items"][number] | EffectCodexSchema.V2ThreadRollbackResponse["thread"]["turns"][number]["items"][number]; @@ -281,6 +282,12 @@ function runtimeModeToThreadConfig(input: RuntimeMode): { } } +function normalizeCodexServiceTier( + serviceTier: CodexServiceTier | undefined, +): SupportedCodexServiceTier | undefined { + return serviceTier === "fast" || serviceTier === "flex" ? serviceTier : undefined; +} + function buildThreadStartParams(input: { readonly cwd: string; readonly runtimeMode: RuntimeMode; @@ -288,12 +295,13 @@ function buildThreadStartParams(input: { readonly serviceTier: CodexServiceTier | undefined; }): EffectCodexSchema.V2ThreadStartParams { const config = runtimeModeToThreadConfig(input.runtimeMode); + const serviceTier = normalizeCodexServiceTier(input.serviceTier); return { cwd: input.cwd, approvalPolicy: config.approvalPolicy, sandbox: config.sandbox, ...(input.model ? { model: input.model } : {}), - ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), + ...(serviceTier ? { serviceTier } : {}), }; } @@ -367,6 +375,7 @@ export function buildTurnStartParams(input: { } const config = runtimeModeToThreadConfig(input.runtimeMode); + const serviceTier = normalizeCodexServiceTier(input.serviceTier); const collaborationMode = buildCodexCollaborationMode({ ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), ...(input.model ? { model: input.model } : {}), @@ -379,7 +388,7 @@ export function buildTurnStartParams(input: { approvalPolicy: config.approvalPolicy, sandboxPolicy: runtimeModeToTurnSandboxPolicy(input.runtimeMode), ...(input.model ? { model: input.model } : {}), - ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), + ...(serviceTier ? { serviceTier } : {}), ...(input.effort ? { effort: input.effort } : {}), ...(collaborationMode ? { collaborationMode } : {}), }).pipe( diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 5f5f975a4e..33b8c3cef2 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -208,6 +208,23 @@ const makeTestRunner = (registry: ProviderRegistryShape) => ); describe("providerMaintenanceRunner", () => { + it("resolves Windows command shims before spawning update commands", () => { + const resolvedNpm = ProviderMaintenanceRunner.resolveProviderMaintenanceSpawnCommand("npm", { + platform: "win32", + env: process.env, + }); + if (resolvedNpm !== "npm") { + assert.match(resolvedNpm, /npm\.(cmd|exe|bat|com)$/i); + } + assert.strictEqual( + ProviderMaintenanceRunner.resolveProviderMaintenanceSpawnCommand("npm", { + platform: "linux", + env: process.env, + }), + "npm", + ); + }); + it.effect("runs the allowlisted provider update command and records success", () => { const calls: Array<{ command: string; args: ReadonlyArray }> = []; return Effect.gen(function* () { @@ -271,7 +288,7 @@ describe("providerMaintenanceRunner", () => { yield* updater.updateProvider(CODEX_DRIVER); assert.deepStrictEqual(calls, [ { - command: "bun", + command: ProviderMaintenanceRunner.resolveProviderMaintenanceSpawnCommand("bun"), args: ["i", "-g", "@openai/codex@latest"], }, ]); @@ -300,7 +317,7 @@ describe("providerMaintenanceRunner", () => { assert.deepStrictEqual(calls, [ { - command: "npm", + command: ProviderMaintenanceRunner.resolveProviderMaintenanceSpawnCommand("npm"), args: ["install", "-g", "@openai/codex@latest"], }, ]); diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index 5f76afb34d..0cf4e48743 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -7,6 +7,7 @@ import { type ServerProviderUpdatedPayload, type ServerProviderUpdateState, } from "@t3tools/contracts"; +import { resolveCommandPath } from "@t3tools/shared/shell"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Data from "effect/Data"; @@ -67,6 +68,26 @@ interface VerifiedProviderRefresh { const nowIso = Effect.map(DateTime.now, DateTime.formatIso); +export function resolveProviderMaintenanceSpawnCommand( + command: string, + options: { + readonly platform?: NodeJS.Platform; + readonly env?: NodeJS.ProcessEnv; + } = {}, +): string { + const platform = options.platform ?? process.platform; + if (platform !== "win32") { + return command; + } + + return ( + resolveCommandPath(command, { + platform, + env: options.env ?? process.env, + }) ?? command + ); +} + const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceRunner.runCommand")( function* (input: { readonly spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; @@ -75,8 +96,9 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR }) { const collectCommandResult = Effect.fn("ProviderMaintenanceRunner.collectCommandResult")( function* () { + const spawnCommand = resolveProviderMaintenanceSpawnCommand(input.command); const child = yield* input.spawner - .spawn(ChildProcess.make(input.command, [...input.args])) + .spawn(ChildProcess.make(spawnCommand, [...input.args])) .pipe( Effect.mapError( (cause) => diff --git a/apps/web/src/lib/archivedThreadsState.ts b/apps/web/src/lib/archivedThreadsState.ts index 1fe91455fc..17cd973381 100644 --- a/apps/web/src/lib/archivedThreadsState.ts +++ b/apps/web/src/lib/archivedThreadsState.ts @@ -1,10 +1,10 @@ import { useAtomValue } from "@effect/atom-react"; import { EnvironmentId, type OrchestrationShellSnapshot } from "@t3tools/contracts"; -import { AsyncResult, Atom } from "effect/unstable/reactivity"; -import { useCallback, useMemo } from "react"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; +import { AsyncResult, Atom } from "effect/unstable/reactivity"; +import { useCallback, useMemo } from "react"; import { readEnvironmentApi } from "../environmentApi"; import { appAtomRegistry } from "../rpc/atomRegistry"; diff --git a/oxlint-plugin-t3code/test/utils.ts b/oxlint-plugin-t3code/test/utils.ts index f44dd3a5d1..f04e01db46 100644 --- a/oxlint-plugin-t3code/test/utils.ts +++ b/oxlint-plugin-t3code/test/utils.ts @@ -28,6 +28,7 @@ class OxlintFixtureExpectedFailure extends Data.TaggedError("OxlintFixtureExpect } const encodeOxlintConfig = Schema.encodeEffect(Schema.UnknownFromJsonString); +const OXLINT_RULE_TEST_TIMEOUT_MS = 30_000; interface RuleHarness { readonly run: ( @@ -131,20 +132,23 @@ export const createOxlintRuleHarness = (ruleName: string): RuleHarness => { runAndExpectFailure, valid(name, source) { test(name, (it) => { - it.effect("passes", () => run(source)); + it.effect("passes", () => run(source), OXLINT_RULE_TEST_TIMEOUT_MS); }); }, invalid(name, source, assertion) { test(name, (it) => { - it.effect("reports the rule diagnostic", () => - runAndExpectFailure(source).pipe( - Effect.tap((output) => - Effect.sync(() => { - assert.match(output, new RegExp(diagnosticRuleName)); - assertion?.(output); - }), + it.effect( + "reports the rule diagnostic", + () => + runAndExpectFailure(source).pipe( + Effect.tap((output) => + Effect.sync(() => { + assert.match(output, new RegExp(diagnosticRuleName)); + assertion?.(output); + }), + ), ), - ), + OXLINT_RULE_TEST_TIMEOUT_MS, ); }); }, diff --git a/scripts/mock-update-server.test.ts b/scripts/mock-update-server.test.ts index 747a21c68b..a694feec8b 100644 --- a/scripts/mock-update-server.test.ts +++ b/scripts/mock-update-server.test.ts @@ -90,7 +90,15 @@ it.layer(NodeServices.layer)("mock-update-server", (it) => { yield* fileSystem.writeFileString(outsideFile, "version: outside\n"); yield* fileSystem.makeDirectory(linksDir, { recursive: true }); - yield* fileSystem.symlink(outsideFile, symlinkPath); + const symlinkCreated = yield* fileSystem.symlink(outsideFile, symlinkPath).pipe( + Effect.as(true), + Effect.catch((error) => + process.platform === "win32" ? Effect.succeed(false) : Effect.fail(error), + ), + ); + if (!symlinkCreated) { + return; + } yield* withMockUpdateServer( rootRealPath,