diff --git a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts index c1580d156..f67fa664f 100644 --- a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts +++ b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts @@ -111,7 +111,7 @@ describe("CheckpointDiffQueryLive", () => { ); const expectedFromRef = checkpointRefForThreadTurnStart(threadId, TurnId.makeUnsafe("turn-1")); - expect(hasCheckpointRefCalls).toEqual([expectedFromRef, expectedFromRef, toCheckpointRef]); + expect(hasCheckpointRefCalls).toEqual([expectedFromRef, toCheckpointRef]); expect(diffCheckpointsCalls).toEqual([ { cwd: "/tmp/workspace", diff --git a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts index f048dc97d..b5d9a630b 100644 --- a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts +++ b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts @@ -132,6 +132,7 @@ const make = Effect.gen(function* () { detail: `Checkpoint diff is not available yet for turn ${input.toTurnCount}.`, }); } + let fromCheckpointExists = false; if (input.toTurnCount === input.fromTurnCount + 1) { const turnStartCheckpointRef = checkpointRefForThreadTurnStart( input.threadId, @@ -143,15 +144,18 @@ const make = Effect.gen(function* () { }); if (turnStartExists) { fromCheckpointRef = turnStartCheckpointRef; + fromCheckpointExists = true; } } const [fromExists, toExists] = yield* Effect.all( [ - checkpointStore.hasCheckpointRef({ - cwd: workspaceCwd, - checkpointRef: fromCheckpointRef, - }), + fromCheckpointExists + ? Effect.succeed(true) + : checkpointStore.hasCheckpointRef({ + cwd: workspaceCwd, + checkpointRef: fromCheckpointRef, + }), checkpointStore.hasCheckpointRef({ cwd: workspaceCwd, checkpointRef: toCheckpointRef, diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 4436d1f93..bd22c266d 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -3,7 +3,7 @@ import { describe, it, assert } from "@effect/vitest"; import { DEFAULT_SERVER_SETTINGS } from "@jcode/contracts"; import { Effect, FileSystem, Layer, Path, Sink, Stream } from "effect"; import * as PlatformError from "effect/PlatformError"; -import { ChildProcessSpawner } from "effect/unstable/process"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { checkClaudeProviderStatus, @@ -19,8 +19,12 @@ import { makeCheckOpenCodeProviderStatus, parseAuthStatusFromOutput, parseClaudeAuthStatusFromOutput, + ProviderHealthLive, readCodexConfigModelProvider, } from "./ProviderHealth"; +import { ServerConfig } from "../../config"; +import { ServerSettingsService } from "../../serverSettings"; +import { ProviderHealth } from "../Services/ProviderHealth"; // ── Test helpers ──────────────────────────────────────────────────── @@ -45,6 +49,7 @@ function mockSpawnerLayer( handler: ( args: ReadonlyArray, command: string, + options: ChildProcess.CommandOptions, ) => { stdout: string; stderr: string; @@ -54,12 +59,36 @@ function mockSpawnerLayer( return Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { - const cmd = command as unknown as { command: string; args: ReadonlyArray }; - return Effect.succeed(mockHandle(handler(cmd.args, cmd.command))); + const cmd = command as unknown as { + command: string; + args: ReadonlyArray; + options: ChildProcess.CommandOptions; + }; + return Effect.succeed(mockHandle(handler(cmd.args, cmd.command, cmd.options))); }), ); } +function withProcessPlatform( + platform: NodeJS.Platform, + effect: Effect.Effect, +): Effect.Effect { + return Effect.acquireUseRelease( + Effect.sync(() => { + const descriptor = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value: platform }); + return descriptor; + }), + () => effect, + (descriptor) => + Effect.sync(() => { + if (descriptor) { + Object.defineProperty(process, "platform", descriptor); + } + }), + ); +} + function failingSpawnerLayer(description: string) { return Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, @@ -858,6 +887,47 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => { ); }); + // ── provider update command tests ────────────────────────────────── + + describe("updateProvider", () => { + it.effect("runs provider update commands through a shell on Windows", () => { + const calls: Array<{ + command: string; + args: ReadonlyArray; + shell: ChildProcess.CommandOptions["shell"]; + }> = []; + + return withProcessPlatform( + "win32", + Effect.gen(function* () { + const providerHealth = yield* ProviderHealth; + const result = yield* providerHealth.updateProvider({ provider: "cursor" }); + + assert.strictEqual(result.providers.length, 0); + assert.deepStrictEqual(calls, [ + { + command: "agent", + args: ["update"], + shell: true, + }, + ]); + }).pipe( + Effect.provide(ProviderHealthLive), + Effect.provide(ServerSettingsService.layerTest()), + Effect.provide( + ServerConfig.layerTest(process.cwd(), { prefix: "jcode-provider-health-" }), + ), + Effect.provide( + mockSpawnerLayer((args, command, options) => { + calls.push({ command, args, shell: options.shell }); + return { stdout: "", stderr: "update failed\n", code: 1 }; + }), + ), + ), + ); + }); + }); + // ── parseClaudeAuthStatusFromOutput pure tests ──────────────────── describe("parseClaudeAuthStatusFromOutput", () => { diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 5980bcba9..90734c281 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -7448,7 +7448,7 @@ export default function ChatView({ key={queuedTurn.id} data-testid="queued-follow-up-row" className={cn( - "chat-composer-surface flex items-center gap-2 border border-b-0 border-[color:var(--color-border)] px-2.5 py-2 text-[12px] shadow-[0_1px_0_rgba(255,255,255,0.03)_inset]", + "chat-composer-surface flex items-center gap-2 border border-b-0 px-2.5 py-2 text-[12px] shadow-[0_1px_0_rgba(255,255,255,0.03)_inset]", queuedTurnIndex === 0 && !taskListAboveComposer ? "rounded-t-2xl" : "rounded-none", @@ -7463,7 +7463,7 @@ export default function ChatView({
{fileChangesExpanded && ( -
+
{checkpointFiles.map((file) => (