From 13086ee0e2aa4ff4783700354c4a419f0b99cb34 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 5 Sep 2026 05:21:52 +0000 Subject: [PATCH 1/2] refactor: reduce redundant tests and helper logic --- apps/desktop/src/wsl/wslPathParsing.test.ts | 45 +++---- apps/mobile/src/lib/mobileTheme.test.ts | 6 - apps/server/src/cli/invocation.ts | 25 +--- apps/server/src/imageMime.test.ts | 79 ++++-------- .../TextGenerationPrompts.test.ts | 26 ++-- .../src/textGeneration/TextGenerationUtils.ts | 15 +-- apps/web/src/pullRequestReference.test.ts | 76 +++-------- apps/web/src/session-logic.ts | 53 ++------ .../environments/ManagedTunnelLimits.test.ts | 29 +---- packages/contracts/src/browserProfile.test.ts | 14 +- packages/shared/src/cliArgs.test.ts | 121 +++++------------- packages/shared/src/git.test.ts | 66 ++++------ packages/shared/src/terminalLabels.test.ts | 31 ++--- packages/shared/src/terminalLabels.ts | 21 +-- packages/shared/src/threadEnvMode.test.ts | 81 ++++-------- scripts/lib/build-target-arch.ts | 7 +- scripts/lib/cli-external-packages.test.ts | 57 ++------- scripts/lib/cli-external-packages.ts | 80 ++---------- scripts/lib/public-config.test.ts | 22 +--- 19 files changed, 218 insertions(+), 636 deletions(-) diff --git a/apps/desktop/src/wsl/wslPathParsing.test.ts b/apps/desktop/src/wsl/wslPathParsing.test.ts index 41e358227e1e..b3120d1ad408 100644 --- a/apps/desktop/src/wsl/wslPathParsing.test.ts +++ b/apps/desktop/src/wsl/wslPathParsing.test.ts @@ -79,40 +79,25 @@ describe("parseWslDistroList", () => { }); describe("extractDistroFromUncPath", () => { - it("extracts the distro from \\\\wsl.localhost UNC paths", () => { - expect(extractDistroFromUncPath("\\\\wsl.localhost\\Ubuntu-22.04\\home\\josh")).toBe( - "Ubuntu-22.04", - ); - }); - - it("extracts the distro from the legacy \\\\wsl$ UNC paths", () => { - expect(extractDistroFromUncPath("\\\\wsl$\\Debian\\home\\josh")).toBe("Debian"); - }); - - it("returns null for non-UNC Windows paths", () => { - expect(extractDistroFromUncPath("C:\\Users\\Josh\\project")).toBeNull(); - }); - - it("returns null when the segment is not a valid distro name", () => { - expect(extractDistroFromUncPath("\\\\wsl.localhost\\bad name!\\home")).toBeNull(); + it.each([ + ["\\\\wsl.localhost\\Ubuntu-22.04\\home\\josh", "Ubuntu-22.04"], + ["\\\\wsl$\\Debian\\home\\josh", "Debian"], + ["C:\\Users\\Josh\\project", null], + ["\\\\wsl.localhost\\bad name!\\home", null], + ] as const)("extracts the distro from %s as %s", (path, expected) => { + expect(extractDistroFromUncPath(path)).toBe(expected); }); }); describe("wslUncPathToLinuxPath", () => { - it("maps WSL UNC paths back to Linux absolute paths", () => { - expect(wslUncPathToLinuxPath("\\\\wsl.localhost\\Ubuntu-22.04\\home\\josh\\repo")).toBe( - "/home/josh/repo", - ); - }); - - it("maps a distro UNC root to Linux root", () => { - expect(wslUncPathToLinuxPath("\\\\wsl$\\Debian")).toBe("/"); - expect(wslUncPathToLinuxPath("\\\\wsl.localhost\\Debian\\")).toBe("/"); - }); - - it("rejects invalid distro names and non-WSL paths", () => { - expect(wslUncPathToLinuxPath("\\\\wsl.localhost\\bad!name\\home")).toBeNull(); - expect(wslUncPathToLinuxPath("C:\\Users\\Josh\\repo")).toBeNull(); + it.each([ + ["\\\\wsl.localhost\\Ubuntu-22.04\\home\\josh\\repo", "/home/josh/repo"], + ["\\\\wsl$\\Debian", "/"], + ["\\\\wsl.localhost\\Debian\\", "/"], + ["\\\\wsl.localhost\\bad!name\\home", null], + ["C:\\Users\\Josh\\repo", null], + ] as const)("converts %s to %s", (path, expected) => { + expect(wslUncPathToLinuxPath(path)).toBe(expected); }); }); diff --git a/apps/mobile/src/lib/mobileTheme.test.ts b/apps/mobile/src/lib/mobileTheme.test.ts index a3c6712abae8..e43e1901aa88 100644 --- a/apps/mobile/src/lib/mobileTheme.test.ts +++ b/apps/mobile/src/lib/mobileTheme.test.ts @@ -180,12 +180,6 @@ describe("mobile themes", () => { expect( contrastRatio(variables["--color-placeholder"], variables["--color-input"]), ).toBeGreaterThanOrEqual(4.5); - } - } - - for (const themeId of BUILT_IN_THEME_IDS) { - for (const appearance of ["light", "dark"] as const) { - const variables = getMobileThemeVariables(themeId, appearance); expect( contrastRatio( variables["--color-user-bubble-foreground"], diff --git a/apps/server/src/cli/invocation.ts b/apps/server/src/cli/invocation.ts index e1b03552948d..a6e4d6e9f7ef 100644 --- a/apps/server/src/cli/invocation.ts +++ b/apps/server/src/cli/invocation.ts @@ -7,16 +7,8 @@ import packageJson from "../../package.json" with { type: "json" }; export type CliRunner = "npx" | "pnpm dlx" | "bunx"; /** - * How the CLI was launched, judged by where its entry script lives. Each - * package runner executes out of a distinctive cache/temp layout: - * - * npx ~/.npm/_npx//node_modules/... - * pnpm dlx ~/.cache/pnpm/dlx/..., $PNPM_HOME/.pnpm/dlx/..., - * or %LOCALAPPDATA%/pnpm-cache/dlx/... on Windows - * bunx ~/.bun/install/cache/... or $TMPDIR/bunx--/... - * - * Global installs and repo checkouts match none of these and return null. - * Detection is best-effort; callers must fail closed to a plain `t3` command. + * Detect package runners by their cache paths. Unrecognized paths, including + * global installs and repo checkouts, fall back to a plain `t3` command. */ export function detectCliRunner(entryPath: string): CliRunner | null { const path = entryPath.replaceAll("\\", "/"); @@ -37,21 +29,14 @@ export function detectCliRunner(entryPath: string): CliRunner | null { } /** - * The `t3` package spec to suggest. The literal spec the user typed (e.g. - * `t3@nightly`) is resolved away before our process starts, so re-derive it - * from the running version: nightly builds re-suggest the nightly channel, - * anything else suggests the bare package. + * The original package spec is resolved away before startup, so infer the + * nightly channel from the running version. */ export function suggestedPackageSpec(version: string): string { return version.includes("-nightly.") ? "t3@nightly" : "t3"; } -/** - * Render a `t3 ` suggestion that matches how this process was - * launched, so copy/pasting it actually works: `npx t3 connect` suggests - * `npx t3 serve`, a global install suggests `t3 serve`, and a nightly build - * keeps the `@nightly` tag. - */ +/** Suggest a command using the current package runner and release channel. */ export function formatCliCommand(input: { readonly subcommand: string; readonly entryPath: string; diff --git a/apps/server/src/imageMime.test.ts b/apps/server/src/imageMime.test.ts index e87e4dcbd0dc..e69527e90da2 100644 --- a/apps/server/src/imageMime.test.ts +++ b/apps/server/src/imageMime.test.ts @@ -3,70 +3,37 @@ import { describe, expect, it } from "vite-plus/test"; import { inferImageExtension, parseBase64DataUrl } from "./imageMime.ts"; describe("imageMime", () => { - it("parses base64 data URL with mime type", () => { - expect(parseBase64DataUrl("data:image/png;base64,SGVsbG8=")).toEqual({ + it.each([ + ["mime type", "data:image/png;base64,SGVsbG8="], + ["mime parameters", "data:image/png;charset=utf-8;base64,SGVsbG8="], + ["spaces in payload", "data:image/png;base64,SGVs bG8=\n"], + ["case-insensitive scheme and mime type", "DATA:IMAGE/PNG;BASE64,SGVsbG8="], + ])("parses base64 data URL with %s", (_, dataUrl) => { + expect(parseBase64DataUrl(dataUrl)).toEqual({ mimeType: "image/png", base64: "SGVsbG8=", }); }); - it("parses base64 data URL with mime parameters", () => { - expect(parseBase64DataUrl("data:image/png;charset=utf-8;base64,SGVsbG8=")).toEqual({ - mimeType: "image/png", - base64: "SGVsbG8=", - }); - }); - - it("rejects non-base64 data URL", () => { - expect(parseBase64DataUrl("data:image/png;charset=utf-8,hello")).toBeNull(); - }); - - it("rejects missing mime type", () => { - expect(parseBase64DataUrl("data:;base64,SGVsbG8=")).toBeNull(); + it.each([ + ["non-base64 data URL", "data:image/png;charset=utf-8,hello"], + ["missing mime type", "data:;base64,SGVsbG8="], + ["invalid payload character", "data:image/png;base64,SGVs!bG8="], + ["comma in payload", "data:image/png;base64,SGVs,bG8="], + ["interior and excess padding", "data:image/png;base64,AB=CD==="], + ["interior padding", "data:image/png;base64,SGV=bG8="], + ["excess padding followed by data", "data:image/png;base64,SGVsbG8=====AAA"], + ["length not a multiple of four", "data:image/png;base64,SGVsbG8"], + ["empty payload", "data:image/png;base64,"], + ["whitespace-only payload", "data:image/png;base64, \r\n"], + ])("rejects %s", (_, dataUrl) => { + expect(parseBase64DataUrl(dataUrl)).toBeNull(); }); - it("parses base64 data URL with spaces in payload", () => { - expect(parseBase64DataUrl("data:image/png;base64,SGVs bG8=\n")).toEqual({ + it.each(["SGVsbA==", "SGVsbG8h"])("accepts base64 payload %s", (base64) => { + expect(parseBase64DataUrl(`data:image/png;base64,${base64}`)).toEqual({ mimeType: "image/png", - base64: "SGVsbG8=", - }); - }); - - it("rejects payload with characters outside the base64 alphabet", () => { - expect(parseBase64DataUrl("data:image/png;base64,SGVs!bG8=")).toBeNull(); - expect(parseBase64DataUrl("data:image/png;base64,SGVs,bG8=")).toBeNull(); - }); - - it("rejects structurally malformed base64", () => { - // '=' before the trailing padding position - expect(parseBase64DataUrl("data:image/png;base64,AB=CD===")).toBeNull(); - expect(parseBase64DataUrl("data:image/png;base64,SGV=bG8=")).toBeNull(); - // more than two padding characters - expect(parseBase64DataUrl("data:image/png;base64,SGVsbG8=====AAA")).toBeNull(); - // length not a multiple of 4 - expect(parseBase64DataUrl("data:image/png;base64,SGVsbG8")).toBeNull(); - }); - - it("accepts base64 with one or two trailing padding characters", () => { - expect(parseBase64DataUrl("data:image/png;base64,SGVsbA==")).toEqual({ - mimeType: "image/png", - base64: "SGVsbA==", - }); - expect(parseBase64DataUrl("data:image/png;base64,SGVsbG8h")).toEqual({ - mimeType: "image/png", - base64: "SGVsbG8h", - }); - }); - - it("rejects empty and whitespace-only payloads", () => { - expect(parseBase64DataUrl("data:image/png;base64,")).toBeNull(); - expect(parseBase64DataUrl("data:image/png;base64, \r\n")).toBeNull(); - }); - - it("parses a case-insensitive scheme and mime type", () => { - expect(parseBase64DataUrl("DATA:IMAGE/PNG;BASE64,SGVsbG8=")).toEqual({ - mimeType: "image/png", - base64: "SGVsbG8=", + base64, }); }); diff --git a/apps/server/src/textGeneration/TextGenerationPrompts.test.ts b/apps/server/src/textGeneration/TextGenerationPrompts.test.ts index 253d88779827..43f8f4dc22d9 100644 --- a/apps/server/src/textGeneration/TextGenerationPrompts.test.ts +++ b/apps/server/src/textGeneration/TextGenerationPrompts.test.ts @@ -265,29 +265,19 @@ describe("sanitizeThreadTitle", () => { }); describe("normalizeCliError", () => { - it("detects 'Command not found' and includes CLI name in the message", () => { - const error = normalizeCliError( - "claude", - "generateCommitMessage", - new Error("Command not found: claude"), - "Something went wrong", - ); - - expect(error).toBeInstanceOf(TextGenerationError); - expect(error.detail).toContain("Claude CLI"); - expect(error.detail).toContain("not available on PATH"); - }); - - it("uses the CLI name from the first argument for codex", () => { + it.each([ + ["claude", "generateCommitMessage", "Claude CLI"], + ["codex", "generateBranchName", "Codex CLI"], + ])("identifies %s when its command is missing", (cliName, operation, label) => { const error = normalizeCliError( - "codex", - "generateBranchName", - new Error("Command not found: codex"), + cliName, + operation, + new Error(`Command not found: ${cliName}`), "Something went wrong", ); expect(error).toBeInstanceOf(TextGenerationError); - expect(error.detail).toContain("Codex CLI"); + expect(error.detail).toContain(label); expect(error.detail).toContain("not available on PATH"); }); diff --git a/apps/server/src/textGeneration/TextGenerationUtils.ts b/apps/server/src/textGeneration/TextGenerationUtils.ts index ad2911c20f76..322763e1123f 100644 --- a/apps/server/src/textGeneration/TextGenerationUtils.ts +++ b/apps/server/src/textGeneration/TextGenerationUtils.ts @@ -3,7 +3,7 @@ import * as Schema from "effect/Schema"; const isTextGenerationError = Schema.is(TextGenerationError); -/** Convert an Effect Schema to a flat JSON Schema object, inlining `$defs` when present. */ +/** Convert an Effect Schema to JSON Schema, including referenced `$defs` when present. */ export function toJsonSchemaObject(schema: Schema.Top): unknown { const document = Schema.toJsonSchemaDocument(schema); if (document.definitions && Object.keys(document.definitions).length > 0) { @@ -19,7 +19,7 @@ export function limitSection(value: string, maxChars: number): string { return `${truncated}\n\n[truncated]`; } -/** Normalise a raw commit subject to imperative-mood, ≤72 chars, no trailing period. */ +/** Normalise a raw commit subject to ≤72 chars with no trailing period. */ export function sanitizeCommitSubject(raw: string): string { const singleLine = raw.trim().split(/\r?\n/g)[0]?.trim() ?? ""; const withoutTrailingPeriod = singleLine.replace(/[.]+$/g, "").trim(); @@ -69,11 +69,7 @@ function cliLabel(cliName: string): string { return `${capitalized} CLI (\`${cliName}\`)`; } -/** - * Normalize an unknown error from a CLI text generation process into a - * typed `TextGenerationError`. Parameterized by CLI name so both Codex - * and Claude (and future providers) can share the same logic. - */ +/** Preserve typed errors and hide CLI failure details behind the public fallback. */ export function normalizeCliError( cliName: string, operation: string, @@ -97,11 +93,6 @@ export function normalizeCliError( cause: error, }); } - return new TextGenerationError({ - operation, - detail: fallback, - cause: error, - }); } return new TextGenerationError({ diff --git a/apps/web/src/pullRequestReference.test.ts b/apps/web/src/pullRequestReference.test.ts index 5e534af0a0be..29393ff952d8 100644 --- a/apps/web/src/pullRequestReference.test.ts +++ b/apps/web/src/pullRequestReference.test.ts @@ -3,44 +3,26 @@ import { describe, expect, it } from "vite-plus/test"; import { parsePullRequestReference } from "./pullRequestReference"; describe("parsePullRequestReference", () => { - it("accepts GitHub pull request URLs", () => { - expect(parsePullRequestReference("https://github.com/pingdotgg/t3code/pull/42")).toBe( - "https://github.com/pingdotgg/t3code/pull/42", - ); - }); - - it("accepts Azure DevOps pull request URLs", () => { - expect( - parsePullRequestReference("https://dev.azure.com/acme/project/_git/t3code/pullrequest/42"), - ).toBe("https://dev.azure.com/acme/project/_git/t3code/pullrequest/42"); - }); - - it("accepts GitLab merge request URLs", () => { - expect(parsePullRequestReference("https://gitlab.com/group/project/-/merge_requests/42")).toBe( - "https://gitlab.com/group/project/-/merge_requests/42", - ); - }); - - it("accepts legacy Azure DevOps pull request URLs", () => { - expect( - parsePullRequestReference("https://acme.visualstudio.com/project/_git/t3code/pullrequest/42"), - ).toBe("https://acme.visualstudio.com/project/_git/t3code/pullrequest/42"); - }); - - it("accepts raw numbers", () => { - expect(parsePullRequestReference("42")).toBe("42"); - }); - - it("accepts #number references", () => { - expect(parsePullRequestReference("#42")).toBe("42"); - }); - - it("accepts gh pr checkout commands with raw numbers", () => { - expect(parsePullRequestReference("gh pr checkout 42")).toBe("42"); - }); - - it("accepts gh pr checkout commands with #number references", () => { - expect(parsePullRequestReference("gh pr checkout #42")).toBe("42"); + it.each([ + "https://github.com/pingdotgg/t3code/pull/42", + "https://dev.azure.com/acme/project/_git/t3code/pullrequest/42", + "https://gitlab.com/group/project/-/merge_requests/42", + "https://acme.visualstudio.com/project/_git/t3code/pullrequest/42", + ])("accepts pull request URL %s", (url) => { + expect(parsePullRequestReference(url)).toBe(url); + }); + + it.each([ + "42", + "#42", + "gh pr checkout 42", + "gh pr checkout #42", + "glab mr checkout 42", + "az repos pr checkout --id 42", + "az repos pr checkout --id=42", + "az repos pr checkout --id 42 --remote-name origin", + ])("extracts the pull request number from %s", (input) => { + expect(parsePullRequestReference(input)).toBe("42"); }); it("accepts gh pr checkout commands with GitHub pull request URLs", () => { @@ -49,24 +31,6 @@ describe("parsePullRequestReference", () => { ).toBe("https://github.com/pingdotgg/t3code/pull/42"); }); - it("accepts glab mr checkout commands with raw numbers", () => { - expect(parsePullRequestReference("glab mr checkout 42")).toBe("42"); - }); - - it("accepts az repos pr checkout commands with raw numbers", () => { - expect(parsePullRequestReference("az repos pr checkout --id 42")).toBe("42"); - }); - - it("accepts az repos pr checkout commands with equals-style ids", () => { - expect(parsePullRequestReference("az repos pr checkout --id=42")).toBe("42"); - }); - - it("accepts az repos pr checkout commands with extra flags", () => { - expect(parsePullRequestReference("az repos pr checkout --id 42 --remote-name origin")).toBe( - "42", - ); - }); - it("rejects non-pull-request input", () => { expect(parsePullRequestReference("feature/my-branch")).toBeNull(); }); diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index 5c9d89e5ea33..5228aa38e769 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -221,47 +221,20 @@ export function workLogEntryIsToolLike(entry: WorkLogEntry): boolean { /** Heuristic: providers often emit successful lifecycle status while error text lives in `detail` / `command`. */ function toolDetailTextLooksLikeFailure(text: string): boolean { const t = text.toLowerCase(); - if (t.includes("file not found")) { - return true; - } - if (t.includes("no files found")) { - return true; - } - if ( + return ( + t.includes("file not found") || + t.includes("no files found") || t.includes("enoent") || - t.includes("no such file or directory") || - t.includes("no such file") - ) { - return true; - } - if (t.includes("cannot find path") && t.includes("because it does not exist")) { - return true; - } - if (t.includes("commandnotfoundexception")) { - return true; - } - if (t.includes("is not recognized as the name of a cmdlet")) { - return true; - } - if (t.includes("is not recognized") && t.includes("the term '")) { - return true; - } - if (t.includes("a parameter cannot be found that matches parameter name")) { - return true; - } - if (t.includes("command not found")) { - return true; - } - if (//i.test(text)) { - return true; - } - if (/exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text)) { - return true; - } - if (/exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text)) { - return true; - } - return false; + t.includes("no such file") || + (t.includes("cannot find path") && t.includes("because it does not exist")) || + t.includes("commandnotfoundexception") || + t.includes("is not recognized as the name of a cmdlet") || + (t.includes("is not recognized") && t.includes("the term '")) || + t.includes("a parameter cannot be found that matches parameter name") || + t.includes("command not found") || + /exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text) || + /exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text) + ); } function workEntryIndicatesToolFailureFromOutput( diff --git a/infra/relay/src/environments/ManagedTunnelLimits.test.ts b/infra/relay/src/environments/ManagedTunnelLimits.test.ts index b53a487ee75a..f8ec6b772aa7 100644 --- a/infra/relay/src/environments/ManagedTunnelLimits.test.ts +++ b/infra/relay/src/environments/ManagedTunnelLimits.test.ts @@ -112,10 +112,13 @@ describe("ManagedTunnelLimits", () => { }).pipe(Effect.provide(layerWithDb(fakeDb))); }); - it.effect("retains database failures with operation and user identity", () => { + it.effect.each([ + { rows: "overrideRows", operation: "load-limit" }, + { rows: "countRows", operation: "count-tunnels" }, + ] as const)("retains $rows failures with operation and user identity", ({ rows, operation }) => { const cause = new Error("database unavailable"); const fakeDb = makeFakeDb({ - overrideRows: Effect.fail(cause), + [rows]: Effect.fail(cause), }); return Effect.gen(function* () { @@ -126,30 +129,10 @@ describe("ManagedTunnelLimits", () => { expect(error).toMatchObject({ _tag: "ManagedTunnelLimitPersistenceError", - operation: "load-limit", + operation, userId: "user-1", }); expect(error).toHaveProperty("cause", cause); }).pipe(Effect.provide(layerWithDb(fakeDb))); }); - - it.effect("retains count failures with operation and user identity", () => { - const cause = new Error("database unavailable"); - const fakeDb = makeFakeDb({ - countRows: Effect.fail(cause), - }); - - return Effect.gen(function* () { - const limits = yield* ManagedTunnelLimits.ManagedTunnelLimits; - const error = yield* Effect.flip( - limits.ensureCapacity({ userId: "user-1", environmentId: "environment-1" }), - ); - - expect(error).toMatchObject({ - _tag: "ManagedTunnelLimitPersistenceError", - operation: "count-tunnels", - userId: "user-1", - }); - }).pipe(Effect.provide(layerWithDb(fakeDb))); - }); }); diff --git a/packages/contracts/src/browserProfile.test.ts b/packages/contracts/src/browserProfile.test.ts index d53423bd6eef..e2e2d9498d5e 100644 --- a/packages/contracts/src/browserProfile.test.ts +++ b/packages/contracts/src/browserProfile.test.ts @@ -13,6 +13,8 @@ import { type BrowserProfile, } from "./browserProfile.ts"; +const isBrowserProfileId = Schema.is(BrowserProfileId); + const work: BrowserProfile = { id: "profile-work", name: "Work", kind: "persistent" }; describe("resolveBrowserProfiles", () => { @@ -91,21 +93,13 @@ describe("resolveBrowserProfiles normalization", () => { kind: "persistent", }); }); - - it("still lets the built-in incognito profile stay ephemeral", () => { - const incognito = resolveBrowserProfiles([]).find( - (profile) => profile.id === INCOGNITO_BROWSER_PROFILE_ID, - ); - - expect(incognito?.kind).toBe("incognito"); - }); }); describe("BrowserProfileId", () => { it("rejects control characters", () => { // Ids are folded into delimiter-joined cache keys on the client, so one // carrying the delimiter would resolve to another profile's partition. - expect(Schema.is(BrowserProfileId)("profile-a\u0000b")).toBe(false); - expect(Schema.is(BrowserProfileId)("profile-a")).toBe(true); + expect(isBrowserProfileId("profile-a\u0000b")).toBe(false); + expect(isBrowserProfileId("profile-a")).toBe(true); }); }); diff --git a/packages/shared/src/cliArgs.test.ts b/packages/shared/src/cliArgs.test.ts index 1fc6ab0215e4..fc61b109e6ec 100644 --- a/packages/shared/src/cliArgs.test.ts +++ b/packages/shared/src/cliArgs.test.ts @@ -24,86 +24,36 @@ describe("tokenizeCliArgs", () => { }); describe("parseCliArgs", () => { - it("returns empty result for empty string", () => { - expect(parseCliArgs("")).toEqual({ flags: {}, positionals: [] }); - }); - - it("returns empty result for whitespace-only string", () => { - expect(parseCliArgs(" ")).toEqual({ flags: {}, positionals: [] }); - }); - - it("returns empty result for empty array", () => { - expect(parseCliArgs([])).toEqual({ flags: {}, positionals: [] }); - }); - - it("parses --chrome boolean flag", () => { - expect(parseCliArgs("--chrome")).toEqual({ - flags: { chrome: null }, - positionals: [], - }); - }); - - it("parses --chrome with --verbose", () => { - expect(parseCliArgs("--chrome --verbose")).toEqual({ - flags: { chrome: null, verbose: null }, - positionals: [], - }); - }); - - it("parses --effort with a value", () => { - expect(parseCliArgs("--effort high")).toEqual({ - flags: { effort: "high" }, - positionals: [], - }); - }); - - it("parses --chrome --effort high --debug", () => { - expect(parseCliArgs("--chrome --effort high --debug")).toEqual({ - flags: { chrome: null, effort: "high", debug: null }, - positionals: [], - }); - }); - - it("parses --model with full model name", () => { - expect(parseCliArgs("--model claude-sonnet-4-6")).toEqual({ - flags: { model: "claude-sonnet-4-6" }, - positionals: [], - }); - }); - - it("parses --append-system-prompt with value and --chrome", () => { - expect(parseCliArgs("--append-system-prompt always-think-step-by-step --chrome")).toEqual({ - flags: { "append-system-prompt": "always-think-step-by-step", chrome: null }, - positionals: [], - }); - }); - - it("parses quoted --append-system-prompt with value and --chrome", () => { - expect(parseCliArgs(`--append-system-prompt "always think step by step" --chrome`)).toEqual({ - flags: { "append-system-prompt": "always think step by step", chrome: null }, - positionals: [], - }); - }); - - it("parses --max-budget-usd with numeric value", () => { - expect(parseCliArgs("--chrome --max-budget-usd 5.00")).toEqual({ - flags: { chrome: null, "max-budget-usd": "5.00" }, - positionals: [], - }); - }); - - it("parses --effort=high syntax", () => { - expect(parseCliArgs("--effort=high")).toEqual({ - flags: { effort: "high" }, - positionals: [], - }); - }); - - it("parses --key=value mixed with boolean flags", () => { - expect(parseCliArgs("--chrome --model=claude-sonnet-4-6 --debug")).toEqual({ - flags: { chrome: null, model: "claude-sonnet-4-6", debug: null }, - positionals: [], - }); + it.each([{ input: "" }, { input: " " }, { input: [] }, { input: "--" }])( + "returns an empty result for $input", + ({ input }) => { + expect(parseCliArgs(input)).toEqual({ flags: {}, positionals: [] }); + }, + ); + + it.each([ + ["--chrome", { chrome: null }], + ["--chrome --verbose", { chrome: null, verbose: null }], + ["--effort high", { effort: "high" }], + ["--chrome --effort high --debug", { chrome: null, effort: "high", debug: null }], + ["--model claude-sonnet-4-6", { model: "claude-sonnet-4-6" }], + [ + "--append-system-prompt always-think-step-by-step --chrome", + { "append-system-prompt": "always-think-step-by-step", chrome: null }, + ], + [ + '--append-system-prompt "always think step by step" --chrome', + { "append-system-prompt": "always think step by step", chrome: null }, + ], + ["--chrome --max-budget-usd 5.00", { chrome: null, "max-budget-usd": "5.00" }], + ["--effort=high", { effort: "high" }], + [ + "--chrome --model=claude-sonnet-4-6 --debug", + { chrome: null, model: "claude-sonnet-4-6", debug: null }, + ], + [" --chrome --verbose ", { chrome: null, verbose: null }], + ])("parses flags from %s", (input, flags) => { + expect(parseCliArgs(input)).toEqual({ flags, positionals: [] }); }); it("collects positional arguments", () => { @@ -120,17 +70,6 @@ describe("parseCliArgs", () => { }); }); - it("handles extra whitespace between tokens", () => { - expect(parseCliArgs(" --chrome --verbose ")).toEqual({ - flags: { chrome: null, verbose: null }, - positionals: [], - }); - }); - - it("ignores bare -- with no flag name", () => { - expect(parseCliArgs("--")).toEqual({ flags: {}, positionals: [] }); - }); - it("boolean flag does not consume next token as value", () => { expect(parseCliArgs(["--github-output", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual( { diff --git a/packages/shared/src/git.test.ts b/packages/shared/src/git.test.ts index 8dea20f0b423..3e4e9e0811aa 100644 --- a/packages/shared/src/git.test.ts +++ b/packages/shared/src/git.test.ts @@ -11,55 +11,43 @@ import { } from "./git.ts"; describe("normalizeGitRemoteUrl", () => { - it("canonicalizes equivalent GitHub remotes across protocol variants", () => { - expect(normalizeGitRemoteUrl("git@github.com:T3Tools/T3Code.git")).toBe( - "github.com/t3tools/t3code", - ); - expect(normalizeGitRemoteUrl("https://github.com/T3Tools/T3Code.git")).toBe( - "github.com/t3tools/t3code", - ); - expect(normalizeGitRemoteUrl("ssh://git@github.com/T3Tools/T3Code")).toBe( - "github.com/t3tools/t3code", - ); + it.each([ + "git@github.com:T3Tools/T3Code.git", + "https://github.com/T3Tools/T3Code.git", + "ssh://git@github.com/T3Tools/T3Code", + ])("canonicalizes GitHub remote %s", (remote) => { + expect(normalizeGitRemoteUrl(remote)).toBe("github.com/t3tools/t3code"); }); - it("preserves nested group paths for providers like GitLab", () => { - expect(normalizeGitRemoteUrl("git@gitlab.com:T3Tools/platform/T3Code.git")).toBe( - "gitlab.com/t3tools/platform/t3code", - ); - expect(normalizeGitRemoteUrl("https://gitlab.com/T3Tools/platform/T3Code.git")).toBe( - "gitlab.com/t3tools/platform/t3code", - ); + it.each([ + "git@gitlab.com:T3Tools/platform/T3Code.git", + "https://gitlab.com/T3Tools/platform/T3Code.git", + ])("preserves nested group paths in %s", (remote) => { + expect(normalizeGitRemoteUrl(remote)).toBe("gitlab.com/t3tools/platform/t3code"); }); - it("drops explicit ports from URL-shaped remotes", () => { - expect(normalizeGitRemoteUrl("https://gitlab.company.com:8443/team/project.git")).toBe( - "gitlab.company.com/team/project", - ); - expect(normalizeGitRemoteUrl("ssh://git@gitlab.company.com:2222/team/project.git")).toBe( - "gitlab.company.com/team/project", - ); + it.each([ + "https://gitlab.company.com:8443/team/project.git", + "ssh://git@gitlab.company.com:2222/team/project.git", + ])("drops explicit ports from %s", (remote) => { + expect(normalizeGitRemoteUrl(remote)).toBe("gitlab.company.com/team/project"); }); - it("normalizes SCP-like remotes with non-git SSH users", () => { - expect(normalizeGitRemoteUrl("gitlab@gitlab.example.com:group/project.git")).toBe( - "gitlab.example.com/group/project", - ); - expect(normalizeGitRemoteUrl("deploy@bitbucket.org:workspace/repo.git")).toBe( - "bitbucket.org/workspace/repo", - ); + it.each([ + ["gitlab@gitlab.example.com:group/project.git", "gitlab.example.com/group/project"], + ["deploy@bitbucket.org:workspace/repo.git", "bitbucket.org/workspace/repo"], + ])("normalizes non-git SSH user in %s", (remote, expected) => { + expect(normalizeGitRemoteUrl(remote)).toBe(expected); }); }); describe("parseGitHubRepositoryNameWithOwnerFromRemoteUrl", () => { - it("extracts the owner and repository from common GitHub remote shapes", () => { - expect( - parseGitHubRepositoryNameWithOwnerFromRemoteUrl("git@github.com:T3Tools/T3Code.git"), - ).toBe("T3Tools/T3Code"); - expect( - parseGitHubRepositoryNameWithOwnerFromRemoteUrl("https://github.com/T3Tools/T3Code.git"), - ).toBe("T3Tools/T3Code"); - }); + it.each(["git@github.com:T3Tools/T3Code.git", "https://github.com/T3Tools/T3Code.git"])( + "extracts the owner and repository from %s", + (remote) => { + expect(parseGitHubRepositoryNameWithOwnerFromRemoteUrl(remote)).toBe("T3Tools/T3Code"); + }, + ); }); describe("isTemporaryWorktreeBranch", () => { diff --git a/packages/shared/src/terminalLabels.test.ts b/packages/shared/src/terminalLabels.test.ts index b8a146b0cc2e..99c8376f6903 100644 --- a/packages/shared/src/terminalLabels.test.ts +++ b/packages/shared/src/terminalLabels.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; -import type { TerminalSummary } from "@t3tools/contracts"; import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts"; import { getTerminalLabel, nextTerminalId, resolveTerminalSessionLabel } from "./terminalLabels.ts"; @@ -20,8 +19,7 @@ describe("getTerminalLabel", () => { describe("resolveTerminalSessionLabel", () => { it("prefers a non-empty summary label", () => { - const summary = { label: " bun " } as Pick; - expect(resolveTerminalSessionLabel("term-1", summary)).toBe("bun"); + expect(resolveTerminalSessionLabel("term-1", { label: " bun " })).toBe("bun"); }); it("falls back to getTerminalLabel when summary is missing or blank", () => { @@ -32,22 +30,15 @@ describe("resolveTerminalSessionLabel", () => { }); describe("nextTerminalId", () => { - it("allocates term-1 when no terminals are listed yet", () => { - expect(nextTerminalId([])).toBe(DEFAULT_TERMINAL_ID); - }); - - it("allocates term-2 when only term-1 exists", () => { - expect(nextTerminalId([DEFAULT_TERMINAL_ID])).toBe("term-2"); - }); - - it("skips over taken term-N slots", () => { - expect(nextTerminalId([DEFAULT_TERMINAL_ID, "term-2", "term-3"])).toBe("term-4"); - expect(nextTerminalId([DEFAULT_TERMINAL_ID, "term-3"])).toBe("term-2"); - expect(nextTerminalId(["term-2", "term-3"])).toBe("term-1"); - }); - - it("ignores blank/whitespace-only ids", () => { - expect(nextTerminalId(["", " ", DEFAULT_TERMINAL_ID])).toBe("term-2"); - expect(nextTerminalId(["", " "])).toBe("term-1"); + it.each([ + [[], DEFAULT_TERMINAL_ID], + [[DEFAULT_TERMINAL_ID], "term-2"], + [[DEFAULT_TERMINAL_ID, "term-2", "term-3"], "term-4"], + [[DEFAULT_TERMINAL_ID, "term-3"], "term-2"], + [["term-2", "term-3"], "term-1"], + [["", " ", DEFAULT_TERMINAL_ID], "term-2"], + [["", " "], "term-1"], + ])("allocates the lowest unused id for %j: %s", (existingIds, expected) => { + expect(nextTerminalId(existingIds)).toBe(expected); }); }); diff --git a/packages/shared/src/terminalLabels.ts b/packages/shared/src/terminalLabels.ts index 1cdae101430c..8ec2be7d6d64 100644 --- a/packages/shared/src/terminalLabels.ts +++ b/packages/shared/src/terminalLabels.ts @@ -3,11 +3,7 @@ import type { TerminalSummary } from "@t3tools/contracts"; /** Human-readable label for a terminal tab; matches mobile and web sidebars. */ export function getTerminalLabel(terminalId: string): string { const numericSuffix = /^term(?:inal)?-(\d+)$/i.exec(terminalId)?.[1]; - if (numericSuffix) { - return `Terminal ${numericSuffix}`; - } - - return terminalId; + return numericSuffix ? `Terminal ${numericSuffix}` : terminalId; } /** Prefer server summary label when present; otherwise fall back to `getTerminalLabel`. */ @@ -15,22 +11,15 @@ export function resolveTerminalSessionLabel( terminalId: string, summary: Pick | null | undefined, ): string { - const trimmed = summary?.label?.trim(); - if (trimmed && trimmed.length > 0) { - return trimmed; - } - return getTerminalLabel(terminalId); + return summary?.label?.trim() || getTerminalLabel(terminalId); } /** - * Client-side terminal id allocator. Ids are ALWAYS chosen by the client and sent explicitly - * on every `terminal.open` / `terminal.attach` call — the server never allocates. - * - * Returns the lowest unused `term-N` id (starting at `term-1`), skipping any ids already in - * `existingTerminalIds`. + * Allocate the lowest unused `term-N` id, starting at `term-1`. + * Clients send the id explicitly on `terminal.open` / `terminal.attach`. */ export function nextTerminalId(existingTerminalIds: ReadonlyArray): string { - const usedIds = new Set(existingTerminalIds.filter((id) => id.trim().length > 0)); + const usedIds = new Set(existingTerminalIds); let nextIndex = 1; while (usedIds.has(`term-${nextIndex}`)) { nextIndex += 1; diff --git a/packages/shared/src/threadEnvMode.test.ts b/packages/shared/src/threadEnvMode.test.ts index 4cf22c248868..203205025d6f 100644 --- a/packages/shared/src/threadEnvMode.test.ts +++ b/packages/shared/src/threadEnvMode.test.ts @@ -3,63 +3,32 @@ import { describe, expect, it } from "vite-plus/test"; import { isDefaultThreadEnvModeSettled, resolveDefaultThreadEnvMode } from "./threadEnvMode.ts"; describe("resolveDefaultThreadEnvMode", () => { - it("prefers the project setting over t3.json over the global default", () => { - expect( - resolveDefaultThreadEnvMode({ - projectSetting: "local", - projectFile: "worktree", - globalDefault: "worktree", - }), - ).toBe("local"); - expect( - resolveDefaultThreadEnvMode({ - projectSetting: null, - projectFile: "local", - globalDefault: "worktree", - }), - ).toBe("local"); - expect( - resolveDefaultThreadEnvMode({ - projectSetting: undefined, - projectFile: null, - globalDefault: "worktree", - }), - ).toBe("worktree"); - }); + it.each([ + ["local", "worktree", "worktree", "local"], + [null, "local", "worktree", "local"], + [undefined, null, "worktree", "worktree"], + ] as const)( + "resolves project %s, file %s, global %s to %s", + (projectSetting, projectFile, globalDefault, expected) => { + expect(resolveDefaultThreadEnvMode({ projectSetting, projectFile, globalDefault })).toBe( + expected, + ); + }, + ); }); describe("isDefaultThreadEnvModeSettled", () => { - it("settles on an explicit pick or project setting even while the file loads", () => { - expect( - isDefaultThreadEnvModeSettled({ - explicitMode: "local", - projectSetting: null, - projectFilePending: true, - }), - ).toBe(true); - expect( - isDefaultThreadEnvModeSettled({ - explicitMode: undefined, - projectSetting: "worktree", - projectFilePending: true, - }), - ).toBe(true); - }); - - it("stays unsettled only while a consulted file read is pending", () => { - expect( - isDefaultThreadEnvModeSettled({ - explicitMode: undefined, - projectSetting: null, - projectFilePending: true, - }), - ).toBe(false); - expect( - isDefaultThreadEnvModeSettled({ - explicitMode: undefined, - projectSetting: null, - projectFilePending: false, - }), - ).toBe(true); - }); + it.each([ + ["local", null, true, true], + [undefined, "worktree", true, true], + [undefined, null, true, false], + [undefined, null, false, true], + ] as const)( + "settles explicit %s, project %s, file pending %s: %s", + (explicitMode, projectSetting, projectFilePending, expected) => { + expect( + isDefaultThreadEnvModeSettled({ explicitMode, projectSetting, projectFilePending }), + ).toBe(expected); + }, + ); }); diff --git a/scripts/lib/build-target-arch.ts b/scripts/lib/build-target-arch.ts index eb804b226eae..adb049f46673 100644 --- a/scripts/lib/build-target-arch.ts +++ b/scripts/lib/build-target-arch.ts @@ -23,9 +23,6 @@ function normalizeWindowsArch(value: string | undefined): BuildArch | undefined return undefined; } -const optionToUndefined = (value: Option.Option): A | undefined => - Option.getOrUndefined(value); - const resolveHostProcessArch = Effect.fn("resolveHostProcessArch")(function* () { const platform = yield* HostProcessPlatform; const processArch = yield* HostProcessArchitecture; @@ -37,8 +34,8 @@ const resolveHostProcessArch = Effect.fn("resolveHostProcessArch")(function* () // still reports ARM64 via the processor environment variables. const env = yield* WindowsProcessorArchitectureConfig; return ( - normalizeWindowsArch(optionToUndefined(env.processorArchitectureW6432)) ?? - normalizeWindowsArch(optionToUndefined(env.processorArchitecture)) ?? + normalizeWindowsArch(Option.getOrUndefined(env.processorArchitectureW6432)) ?? + normalizeWindowsArch(Option.getOrUndefined(env.processorArchitecture)) ?? "x64" ); } diff --git a/scripts/lib/cli-external-packages.test.ts b/scripts/lib/cli-external-packages.test.ts index e894359eccb5..e19bdfbce6b0 100644 --- a/scripts/lib/cli-external-packages.test.ts +++ b/scripts/lib/cli-external-packages.test.ts @@ -16,11 +16,7 @@ import { shouldBundleCliDependency, } from "./cli-external-packages.ts"; -// Only the field this test cares about; decoding ignores everything else. -// optionalDependencies matter as much as dependencies here: every native family -// in the list declares its actual platform bindings there (ffi-rs -> @yuuang/*, -// msgpackr-extract -> @msgpackr-extract/*, fff-node -> @ff-labs/fff-bin-*), so -// reading only `dependencies` would check nothing for exactly those packages. +// Native platform bindings are often optional dependencies. const PackageManifest = Schema.Struct({ dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), optionalDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), @@ -60,9 +56,6 @@ describe("shouldBundleCliDependency", () => { assert.strictEqual(shouldBundleCliDependency("@effect/sql-sqlite-bun"), false); }); - // The real package is `node-gyp-build-optional-packages`, reached by prefix. - // It is transitive to a selected dependency root, so the runtime closure test - // below ensures it follows that root into the sidecar. it("treats prefix-matched siblings as external", () => { assert.strictEqual(shouldBundleCliDependency("node-gyp-build-optional-packages"), false); }); @@ -92,22 +85,9 @@ describe("selectCliRuntimeExternalDependencies", () => { }); }); -// An external package is loaded from the real filesystem, so its own `require` -// also resolves from the real filesystem. If one of its dependencies was -// bundled away instead of left external, that dependency does not follow the -// selected root into the sidecar. -// -// Found the hard way: node-gyp-build-optional-packages requires detect-libc, -// which was bundled. Windows was fine; WSL got MODULE_NOT_FOUND. +// External loaders need their transitive dependencies on disk, including detect-libc on WSL. it.layer(NodeServices.layer)("external package dependency closure", (it) => { - // Read manifests off disk from the pnpm store rather than resolving them. - // `require("/package.json")` cannot do this job: under pnpm isolation a - // transitive package (detect-libc, msgpackr-extract, ffi-rs) is not reachable - // by name from this file at all, and an `exports` map can refuse the - // `/package.json` subpath outright (@ff-labs/fff-node). Both surface as "not - // installed", which would let this test skip everything and pass while - // checking nothing. The store contains the dependency graph the sidecar's - // minimal production install resolves. + // Read the pnpm store directly: isolation and exports maps block package.json resolution. const readInstalledPackages = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -116,10 +96,7 @@ it.layer(NodeServices.layer)("external package dependency closure", (it) => { "../../node_modules/.pnpm", ); - // The store holds regular files too (lock.yaml), so a path built under one - // raises ENOTDIR rather than reporting absence. That throws on Linux while - // Windows quietly returns false, which is exactly the kind of difference - // this test exists to catch, so treat any failure as "not there". + // Store files such as lock.yaml produce ENOTDIR for nested paths on Linux. const isPresent = (candidate: string) => fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false)); @@ -148,8 +125,7 @@ it.layer(NodeServices.layer)("external package dependency closure", (it) => { return installed; }).pipe(Effect.cached, Effect.runSync); - // Runtime-external only. The build-only entries resolve `bun:*` and are never - // loaded by Node, so their closure genuinely does not need to be external. + // Node never loads the build-only Bun imports. const isRuntimeExternal = (name: string) => CLI_RUNTIME_EXTERNAL_PREFIXES.some((prefix) => name.startsWith(prefix)); @@ -162,9 +138,7 @@ it.layer(NodeServices.layer)("external package dependency closure", (it) => { const installed = yield* readInstalledPackages; const found = [...installed.keys()].filter(isRuntimeExternal); - // Without this the closure check below can pass vacuously: if nothing is - // read, nothing is checked. These are the packages whose closure actually - // broke WSL, so require them by name. + // Require the packages behind the WSL regression so an empty scan cannot pass. for (const required of ["node-pty", "node-gyp-build-optional-packages", "detect-libc"]) { assert.ok( found.includes(required), @@ -180,10 +154,7 @@ it.layer(NodeServices.layer)("external package dependency closure", (it) => { const installed = yield* readInstalledPackages; const violations: string[] = []; const seen = new Set(); - // Seeded from what is actually installed and matches a prefix, so scoped - // prefixes like "@yuuang/" and "@ff-labs/" are covered too. Seeding from - // the prefix strings themselves would skip every scoped entry, since a - // prefix is not a package name. + // Expand scoped prefixes to installed package names before walking dependencies. const queue = [...installed.keys()].filter(isRuntimeExternal); for (const name of queue) { @@ -194,9 +165,9 @@ it.layer(NodeServices.layer)("external package dependency closure", (it) => { if (!manifest) continue; const declared = { - ...(manifest.dependencies ?? {}), - ...(manifest.optionalDependencies ?? {}), - ...(manifest.peerDependencies ?? {}), + ...manifest.dependencies, + ...manifest.optionalDependencies, + ...manifest.peerDependencies, }; for (const dependency of Object.keys(declared)) { if (!isRuntimeExternal(dependency)) { @@ -215,8 +186,6 @@ it.layer(NodeServices.layer)("external package dependency closure", (it) => { ); }); -// Configuring the bundler is not the same as checking what it emitted. These -// exercise the scanner against the marker shape rolldown actually produces. describe("findInlinedExternalPackages", () => { const region = (path: string) => `//#region ${path} var x = 1; @@ -252,11 +221,7 @@ var x = 1; assert.strictEqual(result.regionCount, 2); }); - // regionCount is what separates "clean" from "this scan went blind because the - // marker format changed". A caller that ignores it gets a vacuous pass. - // The scan has to answer both directions. Checking only that externals are - // absent still passes on a bundle that externalized everything, which is the - // failure this whole change prevents. + // Verify ordinary packages are bundled too; an all-external bundle must not pass. it("reports the packages that were inlined, not just the violations", () => { const source = region("../../node_modules/.pnpm/effect@4.0.0/node_modules/effect/dist/index.js") + diff --git a/scripts/lib/cli-external-packages.ts b/scripts/lib/cli-external-packages.ts index d7a89bc408a4..805e0282001b 100644 --- a/scripts/lib/cli-external-packages.ts +++ b/scripts/lib/cli-external-packages.ts @@ -1,29 +1,7 @@ /** - * The single source of truth for packages the server CLI bundle must NOT inline. - * - * Two consumers derive from this list, and they must never disagree: - * - * - apps/server/vite.config.ts decides what stays external to the bundle. - * - scripts/build-desktop-artifact.ts selects the runtime dependency roots for - * the Windows server sidecar. - * - * A runtime package that is external but absent from the sidecar fails as soon - * as Node resolves it from the emitted bundle. Keeping both consumers on one - * list prevents packaging from drifting away from the bundle boundary. - * - * Entries are matched as prefixes (`id.startsWith(prefix)`), so they also cover - * a package's platform-specific siblings — `node-gyp-build` covers - * `node-gyp-build-optional-packages`, `@yuuang/` covers every `ffi-rs-*` binding. - */ -/** - * External because Node actually loads them from disk at runtime. - * - * Native addons (.node), the JS wrappers that dlopen them by real path, and — - * critically — the ordinary JS packages those wrappers require. An external - * package is loaded from the real filesystem, so its own `require` also - * resolves from the real filesystem; a dependency that was bundled away exists - * only inside the emitted bundle and is unreachable there. This closure is - * enforced by a test, not by inspection. + * Shared by the CLI bundler and Windows sidecar dependency selection. + * Prefixes include platform siblings and native loaders' transitive dependencies: + * external packages resolve their imports from disk, outside the bundle. */ export const CLI_RUNTIME_EXTERNAL_PREFIXES = [ "node-pty", @@ -35,29 +13,14 @@ export const CLI_RUNTIME_EXTERNAL_PREFIXES = [ "msgpackr-extract", "node-gyp-build", "node-addon-api", - // Required by node-gyp-build-optional-packages. Not native, but in the - // closure: without it, WSL gets MODULE_NOT_FOUND while Windows is fine. + // node-gyp-build-optional-packages requires this on Linux/WSL. "detect-libc", - // ws's optional accelerators. Nothing in this repo declares them, so they are - // not in the staged production install and the packaged app does not ship - // them either way -- ws wraps the require in try/catch and falls back to its - // JS paths. They are listed because they were being inlined from the dev - // store: both carry binding.gyp and prebuilds and load through - // node-gyp-build, and a native loader inlined into a bundle chunk searches - // for prebuilds that cannot be beside it. Listing them keeps that from - // becoming real if either is ever declared as a dependency. + // Optional ws addons must stay external so native loaders can locate prebuilds. "bufferutil", "utf-8-validate", ] as const; -/** - * External only so the bundler never has to resolve them. - * - * These are reached through a runtime-conditional dynamic import that Node - * never takes, and they resolve `bun:*` specifiers that do not exist when - * bundling for Node. Because Node never loads them, their dependency closure - * does not need to be external — only the entry point must stay unbundled. - */ +/** Bun-only imports stay unbundled because Node cannot resolve their bun:* specifiers. */ export const CLI_BUILD_ONLY_EXTERNAL_PREFIXES = [ "@effect/platform-bun", "@effect/sql-sqlite-bun", @@ -72,16 +35,7 @@ export function isRuntimeExternalCliDependency(id: string): boolean { return CLI_RUNTIME_EXTERNAL_PREFIXES.some((prefix) => id.startsWith(prefix)); } -/** - * True when `id` must stay out of the bundle. - * - * This has to be wired to the bundler's `neverBundle`, not just to - * `alwaysBundle`. `alwaysBundle` only forces packages IN — returning false from - * it means "no opinion", and the default then applies: a declared dependency - * stays external, but a transitive one gets bundled. That is how - * msgpackr-extract, node-gyp-build-optional-packages and detect-libc ended up - * inlined while node-pty (a declared dependency) stayed external. - */ +/** Use with neverBundle; returning false from alwaysBundle still bundles transitive imports. */ export function isExternalCliDependency(id: string): boolean { return CLI_EXTERNAL_PACKAGE_PREFIXES.some((prefix) => id.startsWith(prefix)); } @@ -102,24 +56,8 @@ export function selectCliRuntimeExternalDependencies( } /** - * Scan an emitted bundle chunk for runtime-external packages that were inlined. - * - * Configuring the bundler is not the same as checking what it produced. The - * `alwaysBundle` predicate only forces packages IN; returning false from it - * means "no opinion", so a transitive dependency still gets bundled by default. - * msgpackr-extract, node-gyp-build-optional-packages and detect-libc were - * inlined that way while every list-based test passed, which is why this reads - * the artifact instead. - * - * `regionCount` is reported so the caller can tell "nothing was inlined" apart - * from "the marker format changed and this scan no longer sees anything". - * - * `inlinedPackages` is every package seen in a region, which lets the caller - * check the opposite direction too. Verifying only that externals are absent - * would still pass if the bundler reverted to leaving everything external: the - * scan would see source-file regions, report nothing inlined, and the packaged - * backends would then fail with ERR_MODULE_NOT_FOUND because those packages - * are not in the selected sidecar closure either. + * Scan Rolldown regions for inlined packages. regionCount detects missing markers; + * inlinedPackages lets callers verify that ordinary dependencies were bundled too. */ export function findInlinedExternalPackages(source: string): { readonly regionCount: number; diff --git a/scripts/lib/public-config.test.ts b/scripts/lib/public-config.test.ts index 62d383484cf4..f576b202645e 100644 --- a/scripts/lib/public-config.test.ts +++ b/scripts/lib/public-config.test.ts @@ -18,27 +18,7 @@ describe("loadRepoEnv", () => { it("does not project cloud configuration for an unconfigured clone", () => { const env = loadRepoEnv({ baseEnv: {}, repoRoot: makeTemporaryDirectory() }); - expect(env.T3CODE_CLERK_PUBLISHABLE_KEY).toBeUndefined(); - expect(env.T3CODE_CLERK_CLI_OAUTH_CLIENT_ID).toBeUndefined(); - expect(env.VITE_CLERK_PUBLISHABLE_KEY).toBeUndefined(); - expect(env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY).toBeUndefined(); - expect(env.T3CODE_CLERK_JWT_TEMPLATE).toBeUndefined(); - expect(env.VITE_CLERK_JWT_TEMPLATE).toBeUndefined(); - expect(env.EXPO_PUBLIC_CLERK_JWT_TEMPLATE).toBeUndefined(); - expect(env.T3CODE_RELAY_URL).toBeUndefined(); - expect(env.VITE_T3CODE_RELAY_URL).toBeUndefined(); - expect(env.T3CODE_MOBILE_OTLP_TRACES_URL).toBeUndefined(); - expect(env.T3CODE_MOBILE_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.T3CODE_MOBILE_OTLP_TRACES_TOKEN).toBeUndefined(); - expect(env.EXPO_PUBLIC_OTLP_TRACES_URL).toBeUndefined(); - expect(env.EXPO_PUBLIC_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.EXPO_PUBLIC_OTLP_TRACES_TOKEN).toBeUndefined(); - expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL).toBeUndefined(); - expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN).toBeUndefined(); - expect(env.VITE_RELAY_OTLP_TRACES_URL).toBeUndefined(); - expect(env.VITE_RELAY_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.VITE_RELAY_OTLP_TRACES_TOKEN).toBeUndefined(); + expect(env).toStrictEqual({}); }); it("applies process, root local, and root precedence in that order", () => { From 8c3cf868e993c7981c3dbcd1e01182e009765ffd Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 5 Sep 2026 06:22:56 +0000 Subject: [PATCH 2/2] refactor: consolidate duplicate runtime logic and test scaffolding --- apps/desktop/src/preview/Manager.test.ts | 477 +-- apps/mobile/src/lib/threadActivity.ts | 414 +-- apps/server/src/orchestration/Errors.ts | 23 - .../Layers/ProjectionSnapshotQuery.test.ts | 2549 +++++++---------- .../Layers/ProjectionSnapshotQuery.ts | 436 +-- apps/server/src/orchestration/decider.ts | 313 +- .../src/provider/Layers/ClaudeAdapter.test.ts | 468 +-- .../provider/Layers/ProviderService.test.ts | 138 +- apps/server/src/provider/providerSnapshot.ts | 14 - apps/server/src/server.test.ts | 113 +- .../src/sourceControl/AzureDevOpsCli.ts | 2 - apps/server/src/sourceControl/GitLabCli.ts | 1 - .../sourceControl/azureDevOpsPullRequests.ts | 4 +- .../src/sourceControl/gitHubPullRequests.ts | 4 +- .../src/sourceControl/gitLabMergeRequests.ts | 4 +- apps/web/src/components/CommandPalette.tsx | 77 +- .../src/components/GitActionsControl.logic.ts | 53 +- .../ProviderUpdateEnvironmentRows.test.tsx | 63 +- apps/web/src/components/color-selector.tsx | 101 - apps/web/src/session-logic.ts | 413 +-- .../src/agentActivity/ApnsDeliveries.test.ts | 156 +- packages/client-runtime/package.json | 4 + .../src/relay/managedRelay.test.ts | 160 +- packages/client-runtime/src/state/runtime.ts | 25 - .../src/state/threadCommands.ts | 168 +- .../src/state/threadReducer.test.ts | 406 +-- .../src/work-log/activity-parsing.ts | 405 +++ packages/contracts/src/ipc.ts | 236 +- packages/shared/src/model.ts | 5 - scripts/build-desktop-artifact.test.ts | 274 +- scripts/build-desktop-artifact.ts | 74 +- scripts/dev-runner.test.ts | 207 +- scripts/dev-runner.ts | 85 +- 33 files changed, 2435 insertions(+), 5437 deletions(-) delete mode 100644 apps/web/src/components/color-selector.tsx create mode 100644 packages/client-runtime/src/work-log/activity-parsing.ts diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts index 79c7fd1725e1..cf65f9bd71e4 100644 --- a/apps/desktop/src/preview/Manager.test.ts +++ b/apps/desktop/src/preview/Manager.test.ts @@ -246,6 +246,33 @@ const makeTestHostWebContents = (): TestHostWebContents => { }; }; +const makeWebviewDefaults = () => ({ + id: 42, + isDestroyed: () => false, + getType: () => "webview", + getURL: () => "https://example.com", + getTitle: () => "Example", + isLoading: () => false, + getZoomFactor: () => 1, + setZoomFactor: vi.fn(), + setAudioMuted: vi.fn(), + isCurrentlyAudible: () => false, + on: vi.fn(), + off: vi.fn(), + ipc: { on: vi.fn(), off: vi.fn() }, + send: webviewSend, + navigationHistory: { canGoBack: () => false, canGoForward: () => false }, + setIgnoreMenuShortcuts: vi.fn(), + setWindowOpenHandler: vi.fn(), + debugger: { + isAttached: () => false, + attach: vi.fn(), + sendCommand: vi.fn(async () => undefined), + on: vi.fn(), + off: vi.fn(), + }, +}); + const makeTestPreviewWebContents = ( capturePage: () => Promise, id = 42, @@ -253,34 +280,12 @@ const makeTestPreviewWebContents = ( ) => { const setBackgroundThrottling = vi.fn<(enabled: boolean) => void>(); return { + ...makeWebviewDefaults(), id, mainFrame: { routingId: id }, hostWebContents, executeJavaScript: vi.fn(async () => ({ width: 1280, height: 720 })), - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), setBackgroundThrottling, - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, capturePage, } as unknown as TestPreviewWebContents; }; @@ -361,17 +366,13 @@ const makeFaviconWebContents = (options?: { const off = vi.fn(); const debuggerOff = vi.fn(); const webContents = { + ...makeWebviewDefaults(), id: options?.id ?? 42, isDestroyed: () => destroyed, - getType: () => "webview", getURL: () => currentUrl, getTitle: () => "Preview", isLoading: () => loading, isDevToolsOpened: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, reload, reloadIgnoringCache: vi.fn(), loadURL, @@ -379,12 +380,7 @@ const makeFaviconWebContents = (options?: { listeners.set(event, listener); }), off, - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, session: { fetch }, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), executeJavaScriptInIsolatedWorld, debugger: { isAttached: () => false, @@ -662,33 +658,13 @@ describe("PreviewManager", () => { const loadURL = vi.fn(async () => undefined); const listeners = new Map void>(); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", + ...makeWebviewDefaults(), getURL: () => "about:blank", getTitle: () => "", - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, loadURL, on: vi.fn((event: string, listener: (...args: never[]) => void) => { listeners.set(event, listener); }), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); yield* manager.navigate("tab_pending", "localhost:3200"); @@ -764,26 +740,13 @@ describe("PreviewManager", () => { off: debuggerOff, }; fromId.mockReturnValue({ - id: 42, + ...makeWebviewDefaults(), isDestroyed: () => destroyed, - getType: () => "webview", getURL: () => "http://localhost:3200/", getTitle: () => "Preview", - isLoading: () => false, isDevToolsOpened: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, reload: vi.fn(), loadURL: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), get debugger() { if (destroyed) throw new Error("Object has been destroyed"); return wcDebugger; @@ -1267,35 +1230,16 @@ describe("PreviewManager", () => { const listeners = new Map void>(); const setZoomFactor = vi.fn(); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", + ...makeWebviewDefaults(), + setZoomFactor, getURL: () => url, - getTitle: () => "Example", - isLoading: () => false, getZoomFactor: () => { if (!zoomReadable) throw new Error("zoom unavailable"); return effectiveZoom; }, - setZoomFactor, - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); const states: PreviewManager.PreviewTabState[] = []; @@ -1337,30 +1281,10 @@ describe("PreviewManager", () => { const replacementSetZoomFactor = vi.fn(); fromId.mockReturnValue({ + ...makeWebviewDefaults(), id: 43, - isDestroyed: () => false, - getType: () => "webview", getURL: () => url, - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, setZoomFactor: replacementSetZoomFactor, - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); yield* manager.registerWebview("tab_zoom", 43); @@ -1378,30 +1302,8 @@ describe("PreviewManager", () => { Effect.gen(function* () { const setZoomFactor = vi.fn(); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, + ...makeWebviewDefaults(), setZoomFactor, - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); yield* manager.createTab("tab_reapply"); @@ -1425,30 +1327,8 @@ describe("PreviewManager", () => { Effect.gen(function* () { const setZoomFactor = vi.fn(); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, + ...makeWebviewDefaults(), setZoomFactor, - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => @@ -1480,24 +1360,9 @@ describe("PreviewManager", () => { return { sendCommand, wc: { + ...makeWebviewDefaults(), id, - isDestroyed: () => false, isDevToolsOpened: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), @@ -1569,15 +1434,9 @@ describe("PreviewManager", () => { audibleAfterFirstRead = true; }, wc: { + ...makeWebviewDefaults(), id, - isDestroyed: () => false, isDevToolsOpened: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), setAudioMuted, isCurrentlyAudible: () => { audibleReads += 1; @@ -1591,18 +1450,6 @@ describe("PreviewManager", () => { off: vi.fn((event: string) => { listeners.delete(event); }), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never, }; }; @@ -1913,32 +1760,13 @@ describe("PreviewManager", () => { let loading = false; const listeners = new Map void>(); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", + ...makeWebviewDefaults(), getURL: () => url, getTitle: () => "localhost:5733", isLoading: () => loading, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); const statuses: PreviewManager.PreviewNavStatus[] = []; @@ -2006,32 +1834,11 @@ describe("PreviewManager", () => { const capturePage = vi.fn(async () => ({ toPNG: () => png })); const listeners = new Map void>(); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", + ...makeWebviewDefaults(), getURL: () => "https://example.com:8443/path?query=value", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: never[]) => void) => { listeners.set(event, listener); }), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, capturePage, } as never); @@ -2561,29 +2368,16 @@ describe("PreviewManager", () => { sendCommand: typeof firstSendCommand, ) => ({ + ...makeWebviewDefaults(), id, mainFrame: { routingId: id }, hostWebContents: host, executeJavaScript: vi.fn(async () => id === 41 ? { width: 800, height: 600 } : { width: 390, height: 844 }, ), - isDestroyed: () => false, - getType: () => "webview", getURL: () => `https://example.com/${id}`, getTitle: () => `Example ${id}`, - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), setBackgroundThrottling: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), @@ -2866,34 +2660,11 @@ describe("PreviewManager", () => { getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue({ - id: 42, + ...makeWebviewDefaults(), mainFrame: { routingId: 42 }, hostWebContents: mainWindowWebContents, executeJavaScript: vi.fn(async () => ({ width: 1280, height: 720 })), - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), setBackgroundThrottling: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, capturePage, } as never); @@ -3368,36 +3139,15 @@ describe("PreviewManager", () => { Effect.gen(function* () { const listeners = new Map void>(); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, + ...makeWebviewDefaults(), isFocused: () => true, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), once: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), - off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn(), removeListener: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); yield* manager.createTab("tab_1"); @@ -3420,21 +3170,9 @@ describe("PreviewManager", () => { Effect.gen(function* () { let onPicked: ((event: unknown, ...args: unknown[]) => void) | undefined; fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, + ...makeWebviewDefaults(), isFocused: () => true, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), once: vi.fn(), - off: vi.fn(), - // A wedged compositor leaves `capturePage` pending forever. capturePage: vi.fn(() => new Promise(() => {})), ipc: { on: vi.fn((channel: string, listener: typeof onPicked) => { @@ -3443,17 +3181,6 @@ describe("PreviewManager", () => { off: vi.fn(), removeListener: vi.fn(), }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); yield* manager.createTab("tab_1"); @@ -3498,20 +3225,9 @@ describe("PreviewManager", () => { Effect.gen(function* () { let onPicked: ((event: unknown, ...args: unknown[]) => void) | undefined; fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, + ...makeWebviewDefaults(), isFocused: () => true, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), once: vi.fn(), - off: vi.fn(), capturePage: vi.fn(() => new Promise(() => {})), ipc: { on: vi.fn((channel: string, listener: typeof onPicked) => { @@ -3520,17 +3236,6 @@ describe("PreviewManager", () => { off: vi.fn(), removeListener: vi.fn(), }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); const annotation = { id: "annotation_1", @@ -3583,40 +3288,19 @@ describe("PreviewManager", () => { const goForward = vi.fn(); let canGoBack = true; fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), + ...makeWebviewDefaults(), ipc: { on: vi.fn((channel: string, listener: typeof mouseNavigate) => { if (channel === "preview:mouse-navigate") mouseNavigate = listener; }), off: vi.fn(), }, - send: webviewSend, navigationHistory: { canGoBack: () => canGoBack, canGoForward: () => true, goBack, goForward, }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), - debugger: { - isAttached: () => false, - attach: vi.fn(), - sendCommand: vi.fn(async () => undefined), - on: vi.fn(), - off: vi.fn(), - }, } as never); yield* manager.createTab("tab_nav"); @@ -3717,29 +3401,14 @@ describe("PreviewManager", () => { return undefined; }); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, + ...makeWebviewDefaults(), isDevToolsOpened: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), ipc: { on: vi.fn((channel: string, listener: typeof humanInput) => { if (channel === "preview:human-input") humanInput = listener; }), off: vi.fn(), }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), @@ -3817,30 +3486,15 @@ describe("PreviewManager", () => { focus: restoreFocus, } as never); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, + ...makeWebviewDefaults(), isDevToolsOpened: () => false, focus, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), ipc: { on: vi.fn((channel: string, listener: typeof humanInput) => { if (channel === "preview:human-input") humanInput = listener; }), off: vi.fn(), }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), @@ -3974,29 +3628,14 @@ describe("PreviewManager", () => { return undefined; }); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, + ...makeWebviewDefaults(), isDevToolsOpened: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), ipc: { on: vi.fn((channel: string, listener: typeof humanInput) => { if (channel === "preview:human-input") humanInput = listener; }), off: vi.fn(), }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), @@ -4044,24 +3683,8 @@ describe("PreviewManager", () => { method === "Runtime.evaluate" ? { exceptionDetails } : undefined, ); fromId.mockReturnValue({ - id: 42, - isDestroyed: () => false, - getType: () => "webview", - getURL: () => "https://example.com", - getTitle: () => "Example", - isLoading: () => false, + ...makeWebviewDefaults(), isDevToolsOpened: () => false, - getZoomFactor: () => 1, - setZoomFactor: vi.fn(), - setAudioMuted: vi.fn(), - isCurrentlyAudible: () => false, - on: vi.fn(), - off: vi.fn(), - ipc: { on: vi.fn(), off: vi.fn() }, - send: webviewSend, - navigationHistory: { canGoBack: () => false, canGoForward: () => false }, - setIgnoreMenuShortcuts: vi.fn(), - setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), diff --git a/apps/mobile/src/lib/threadActivity.ts b/apps/mobile/src/lib/threadActivity.ts index ef75fce56af4..dcdba40d1d50 100644 --- a/apps/mobile/src/lib/threadActivity.ts +++ b/apps/mobile/src/lib/threadActivity.ts @@ -1,8 +1,22 @@ +import { + isNoContentRuntimeWarning, + isPlanBoundaryToolActivity, + mergeChangedFiles, + asRecord, + asTrimmedString, + extractToolCommand, + extractToolTitle, + stripTrailingExitCode, + extractWorkLogItemType, + extractChangedFiles, + compareActivityLifecycleRank, + parseUserInputQuestions, +} from "@t3tools/client-runtime/work-log/activity-parsing"; import { ApprovalRequestId, - isToolLifecycleItemType, ProviderApprovalOption, ProviderRequestKind, + isToolLifecycleItemType, } from "@t3tools/contracts"; import type { OrchestrationLatestTurn, @@ -246,59 +260,6 @@ function parseApprovalRequestId(value: unknown): ApprovalRequestId | null { return typeof value === "string" && value.length > 0 ? ApprovalRequestId.make(value) : null; } -function parseUserInputQuestions( - payload: Record | null, -): ReadonlyArray | null { - const questions = payload?.questions; - if (!Array.isArray(questions)) { - return null; - } - - const parsed = questions - .map((entry) => { - if (!entry || typeof entry !== "object") return null; - const question = entry as Record; - if ( - typeof question.id !== "string" || - typeof question.header !== "string" || - typeof question.question !== "string" || - !Array.isArray(question.options) - ) { - return null; - } - const options = question.options - .map((option) => { - if (!option || typeof option !== "object") return null; - const record = option as Record; - if (typeof record.label !== "string" || typeof record.description !== "string") { - return null; - } - return { - label: record.label, - description: record.description, - ...(typeof record.value === "string" ? { value: record.value } : {}), - }; - }) - .filter((option): option is UserInputQuestion["options"][number] => option !== null); - if (options.length === 0 && question.allowCustomAnswer === false) { - return null; - } - return { - id: question.id, - header: question.header, - question: question.question, - options, - multiSelect: question.multiSelect === true, - ...(typeof question.allowCustomAnswer === "boolean" - ? { allowCustomAnswer: question.allowCustomAnswer } - : {}), - }; - }) - .filter((question): question is UserInputQuestion => question !== null); - - return parsed.length > 0 ? parsed : null; -} - function normalizeDraftAnswer(value: string | undefined): string | null { if (typeof value !== "string") { return null; @@ -431,29 +392,6 @@ function deriveWorkLogEntries( return collapseDerivedWorkLogEntries(entries); } -/** Adapters forward unknown wire-only SDK messages (background_tasks_changed, - * commands_changed, ...) as runtime warnings. The suffix comes from - * describeUnknownSdkMessage in the Claude adapter; a row with no displayable - * text carries nothing a user can act on, so it does not render. */ -function isNoContentRuntimeWarning(activity: OrchestrationThreadActivity): boolean { - return ( - activity.kind === "runtime.warning" && - activity.summary.endsWith("(no displayable text content)") - ); -} - -function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { - if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { - return false; - } - - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - return typeof payload?.detail === "string" && payload.detail.startsWith("ExitPlanMode:"); -} - function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWorkLogEntry { const payload = activity.payload && typeof activity.payload === "object" @@ -722,17 +660,6 @@ function mergeDerivedWorkLogEntries( }; } -function mergeChangedFiles( - previous: ReadonlyArray | undefined, - next: ReadonlyArray | undefined, -): string[] { - const merged = [...(previous ?? []), ...(next ?? [])]; - if (merged.length === 0) { - return []; - } - return [...new Set(merged)]; -} - function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | undefined { if ( entry.sourceActivityKind !== "tool.updated" && @@ -929,210 +856,6 @@ function singleToolCallLabel(activity: ThreadFeedActivity): string { return command || activity.summary; } -function asRecord(value: unknown): Record | null { - return value && typeof value === "object" ? (value as Record) : null; -} - -function asTrimmedString(value: unknown): string | null { - if (typeof value !== "string") { - return null; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function trimMatchingOuterQuotes(value: string): string { - const trimmed = value.trim(); - if ( - (trimmed.startsWith("'") && trimmed.endsWith("'")) || - (trimmed.startsWith('"') && trimmed.endsWith('"')) - ) { - const unquoted = trimmed.slice(1, -1).trim(); - return unquoted.length > 0 ? unquoted : trimmed; - } - return trimmed; -} - -function executableBasename(value: string): string | null { - const trimmed = trimMatchingOuterQuotes(value); - if (trimmed.length === 0) { - return null; - } - const normalized = trimmed.replace(/\\/g, "/"); - const segments = normalized.split("/"); - const last = segments.at(-1)?.trim() ?? ""; - return last.length > 0 ? last.toLowerCase() : null; -} - -function splitExecutableAndRest(value: string): { executable: string; rest: string } | null { - const trimmed = value.trim(); - if (trimmed.length === 0) { - return null; - } - - if (trimmed.startsWith('"') || trimmed.startsWith("'")) { - const quote = trimmed.charAt(0); - const closeIndex = trimmed.indexOf(quote, 1); - if (closeIndex <= 0) { - return null; - } - return { - executable: trimmed.slice(0, closeIndex + 1), - rest: trimmed.slice(closeIndex + 1).trim(), - }; - } - - const firstWhitespace = trimmed.search(/\s/); - if (firstWhitespace < 0) { - return { - executable: trimmed, - rest: "", - }; - } - - return { - executable: trimmed.slice(0, firstWhitespace), - rest: trimmed.slice(firstWhitespace).trim(), - }; -} - -const SHELL_WRAPPER_SPECS = [ - { - executables: ["pwsh", "pwsh.exe", "powershell", "powershell.exe"], - wrapperFlagPattern: /(?:^|\s)-command\s+/i, - }, - { - executables: ["cmd", "cmd.exe"], - wrapperFlagPattern: /(?:^|\s)\/c\s+/i, - }, - { - executables: ["bash", "sh", "zsh"], - wrapperFlagPattern: /(?:^|\s)-(?:l)?c\s+/i, - }, -] as const; - -function findShellWrapperSpec(shell: string) { - return SHELL_WRAPPER_SPECS.find((spec) => - (spec.executables as ReadonlyArray).includes(shell), - ); -} - -function unwrapCommandRemainder(value: string, wrapperFlagPattern: RegExp): string | null { - const match = wrapperFlagPattern.exec(value); - if (!match) { - return null; - } - - const command = value.slice(match.index + match[0].length).trim(); - if (command.length === 0) { - return null; - } - - const openingQuote = command[0]; - if ((openingQuote === "'" || openingQuote === '"') && !command.endsWith(openingQuote)) { - return null; - } - - const unwrapped = trimMatchingOuterQuotes(command); - return unwrapped.length > 0 ? unwrapped : null; -} - -function unwrapKnownShellCommandWrapper(value: string): string { - const split = splitExecutableAndRest(value); - if (!split || split.rest.length === 0) { - return value; - } - - const shell = executableBasename(split.executable); - if (!shell) { - return value; - } - - const spec = findShellWrapperSpec(shell); - if (!spec) { - return value; - } - - return unwrapCommandRemainder(split.rest, spec.wrapperFlagPattern) ?? value; -} - -function formatCommandArrayPart(value: string): string { - return /[\s"'`]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; -} - -function formatCommandValue(value: unknown): string | null { - const direct = asTrimmedString(value); - if (direct) { - return direct; - } - if (!Array.isArray(value)) { - return null; - } - const parts: Array = []; - for (const entry of value) { - const part = asTrimmedString(entry); - if (part !== null) { - parts.push(part); - } - } - if (parts.length === 0) { - return null; - } - return parts.map((part) => formatCommandArrayPart(part)).join(" "); -} - -function normalizeCommandValue(value: unknown): string | null { - const formatted = formatCommandValue(value); - return formatted ? unwrapKnownShellCommandWrapper(formatted) : null; -} - -function toRawToolCommand(value: unknown, normalizedCommand: string | null): string | null { - const formatted = formatCommandValue(value); - if (!formatted || normalizedCommand === null) { - return null; - } - return formatted === normalizedCommand ? null : formatted; -} - -function extractToolCommand(payload: Record | null): { - command: string | null; - rawCommand: string | null; -} { - const data = asRecord(payload?.data); - const item = asRecord(data?.item); - const itemResult = asRecord(item?.result); - const itemInput = asRecord(item?.input); - const itemType = asTrimmedString(payload?.itemType); - const detail = asTrimmedString(payload?.detail); - const candidates: unknown[] = [ - item?.command, - itemInput?.command, - itemResult?.command, - data?.command, - itemType === "command_execution" && detail ? stripTrailingExitCode(detail).output : null, - ]; - - for (const candidate of candidates) { - const command = normalizeCommandValue(candidate); - if (!command) { - continue; - } - return { - command, - rawCommand: toRawToolCommand(candidate, command), - }; - } - - return { - command: null, - rawCommand: null, - }; -} - -function extractToolTitle(payload: Record | null): string | null { - return asTrimmedString(payload?.title); -} - function extractWorkLogToolLifecycleStatus( payload: Record | null, ): WorkLogToolLifecycleStatus | undefined { @@ -1154,36 +877,6 @@ function extractWorkLogToolLifecycleStatus( return undefined; } -function stripTrailingExitCode(value: string): { - output: string | null; - exitCode?: number | undefined; -} { - const trimmed = value.trim(); - const match = /^(?[\s\S]*?)(?:\s*\d+)>)\s*$/i.exec( - trimmed, - ); - if (!match?.groups) { - return { - output: trimmed.length > 0 ? trimmed : null, - }; - } - const exitCode = Number.parseInt(match.groups.code ?? "", 10); - const normalizedOutput = match.groups.output?.trim() ?? ""; - return { - output: normalizedOutput.length > 0 ? normalizedOutput : null, - ...(Number.isInteger(exitCode) ? { exitCode } : {}), - }; -} - -function extractWorkLogItemType( - payload: Record | null, -): WorkLogEntry["itemType"] | undefined { - if (typeof payload?.itemType === "string" && isToolLifecycleItemType(payload.itemType)) { - return payload.itemType; - } - return undefined; -} - function extractWorkLogRequestKind( payload: Record | null, ): WorkLogEntry["requestKind"] | undefined { @@ -1197,83 +890,6 @@ function extractWorkLogRequestKind( return requestKindFromRequestType(payload?.requestType) ?? undefined; } -function pushChangedFile(target: string[], seen: Set, value: unknown) { - const normalized = asTrimmedString(value); - if (!normalized || seen.has(normalized)) { - return; - } - seen.add(normalized); - target.push(normalized); -} - -function collectChangedFiles(value: unknown, target: string[], seen: Set, depth: number) { - if (depth > 4 || target.length >= 12) { - return; - } - if (Array.isArray(value)) { - for (const entry of value) { - collectChangedFiles(entry, target, seen, depth + 1); - if (target.length >= 12) { - return; - } - } - return; - } - - const record = asRecord(value); - if (!record) { - return; - } - - pushChangedFile(target, seen, record.path); - pushChangedFile(target, seen, record.filePath); - pushChangedFile(target, seen, record.relativePath); - pushChangedFile(target, seen, record.filename); - pushChangedFile(target, seen, record.newPath); - pushChangedFile(target, seen, record.oldPath); - - for (const nestedKey of [ - "item", - "result", - "input", - "data", - "changes", - "files", - "edits", - "patch", - "patches", - "operations", - ]) { - if (!(nestedKey in record)) { - continue; - } - collectChangedFiles(record[nestedKey], target, seen, depth + 1); - if (target.length >= 12) { - return; - } - } -} - -function extractChangedFiles(payload: Record | null): string[] { - const changedFiles: string[] = []; - const seen = new Set(); - collectChangedFiles(asRecord(payload?.data), changedFiles, seen, 0); - return changedFiles; -} - -function compareActivityLifecycleRank(kind: string): number { - if (kind.endsWith(".started") || kind === "tool.started") { - return 0; - } - if (kind.endsWith(".progress") || kind.endsWith(".updated")) { - return 1; - } - if (kind.endsWith(".completed") || kind.endsWith(".resolved")) { - return 2; - } - return 1; -} - const activityOrder = Order.combineAll([ Order.mapInput(Order.Number, (activity) => activity.sequence ?? Number.MAX_SAFE_INTEGER), Order.mapInput(Order.String, (activity) => activity.createdAt), diff --git a/apps/server/src/orchestration/Errors.ts b/apps/server/src/orchestration/Errors.ts index dc29dcbfa6f8..3d634579e392 100644 --- a/apps/server/src/orchestration/Errors.ts +++ b/apps/server/src/orchestration/Errors.ts @@ -126,13 +126,6 @@ export type OrchestrationEngineError = | OrchestrationCommandJsonParseError | OrchestrationCommandDecodeError; -export function toOrchestrationCommandDecodeError(error: Schema.SchemaError) { - return new OrchestrationCommandDecodeError({ - issue: SchemaIssue.makeFormatterDefault()(error.issue), - cause: error, - }); -} - export function toProjectorDecodeError(eventType: string) { return (error: Schema.SchemaError): OrchestrationProjectorDecodeError => new OrchestrationProjectorDecodeError({ @@ -141,19 +134,3 @@ export function toProjectorDecodeError(eventType: string) { cause: error, }); } - -export function toOrchestrationJsonParseError(cause: unknown) { - return new OrchestrationCommandJsonParseError({ - detail: `Failed to parse orchestration command JSON`, - cause, - }); -} - -export function toListenerCallbackError(listener: "read-model" | "domain-event") { - return (cause: unknown): OrchestrationListenerCallbackError => - new OrchestrationListenerCallbackError({ - listener, - detail: `Failed to invoke orchestration ${listener} listener`, - cause, - }); -} diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index 6330c38ca0c3..af72327bec55 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -51,217 +51,111 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_thread_proposed_plans`; yield* sql`DELETE FROM projection_turns`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-1", + title: "Project 1", + workspace_root: "/tmp/project-1", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: '[{"id":"script-1","name":"Build","command":"bun run build","icon":"build","runOnWorktreeCreate":false}]', - '2026-02-24T00:00:00.000Z', - '2026-02-24T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - linked_pull_request_json, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - pinned_at, - pin_order_key, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, + created_at: "2026-02-24T00:00:00.000Z", + updated_at: "2026-02-24T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-1", + project_id: "project-1", + title: "Thread 1", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + linked_pull_request_json: '{"projectId":"project-1","repository":"pingdotgg/t3code","number":42,"url":"https://github.com/pingdotgg/t3code/pull/42"}', - 'turn-1', - '2026-02-24T00:00:04.000Z', - 1, - 0, - 0, - '2026-02-24T00:00:01.000Z', - 'gm', - '2026-02-24T00:00:02.000Z', - '2026-02-24T00:00:03.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_thread_messages ( - message_id, - thread_id, - turn_id, - role, - text, - is_streaming, - created_at, - updated_at - ) - VALUES ( - 'message-1', - 'thread-1', - 'turn-1', - 'assistant', - 'hello from projection', - 0, - '2026-02-24T00:00:04.000Z', - '2026-02-24T00:00:05.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_thread_proposed_plans ( - plan_id, - thread_id, - turn_id, - plan_markdown, - implemented_at, - implementation_thread_id, - created_at, - updated_at - ) - VALUES ( - 'plan-1', - 'thread-1', - 'turn-1', - '# Ship it', - '2026-02-24T00:00:05.500Z', - 'thread-2', - '2026-02-24T00:00:05.000Z', - '2026-02-24T00:00:05.500Z' - ) - `; - - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - created_at - ) - VALUES ( - 'activity-1', - 'thread-1', - 'turn-1', - 'info', - 'runtime.note', - 'provider started', - '{"stage":"start"}', - '2026-02-24T00:00:06.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_thread_sessions ( - thread_id, - status, - provider_name, - provider_session_id, - provider_thread_id, - runtime_mode, - active_turn_id, - last_error, - updated_at - ) - VALUES ( - 'thread-1', - 'running', - 'codex', - 'provider-session-1', - 'provider-thread-1', - 'approval-required', - 'turn-1', - NULL, - '2026-02-24T00:00:07.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES ( - 'thread-1', - 'turn-1', - NULL, - 'thread-1', - 'plan-1', - 'message-1', - 'completed', - '2026-02-24T00:00:08.000Z', - '2026-02-24T00:00:08.000Z', - '2026-02-24T00:00:08.000Z', - 1, - 'checkpoint-1', - 'ready', - '[{"path":"README.md","kind":"modified","additions":2,"deletions":1}]' - ) - `; + latest_turn_id: "turn-1", + latest_user_message_at: "2026-02-24T00:00:04.000Z", + pending_approval_count: 1, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + pinned_at: "2026-02-24T00:00:01.000Z", + pin_order_key: "gm", + created_at: "2026-02-24T00:00:02.000Z", + updated_at: "2026-02-24T00:00:03.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_thread_messages ${sql.insert({ + message_id: "message-1", + thread_id: "thread-1", + turn_id: "turn-1", + role: "assistant", + text: "hello from projection", + is_streaming: 0, + created_at: "2026-02-24T00:00:04.000Z", + updated_at: "2026-02-24T00:00:05.000Z", + })}`; + + yield* sql`INSERT INTO projection_thread_proposed_plans ${sql.insert({ + plan_id: "plan-1", + thread_id: "thread-1", + turn_id: "turn-1", + plan_markdown: "# Ship it", + implemented_at: "2026-02-24T00:00:05.500Z", + implementation_thread_id: "thread-2", + created_at: "2026-02-24T00:00:05.000Z", + updated_at: "2026-02-24T00:00:05.500Z", + })}`; + + yield* sql`INSERT INTO projection_thread_activities ${sql.insert({ + activity_id: "activity-1", + thread_id: "thread-1", + turn_id: "turn-1", + tone: "info", + kind: "runtime.note", + summary: "provider started", + payload_json: '{"stage":"start"}', + created_at: "2026-02-24T00:00:06.000Z", + })}`; + + yield* sql`INSERT INTO projection_thread_sessions ${sql.insert({ + thread_id: "thread-1", + status: "running", + provider_name: "codex", + provider_session_id: "provider-session-1", + provider_thread_id: "provider-thread-1", + runtime_mode: "approval-required", + active_turn_id: "turn-1", + last_error: null, + updated_at: "2026-02-24T00:00:07.000Z", + })}`; + + yield* sql`INSERT INTO projection_turns ${sql.insert({ + thread_id: "thread-1", + turn_id: "turn-1", + pending_message_id: null, + source_proposed_plan_thread_id: "thread-1", + source_proposed_plan_id: "plan-1", + assistant_message_id: "message-1", + state: "completed", + requested_at: "2026-02-24T00:00:08.000Z", + started_at: "2026-02-24T00:00:08.000Z", + completed_at: "2026-02-24T00:00:08.000Z", + checkpoint_turn_count: 1, + checkpoint_ref: "checkpoint-1", + checkpoint_status: "ready", + checkpoint_files_json: + '[{"path":"README.md","kind":"modified","additions":2,"deletions":1}]', + })}`; let sequence = 5; for (const projector of Object.values(ORCHESTRATION_PROJECTOR_NAMES)) { - yield* sql` - INSERT INTO projection_state ( - projector, - last_applied_sequence, - updated_at - ) - VALUES ( - ${projector}, - ${sequence}, - '2026-02-24T00:00:09.000Z' - ) - `; + yield* sql`INSERT INTO projection_state ${sql.insert({ + projector: projector, + last_applied_sequence: sequence, + updated_at: "2026-02-24T00:00:09.000Z", + })}`; sequence += 1; } @@ -491,39 +385,28 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { assert.deepEqual(threadDetail.value, snapshot.threads[0]); } - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - created_at - ) - VALUES - ( - 'activity-task-started', - 'thread-1', - 'turn-1', - 'info', - 'task.started', - 'Ship the query filter', - '{"taskId":"task-1","detail":"Ship the query filter"}', - '2026-02-24T00:00:06.100Z' - ), - ( - 'activity-malformed-tool', - 'thread-1', - 'turn-1', - 'info', - 'tool.completed', - 'Malformed tool output', - 'not-json', - '2026-02-24T00:00:06.200Z' - ) - `; + yield* sql`INSERT INTO projection_thread_activities ${sql.insert([ + { + activity_id: "activity-task-started", + thread_id: "thread-1", + turn_id: "turn-1", + tone: "info", + kind: "task.started", + summary: "Ship the query filter", + payload_json: '{"taskId":"task-1","detail":"Ship the query filter"}', + created_at: "2026-02-24T00:00:06.100Z", + }, + { + activity_id: "activity-malformed-tool", + thread_id: "thread-1", + turn_id: "turn-1", + tone: "info", + kind: "tool.completed", + summary: "Malformed tool output", + payload_json: "not-json", + created_at: "2026-02-24T00:00:06.200Z", + }, + ])}`; const detailWithoutActivities = yield* snapshotQuery.getThreadDetailById( ThreadId.make("thread-1"), @@ -605,101 +488,95 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_threads`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-archive-test', - 'Archive Test', - '/tmp/archive-test', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-06T00:00:00.000Z', - '2026-04-06T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES - ( - 'thread-active', - 'project-archive-test', - 'Active Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-04-06T00:00:02.000Z', - '2026-04-06T00:00:03.000Z', - NULL, - NULL - ), - ( - 'thread-archived', - 'project-archive-test', - 'Archived Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-04-06T00:00:04.000Z', - '2026-04-06T00:00:05.000Z', - '2026-04-06T00:00:06.000Z', - NULL - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-archive-test", + title: "Archive Test", + workspace_root: "/tmp/archive-test", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-06T00:00:00.000Z", + updated_at: "2026-04-06T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert([ + { + thread_id: "thread-active", + project_id: "project-archive-test", + title: "Active Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + latest_user_message_at: null, + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-04-06T00:00:02.000Z", + updated_at: "2026-04-06T00:00:03.000Z", + archived_at: null, + deleted_at: null, + }, + { + thread_id: "thread-archived", + project_id: "project-archive-test", + title: "Archived Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + latest_user_message_at: null, + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-04-06T00:00:04.000Z", + updated_at: "2026-04-06T00:00:05.000Z", + archived_at: "2026-04-06T00:00:06.000Z", + deleted_at: null, + }, + ])}`; - yield* sql` - INSERT INTO projection_state (projector, last_applied_sequence, updated_at) - VALUES - (${ORCHESTRATION_PROJECTOR_NAMES.projects}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threads}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadMessages}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadActivities}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadSessions}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.checkpoints}, 4, '2026-04-06T00:00:07.000Z') - `; + yield* sql`INSERT INTO projection_state ${sql.insert([ + { + projector: ORCHESTRATION_PROJECTOR_NAMES.projects, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threads, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadMessages, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadActivities, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadSessions, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.checkpoints, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + ])}`; const shellSnapshot = yield* snapshotQuery.getShellSnapshot(); assert.deepEqual( @@ -741,85 +618,76 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_threads`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-settled-test', - 'Settled Test', - '/tmp/settled-test', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-06T00:00:00.000Z', - '2026-04-06T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - settled_override, - settled_at, - deleted_at - ) - VALUES ( - 'thread-settled', - 'project-settled-test', - 'Settled Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-04-06T00:00:02.000Z', - '2026-04-06T00:00:05.000Z', - NULL, - 'settled', - '2026-04-06T00:00:04.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_state (projector, last_applied_sequence, updated_at) - VALUES - (${ORCHESTRATION_PROJECTOR_NAMES.projects}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threads}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadMessages}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadActivities}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadSessions}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.checkpoints}, 4, '2026-04-06T00:00:07.000Z') - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-settled-test", + title: "Settled Test", + workspace_root: "/tmp/settled-test", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-06T00:00:00.000Z", + updated_at: "2026-04-06T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-settled", + project_id: "project-settled-test", + title: "Settled Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + latest_user_message_at: null, + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-04-06T00:00:02.000Z", + updated_at: "2026-04-06T00:00:05.000Z", + archived_at: null, + settled_override: "settled", + settled_at: "2026-04-06T00:00:04.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_state ${sql.insert([ + { + projector: ORCHESTRATION_PROJECTOR_NAMES.projects, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threads, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadMessages, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadActivities, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadSessions, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.checkpoints, + last_applied_sequence: 4, + updated_at: "2026-04-06T00:00:07.000Z", + }, + ])}`; // Settled ≠ archived: the thread must appear in the LIVE shell // snapshot, carrying its settlement fields through the row aliases. @@ -852,103 +720,76 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_threads`; yield* sql`DELETE FROM projection_turns`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES - ( - 'project-active', - 'Active Project', - '/tmp/workspace', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-03-01T00:00:00.000Z', - '2026-03-01T00:00:01.000Z', - NULL - ), - ( - 'project-deleted', - 'Deleted Project', - '/tmp/deleted', - NULL, - '[]', - '2026-03-01T00:00:02.000Z', - '2026-03-01T00:00:03.000Z', - '2026-03-01T00:00:04.000Z' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert([ + { + project_id: "project-active", + title: "Active Project", + workspace_root: "/tmp/workspace", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-03-01T00:00:00.000Z", + updated_at: "2026-03-01T00:00:01.000Z", + deleted_at: null, + }, + { + project_id: "project-deleted", + title: "Deleted Project", + workspace_root: "/tmp/deleted", + default_model_selection_json: null, + scripts_json: "[]", + created_at: "2026-03-01T00:00:02.000Z", + updated_at: "2026-03-01T00:00:03.000Z", + deleted_at: "2026-03-01T00:00:04.000Z", + }, + ])}`; - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES - ( - 'thread-first', - 'project-active', - 'First Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - '2026-03-01T00:00:05.000Z', - '2026-03-01T00:00:06.000Z', - NULL, - NULL - ), - ( - 'thread-second', - 'project-active', - 'Second Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - '2026-03-01T00:00:07.000Z', - '2026-03-01T00:00:08.000Z', - NULL, - NULL - ), - ( - 'thread-deleted', - 'project-active', - 'Deleted Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - '2026-03-01T00:00:09.000Z', - '2026-03-01T00:00:10.000Z', - NULL, - '2026-03-01T00:00:11.000Z' - ) - `; + yield* sql`INSERT INTO projection_threads ${sql.insert([ + { + thread_id: "thread-first", + project_id: "project-active", + title: "First Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + created_at: "2026-03-01T00:00:05.000Z", + updated_at: "2026-03-01T00:00:06.000Z", + archived_at: null, + deleted_at: null, + }, + { + thread_id: "thread-second", + project_id: "project-active", + title: "Second Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + created_at: "2026-03-01T00:00:07.000Z", + updated_at: "2026-03-01T00:00:08.000Z", + archived_at: null, + deleted_at: null, + }, + { + thread_id: "thread-deleted", + project_id: "project-active", + title: "Deleted Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + created_at: "2026-03-01T00:00:09.000Z", + updated_at: "2026-03-01T00:00:10.000Z", + archived_at: null, + deleted_at: "2026-03-01T00:00:11.000Z", + }, + ])}`; const counts = yield* snapshotQuery.getCounts(); assert.deepEqual(counts, { @@ -1030,113 +871,67 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_threads`; yield* sql`DELETE FROM projection_turns`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-context', - 'Context Project', - '/tmp/context-workspace', - NULL, - '[]', - '2026-03-02T00:00:00.000Z', - '2026-03-02T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-context', - 'project-context', - 'Context Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - 'feature/perf', - '/tmp/context-worktree', - NULL, - '2026-03-02T00:00:02.000Z', - '2026-03-02T00:00:03.000Z', - NULL, - NULL - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES - ( - 'thread-context', - 'turn-1', - NULL, - NULL, - NULL, - NULL, - 'completed', - '2026-03-02T00:00:04.000Z', - '2026-03-02T00:00:04.000Z', - '2026-03-02T00:00:04.000Z', - 1, - 'checkpoint-a', - 'ready', - '[]' - ), - ( - 'thread-context', - 'turn-2', - NULL, - NULL, - NULL, - NULL, - 'completed', - '2026-03-02T00:00:05.000Z', - '2026-03-02T00:00:05.000Z', - '2026-03-02T00:00:05.000Z', - 2, - 'checkpoint-b', - 'ready', - '[]' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-context", + title: "Context Project", + workspace_root: "/tmp/context-workspace", + default_model_selection_json: null, + scripts_json: "[]", + created_at: "2026-03-02T00:00:00.000Z", + updated_at: "2026-03-02T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-context", + project_id: "project-context", + title: "Context Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: "feature/perf", + worktree_path: "/tmp/context-worktree", + latest_turn_id: null, + created_at: "2026-03-02T00:00:02.000Z", + updated_at: "2026-03-02T00:00:03.000Z", + archived_at: null, + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_turns ${sql.insert([ + { + thread_id: "thread-context", + turn_id: "turn-1", + pending_message_id: null, + source_proposed_plan_thread_id: null, + source_proposed_plan_id: null, + assistant_message_id: null, + state: "completed", + requested_at: "2026-03-02T00:00:04.000Z", + started_at: "2026-03-02T00:00:04.000Z", + completed_at: "2026-03-02T00:00:04.000Z", + checkpoint_turn_count: 1, + checkpoint_ref: "checkpoint-a", + checkpoint_status: "ready", + checkpoint_files_json: "[]", + }, + { + thread_id: "thread-context", + turn_id: "turn-2", + pending_message_id: null, + source_proposed_plan_thread_id: null, + source_proposed_plan_id: null, + assistant_message_id: null, + state: "completed", + requested_at: "2026-03-02T00:00:05.000Z", + started_at: "2026-03-02T00:00:05.000Z", + completed_at: "2026-03-02T00:00:05.000Z", + checkpoint_turn_count: 2, + checkpoint_ref: "checkpoint-b", + checkpoint_status: "ready", + checkpoint_files_json: "[]", + }, + ])}`; const context = yield* snapshotQuery.getThreadCheckpointContext( ThreadId.make("thread-context"), @@ -1183,115 +978,71 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_thread_activities`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-01T00:00:00.000Z', - '2026-04-01T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-04-01T00:00:02.000Z', - '2026-04-01T00:00:03.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - sequence, - created_at - ) - VALUES - ( - 'activity-unsequenced', - 'thread-1', - NULL, - 'info', - 'runtime.note', - 'unsequenced first', - '{"source":"unsequenced"}', - NULL, - '2026-04-01T00:00:06.000Z' - ), - ( - 'activity-sequence-2', - 'thread-1', - NULL, - 'info', - 'runtime.note', - 'sequence two', - '{"source":"sequence-2"}', - 2, - '2026-04-01T00:00:04.000Z' - ), - ( - 'activity-sequence-1', - 'thread-1', - NULL, - 'info', - 'runtime.note', - 'sequence one', - '{"source":"sequence-1"}', - 1, - '2026-04-01T00:00:05.000Z' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-1", + title: "Project 1", + workspace_root: "/tmp/project-1", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-01T00:00:00.000Z", + updated_at: "2026-04-01T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-1", + project_id: "project-1", + title: "Thread 1", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + latest_user_message_at: null, + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-04-01T00:00:02.000Z", + updated_at: "2026-04-01T00:00:03.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_thread_activities ${sql.insert([ + { + activity_id: "activity-unsequenced", + thread_id: "thread-1", + turn_id: null, + tone: "info", + kind: "runtime.note", + summary: "unsequenced first", + payload_json: '{"source":"unsequenced"}', + sequence: null, + created_at: "2026-04-01T00:00:06.000Z", + }, + { + activity_id: "activity-sequence-2", + thread_id: "thread-1", + turn_id: null, + tone: "info", + kind: "runtime.note", + summary: "sequence two", + payload_json: '{"source":"sequence-2"}', + sequence: 2, + created_at: "2026-04-01T00:00:04.000Z", + }, + { + activity_id: "activity-sequence-1", + thread_id: "thread-1", + turn_id: null, + tone: "info", + kind: "runtime.note", + summary: "sequence one", + payload_json: '{"source":"sequence-1"}', + sequence: 1, + created_at: "2026-04-01T00:00:05.000Z", + }, + ])}`; const snapshot = yield* snapshotQuery.getSnapshot(); const threadDetail = yield* snapshotQuery.getThreadDetailById(ThreadId.make("thread-1")); @@ -1344,121 +1095,71 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_threads`; yield* sql`DELETE FROM projection_turns`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-02T00:00:00.000Z', - '2026-04-02T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - 'turn-running', - '2026-04-02T00:00:04.000Z', - 0, - 0, - 0, - '2026-04-02T00:00:02.000Z', - '2026-04-02T00:00:03.000Z', - NULL, - NULL - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES - ( - 'thread-1', - 'turn-completed', - 'message-user-1', - NULL, - NULL, - 'message-assistant-1', - 'completed', - '2026-04-02T00:00:05.000Z', - '2026-04-02T00:00:06.000Z', - '2026-04-02T00:00:20.000Z', - 5, - 'checkpoint-5', - 'ready', - '[]' - ), - ( - 'thread-1', - 'turn-running', - 'message-user-2', - NULL, - NULL, - NULL, - 'running', - '2026-04-02T00:00:30.000Z', - '2026-04-02T00:00:30.000Z', - NULL, - NULL, - NULL, - NULL, - '[]' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-1", + title: "Project 1", + workspace_root: "/tmp/project-1", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-02T00:00:00.000Z", + updated_at: "2026-04-02T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-1", + project_id: "project-1", + title: "Thread 1", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: "turn-running", + latest_user_message_at: "2026-04-02T00:00:04.000Z", + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-04-02T00:00:02.000Z", + updated_at: "2026-04-02T00:00:03.000Z", + archived_at: null, + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_turns ${sql.insert([ + { + thread_id: "thread-1", + turn_id: "turn-completed", + pending_message_id: "message-user-1", + source_proposed_plan_thread_id: null, + source_proposed_plan_id: null, + assistant_message_id: "message-assistant-1", + state: "completed", + requested_at: "2026-04-02T00:00:05.000Z", + started_at: "2026-04-02T00:00:06.000Z", + completed_at: "2026-04-02T00:00:20.000Z", + checkpoint_turn_count: 5, + checkpoint_ref: "checkpoint-5", + checkpoint_status: "ready", + checkpoint_files_json: "[]", + }, + { + thread_id: "thread-1", + turn_id: "turn-running", + pending_message_id: "message-user-2", + source_proposed_plan_thread_id: null, + source_proposed_plan_id: null, + assistant_message_id: null, + state: "running", + requested_at: "2026-04-02T00:00:30.000Z", + started_at: "2026-04-02T00:00:30.000Z", + completed_at: null, + checkpoint_turn_count: null, + checkpoint_ref: null, + checkpoint_status: null, + checkpoint_files_json: "[]", + }, + ])}`; const threadShell = yield* snapshotQuery.getThreadShellById(ThreadId.make("thread-1")); assert.equal(threadShell._tag, "Some"); @@ -1488,133 +1189,109 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_turns`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-03T00:00:00.000Z', - '2026-04-03T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - 'turn-running', - '2026-04-03T00:00:04.000Z', - 0, - 0, - 0, - '2026-04-03T00:00:02.000Z', - '2026-04-03T00:00:03.000Z', - NULL, - NULL - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES - ( - 'thread-1', - 'turn-running', - 'message-user-2', - NULL, - NULL, - NULL, - 'running', - '2026-04-03T00:00:30.000Z', - '2026-04-03T00:00:30.000Z', - NULL, - NULL, - NULL, - NULL, - '[]' - ), - ( - 'thread-1', - 'turn-completed', - 'message-user-1', - NULL, - NULL, - 'message-assistant-1', - 'completed', - '2026-04-03T00:00:05.000Z', - '2026-04-03T00:00:06.000Z', - '2026-04-03T00:00:20.000Z', - NULL, - NULL, - NULL, - '[]' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-1", + title: "Project 1", + workspace_root: "/tmp/project-1", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-03T00:00:00.000Z", + updated_at: "2026-04-03T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-1", + project_id: "project-1", + title: "Thread 1", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: "turn-running", + latest_user_message_at: "2026-04-03T00:00:04.000Z", + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-04-03T00:00:02.000Z", + updated_at: "2026-04-03T00:00:03.000Z", + archived_at: null, + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_turns ${sql.insert([ + { + thread_id: "thread-1", + turn_id: "turn-running", + pending_message_id: "message-user-2", + source_proposed_plan_thread_id: null, + source_proposed_plan_id: null, + assistant_message_id: null, + state: "running", + requested_at: "2026-04-03T00:00:30.000Z", + started_at: "2026-04-03T00:00:30.000Z", + completed_at: null, + checkpoint_turn_count: null, + checkpoint_ref: null, + checkpoint_status: null, + checkpoint_files_json: "[]", + }, + { + thread_id: "thread-1", + turn_id: "turn-completed", + pending_message_id: "message-user-1", + source_proposed_plan_thread_id: null, + source_proposed_plan_id: null, + assistant_message_id: "message-assistant-1", + state: "completed", + requested_at: "2026-04-03T00:00:05.000Z", + started_at: "2026-04-03T00:00:06.000Z", + completed_at: "2026-04-03T00:00:20.000Z", + checkpoint_turn_count: null, + checkpoint_ref: null, + checkpoint_status: null, + checkpoint_files_json: "[]", + }, + ])}`; - yield* sql` - INSERT INTO projection_state (projector, last_applied_sequence, updated_at) - VALUES - (${ORCHESTRATION_PROJECTOR_NAMES.projects}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threads}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadMessages}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadActivities}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadSessions}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.checkpoints}, 3, '2026-04-03T00:00:40.000Z') - `; + yield* sql`INSERT INTO projection_state ${sql.insert([ + { + projector: ORCHESTRATION_PROJECTOR_NAMES.projects, + last_applied_sequence: 3, + updated_at: "2026-04-03T00:00:40.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threads, + last_applied_sequence: 3, + updated_at: "2026-04-03T00:00:40.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadMessages, + last_applied_sequence: 3, + updated_at: "2026-04-03T00:00:40.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans, + last_applied_sequence: 3, + updated_at: "2026-04-03T00:00:40.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadActivities, + last_applied_sequence: 3, + updated_at: "2026-04-03T00:00:40.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.threadSessions, + last_applied_sequence: 3, + updated_at: "2026-04-03T00:00:40.000Z", + }, + { + projector: ORCHESTRATION_PROJECTOR_NAMES.checkpoints, + last_applied_sequence: 3, + updated_at: "2026-04-03T00:00:40.000Z", + }, + ])}`; const commandReadModel = yield* snapshotQuery.getCommandReadModel(); assert.equal(commandReadModel.threads[0]?.latestTurn?.turnId, asTurnId("turn-running")); @@ -1640,104 +1317,53 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_turns`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-deleted', - 'Deleted Project', - '/tmp/deleted-project', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-05T00:00:00.000Z', - '2026-04-05T00:00:01.000Z', - '2026-04-05T00:00:02.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-deleted', - 'project-deleted', - 'Deleted Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - 'turn-deleted', - NULL, - 0, - 0, - 0, - '2026-04-05T00:00:03.000Z', - '2026-04-05T00:00:04.000Z', - NULL, - '2026-04-05T00:00:05.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES ( - 'thread-deleted', - 'turn-deleted', - 'message-deleted-user', - NULL, - NULL, - 'message-deleted-assistant', - 'completed', - '2026-04-05T00:00:04.100Z', - '2026-04-05T00:00:04.200Z', - '2026-04-05T00:00:04.300Z', - NULL, - NULL, - NULL, - '[]' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-deleted", + title: "Deleted Project", + workspace_root: "/tmp/deleted-project", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-05T00:00:00.000Z", + updated_at: "2026-04-05T00:00:01.000Z", + deleted_at: "2026-04-05T00:00:02.000Z", + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-deleted", + project_id: "project-deleted", + title: "Deleted Thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: "turn-deleted", + latest_user_message_at: null, + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-04-05T00:00:03.000Z", + updated_at: "2026-04-05T00:00:04.000Z", + archived_at: null, + deleted_at: "2026-04-05T00:00:05.000Z", + })}`; + + yield* sql`INSERT INTO projection_turns ${sql.insert({ + thread_id: "thread-deleted", + turn_id: "turn-deleted", + pending_message_id: "message-deleted-user", + source_proposed_plan_thread_id: null, + source_proposed_plan_id: null, + assistant_message_id: "message-deleted-assistant", + state: "completed", + requested_at: "2026-04-05T00:00:04.100Z", + started_at: "2026-04-05T00:00:04.200Z", + completed_at: "2026-04-05T00:00:04.300Z", + checkpoint_turn_count: null, + checkpoint_ref: null, + checkpoint_status: null, + checkpoint_files_json: "[]", + })}`; const commandReadModel = yield* snapshotQuery.getCommandReadModel(); assert.equal(commandReadModel.projects[0]?.id, asProjectId("project-deleted")); @@ -1768,217 +1394,161 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_threads`; yield* sql`DELETE FROM projection_projects`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-search', - 'Project Needle', - '/tmp/project-search', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-05-01T00:00:00.000Z', - '2026-05-01T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES - ( - 'thread-active', - 'project-search', - 'Literal 100% fix', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - 'search-branch', - NULL, - 'turn-active', - '2026-05-01T00:00:02.000Z', - 0, - 0, - 0, - '2026-05-01T00:00:02.000Z', - '2026-05-01T00:00:03.000Z', - NULL, - NULL - ), - ( - 'thread-percent-decoy', - 'project-search', - 'Literal 100x fix', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-05-01T00:00:04.000Z', - '2026-05-01T00:00:05.000Z', - NULL, - NULL - ), - ( - 'thread-hidden', - 'project-search', - 'Archived search', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-05-01T00:00:06.000Z', - '2026-05-01T00:00:07.000Z', - '2026-05-01T00:00:08.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_thread_messages ( - message_id, - thread_id, - turn_id, - role, - text, - is_streaming, - created_at, - updated_at - ) - VALUES - ( - 'message-user', - 'thread-active', - 'turn-active', - 'user', - 'Please find this USER needle in an old prompt.', - 0, - '2026-05-01T00:00:12.000Z', - '2026-05-01T00:00:12.000Z' - ), - ( - 'message-percent', - 'thread-active', - NULL, - 'user', - 'Literal 100% fix in a prompt.', - 0, - '2026-05-01T00:00:11.000Z', - '2026-05-01T00:00:11.000Z' - ), - ( - 'message-percent-decoy', - 'thread-percent-decoy', - NULL, - 'user', - 'Literal 100x fix in a prompt.', - 0, - '2026-05-01T00:00:11.000Z', - '2026-05-01T00:00:11.000Z' - ), - ( - 'message-final', - 'thread-active', - 'turn-active', - 'assistant', - 'The canonical final needle appears in this completed answer.', - 0, - '2026-05-01T00:00:13.000Z', - '2026-05-01T00:00:13.000Z' - ), - ( - 'message-interim', - 'thread-active', - 'turn-active', - 'assistant', - 'Interim needle must not be searchable.', - 0, - '2026-05-01T00:00:14.000Z', - '2026-05-01T00:00:14.000Z' - ), - ( - 'message-system', - 'thread-active', - NULL, - 'system', - 'System needle must not be searchable.', - 0, - '2026-05-01T00:00:15.000Z', - '2026-05-01T00:00:15.000Z' - ), - ( - 'message-hidden', - 'thread-hidden', - NULL, - 'user', - 'Hidden needle in archive.', - 0, - '2026-05-01T00:00:16.000Z', - '2026-05-01T00:00:16.000Z' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-search", + title: "Project Needle", + workspace_root: "/tmp/project-search", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-05-01T00:00:00.000Z", + updated_at: "2026-05-01T00:00:01.000Z", + deleted_at: null, + })}`; + + yield* sql`INSERT INTO projection_threads ${sql.insert([ + { + thread_id: "thread-active", + project_id: "project-search", + title: "Literal 100% fix", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: "search-branch", + worktree_path: null, + latest_turn_id: "turn-active", + latest_user_message_at: "2026-05-01T00:00:02.000Z", + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-05-01T00:00:02.000Z", + updated_at: "2026-05-01T00:00:03.000Z", + archived_at: null, + deleted_at: null, + }, + { + thread_id: "thread-percent-decoy", + project_id: "project-search", + title: "Literal 100x fix", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + latest_user_message_at: null, + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-05-01T00:00:04.000Z", + updated_at: "2026-05-01T00:00:05.000Z", + archived_at: null, + deleted_at: null, + }, + { + thread_id: "thread-hidden", + project_id: "project-search", + title: "Archived search", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + branch: null, + worktree_path: null, + latest_turn_id: null, + latest_user_message_at: null, + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-05-01T00:00:06.000Z", + updated_at: "2026-05-01T00:00:07.000Z", + archived_at: "2026-05-01T00:00:08.000Z", + deleted_at: null, + }, + ])}`; - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_files_json - ) - VALUES ( - 'thread-active', - 'turn-active', - 'message-user', - 'message-final', - 'completed', - '2026-05-01T00:00:12.000Z', - '2026-05-01T00:00:12.000Z', - '2026-05-01T00:00:13.000Z', - '[]' - ) - `; + yield* sql`INSERT INTO projection_thread_messages ${sql.insert([ + { + message_id: "message-user", + thread_id: "thread-active", + turn_id: "turn-active", + role: "user", + text: "Please find this USER needle in an old prompt.", + is_streaming: 0, + created_at: "2026-05-01T00:00:12.000Z", + updated_at: "2026-05-01T00:00:12.000Z", + }, + { + message_id: "message-percent", + thread_id: "thread-active", + turn_id: null, + role: "user", + text: "Literal 100% fix in a prompt.", + is_streaming: 0, + created_at: "2026-05-01T00:00:11.000Z", + updated_at: "2026-05-01T00:00:11.000Z", + }, + { + message_id: "message-percent-decoy", + thread_id: "thread-percent-decoy", + turn_id: null, + role: "user", + text: "Literal 100x fix in a prompt.", + is_streaming: 0, + created_at: "2026-05-01T00:00:11.000Z", + updated_at: "2026-05-01T00:00:11.000Z", + }, + { + message_id: "message-final", + thread_id: "thread-active", + turn_id: "turn-active", + role: "assistant", + text: "The canonical final needle appears in this completed answer.", + is_streaming: 0, + created_at: "2026-05-01T00:00:13.000Z", + updated_at: "2026-05-01T00:00:13.000Z", + }, + { + message_id: "message-interim", + thread_id: "thread-active", + turn_id: "turn-active", + role: "assistant", + text: "Interim needle must not be searchable.", + is_streaming: 0, + created_at: "2026-05-01T00:00:14.000Z", + updated_at: "2026-05-01T00:00:14.000Z", + }, + { + message_id: "message-system", + thread_id: "thread-active", + turn_id: null, + role: "system", + text: "System needle must not be searchable.", + is_streaming: 0, + created_at: "2026-05-01T00:00:15.000Z", + updated_at: "2026-05-01T00:00:15.000Z", + }, + { + message_id: "message-hidden", + thread_id: "thread-hidden", + turn_id: null, + role: "user", + text: "Hidden needle in archive.", + is_streaming: 0, + created_at: "2026-05-01T00:00:16.000Z", + updated_at: "2026-05-01T00:00:16.000Z", + }, + ])}`; + + yield* sql`INSERT INTO projection_turns ${sql.insert({ + thread_id: "thread-active", + turn_id: "turn-active", + pending_message_id: "message-user", + assistant_message_id: "message-final", + state: "completed", + requested_at: "2026-05-01T00:00:12.000Z", + started_at: "2026-05-01T00:00:12.000Z", + completed_at: "2026-05-01T00:00:13.000Z", + checkpoint_files_json: "[]", + })}`; const literalPercent = yield* snapshotQuery.searchThreads({ query: "100%" }); assert.deepStrictEqual( @@ -2060,49 +1630,38 @@ it.effect( yield* sql`DELETE FROM projection_turns`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES - ( - 'project-1', - 'Shared Project 1', - '/tmp/shared-root', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-04T00:00:00.000Z', - '2026-04-04T00:00:01.000Z', - NULL - ), - ( - 'project-2', - 'Shared Project 2', - '/tmp/shared-root', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-04T00:00:02.000Z', - '2026-04-04T00:00:03.000Z', - NULL - ), - ( - 'project-3', - 'Deleted Project', - '/tmp/deleted-root', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-04T00:00:04.000Z', - '2026-04-04T00:00:05.000Z', - '2026-04-04T00:00:06.000Z' - ) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert([ + { + project_id: "project-1", + title: "Shared Project 1", + workspace_root: "/tmp/shared-root", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-04T00:00:00.000Z", + updated_at: "2026-04-04T00:00:01.000Z", + deleted_at: null, + }, + { + project_id: "project-2", + title: "Shared Project 2", + workspace_root: "/tmp/shared-root", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-04T00:00:02.000Z", + updated_at: "2026-04-04T00:00:03.000Z", + deleted_at: null, + }, + { + project_id: "project-3", + title: "Deleted Project", + workspace_root: "/tmp/deleted-root", + default_model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + scripts_json: "[]", + created_at: "2026-04-04T00:00:04.000Z", + updated_at: "2026-04-04T00:00:05.000Z", + deleted_at: "2026-04-04T00:00:06.000Z", + }, + ])}`; const shellSnapshot = yield* snapshotQuery.getShellSnapshot(); assert.deepStrictEqual(resolveCalls.toSorted(), ["/tmp/shared-root"]); @@ -2145,23 +1704,30 @@ projectionSnapshotLayer("ProjectionSnapshotQuery windowed thread detail", (it) = yield* sql`DELETE FROM projection_thread_activities`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, title, workspace_root, scripts_json, created_at, updated_at, deleted_at - ) - VALUES ('project-w', 'Windowed', '/tmp/project-w', '[]', - '2026-03-01T00:00:00.000Z', '2026-03-01T00:00:00.000Z', NULL) - `; - yield* sql` - INSERT INTO projection_threads ( - thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, - latest_turn_id, pending_approval_count, pending_user_input_count, - has_actionable_proposed_plan, created_at, updated_at, deleted_at - ) - VALUES ('thread-w', 'project-w', 'Windowed thread', - '{"provider":"codex","model":"gpt-5-codex"}', 'full-access', 'default', - 'turn-5', 0, 0, 0, '2026-03-01T00:00:00.000Z', '2026-03-01T00:00:10.000Z', NULL) - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-w", + title: "Windowed", + workspace_root: "/tmp/project-w", + scripts_json: "[]", + created_at: "2026-03-01T00:00:00.000Z", + updated_at: "2026-03-01T00:00:00.000Z", + deleted_at: null, + })}`; + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-w", + project_id: "project-w", + title: "Windowed thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + latest_turn_id: "turn-5", + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-03-01T00:00:00.000Z", + updated_at: "2026-03-01T00:00:10.000Z", + deleted_at: null, + })}`; const turns: ReadonlyArray<{ turn: string; @@ -2175,59 +1741,80 @@ projectionSnapshotLayer("ProjectionSnapshotQuery windowed thread detail", (it) = { turn: "turn-5", pendingMessage: "user-msg-5", at: "2026-03-01T00:04:00.000Z" }, ]; for (const { turn, pendingMessage, at } of turns) { - yield* sql` - INSERT INTO projection_turns ( - thread_id, turn_id, pending_message_id, state, requested_at, started_at, completed_at, - checkpoint_files_json - ) - VALUES ('thread-w', ${turn}, ${pendingMessage}, 'completed', ${at}, ${at}, ${at}, '[]') - `; + yield* sql`INSERT INTO projection_turns ${sql.insert({ + thread_id: "thread-w", + turn_id: turn, + pending_message_id: pendingMessage, + state: "completed", + requested_at: at, + started_at: at, + completed_at: at, + checkpoint_files_json: "[]", + })}`; if (pendingMessage !== null) { - yield* sql` - INSERT INTO projection_thread_messages ( - message_id, thread_id, turn_id, role, text, is_streaming, created_at, updated_at - ) - VALUES (${pendingMessage}, 'thread-w', NULL, 'user', ${"prompt for " + turn}, 0, ${at}, ${at}) - `; + yield* sql`INSERT INTO projection_thread_messages ${sql.insert({ + message_id: pendingMessage, + thread_id: "thread-w", + turn_id: null, + role: "user", + text: "prompt for " + turn, + is_streaming: 0, + created_at: at, + updated_at: at, + })}`; } - yield* sql` - INSERT INTO projection_thread_messages ( - message_id, thread_id, turn_id, role, text, is_streaming, created_at, updated_at - ) - VALUES (${turn + "-reply"}, 'thread-w', ${turn}, 'assistant', ${"reply from " + turn}, 0, ${at}, ${at}) - `; - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, thread_id, turn_id, tone, kind, summary, payload_json, created_at - ) - VALUES (${turn + "-activity"}, 'thread-w', ${turn}, 'tool', 'tool.completed', - 'ran tool', '{"ok":true}', ${at}) - `; + yield* sql`INSERT INTO projection_thread_messages ${sql.insert({ + message_id: turn + "-reply", + thread_id: "thread-w", + turn_id: turn, + role: "assistant", + text: "reply from " + turn, + is_streaming: 0, + created_at: at, + updated_at: at, + })}`; + yield* sql`INSERT INTO projection_thread_activities ${sql.insert({ + activity_id: turn + "-activity", + thread_id: "thread-w", + turn_id: turn, + tone: "tool", + kind: "tool.completed", + summary: "ran tool", + payload_json: '{"ok":true}', + created_at: at, + })}`; } // Straggler user message sent while turn-4 ran: turn_id NULL and not any // turn's pending_message_id. - yield* sql` - INSERT INTO projection_thread_messages ( - message_id, thread_id, turn_id, role, text, is_streaming, created_at, updated_at - ) - VALUES ('user-msg-straggler', 'thread-w', NULL, 'user', 'while you are at it', - 0, '2026-03-01T00:03:30.000Z', '2026-03-01T00:03:30.000Z') - `; + yield* sql`INSERT INTO projection_thread_messages ${sql.insert({ + message_id: "user-msg-straggler", + thread_id: "thread-w", + turn_id: null, + role: "user", + text: "while you are at it", + is_streaming: 0, + created_at: "2026-03-01T00:03:30.000Z", + updated_at: "2026-03-01T00:03:30.000Z", + })}`; // Turnless activity in the same time range. - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, thread_id, turn_id, tone, kind, summary, payload_json, created_at - ) - VALUES ('turnless-activity', 'thread-w', NULL, 'info', 'context-window.updated', - 'usage', '{"usedTokens":1}', '2026-03-01T00:03:36.000Z') - `; + yield* sql`INSERT INTO projection_thread_activities ${sql.insert({ + activity_id: "turnless-activity", + thread_id: "thread-w", + turn_id: null, + tone: "info", + kind: "context-window.updated", + summary: "usage", + payload_json: '{"usedTokens":1}', + created_at: "2026-03-01T00:03:36.000Z", + })}`; for (const projector of Object.values(ORCHESTRATION_PROJECTOR_NAMES)) { - yield* sql` - INSERT INTO projection_state (projector, last_applied_sequence, updated_at) - VALUES (${projector}, 42, '2026-03-01T00:00:10.000Z') - `; + yield* sql`INSERT INTO projection_state ${sql.insert({ + projector: projector, + last_applied_sequence: 42, + updated_at: "2026-03-01T00:00:10.000Z", + })}`; } }); @@ -2328,16 +1915,16 @@ projectionSnapshotLayer("ProjectionSnapshotQuery windowed thread detail", (it) = `; yield* sql`DELETE FROM projection_turns WHERE thread_id = 'thread-w'`; for (const row of turnRows) { - yield* sql` - INSERT INTO projection_turns ( - thread_id, turn_id, pending_message_id, state, requested_at, started_at, - completed_at, checkpoint_files_json - ) - VALUES (${row.thread_id as string}, ${row.turn_id as string}, - ${row.pending_message_id as string | null}, ${row.state as string}, - ${row.requested_at as string}, ${row.started_at as string}, - ${row.completed_at as string}, ${row.checkpoint_files_json as string}) - `; + yield* sql`INSERT INTO projection_turns ${sql.insert({ + thread_id: row.thread_id as string, + turn_id: row.turn_id as string, + pending_message_id: row.pending_message_id as string | null, + state: row.state as string, + requested_at: row.requested_at as string, + started_at: row.started_at as string, + completed_at: row.completed_at as string, + checkpoint_files_json: row.checkpoint_files_json as string, + })}`; } const olderPage = yield* snapshotQuery.getThreadDetailSnapshot(threadW, { @@ -2590,51 +2177,83 @@ projectionSnapshotLayer("ProjectionSnapshotQuery windowed thread detail", (it) = assert.equal(windowedDetail.value.thread.activities.at(-1)?.id, asEventId("activity-0501")); } - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, thread_id, turn_id, tone, kind, summary, payload_json, sequence, created_at - ) - VALUES - ( - 'approval-old', 'thread-w', NULL, 'approval', 'approval.requested', - 'Approve old command', '{"requestId":"approval-1"}', NULL, - '2026-03-01T00:00:01.000Z' - ), - ( - 'user-input-old', 'thread-w', NULL, 'approval', 'user-input.requested', - 'Answer old question', '{"requestId":"input-1"}', NULL, - '2026-03-01T00:00:02.000Z' - ), - ( - 'user-input-closed', 'thread-w', NULL, 'approval', 'user-input.requested', - 'Closed question', '{"requestId":"input-closed"}', NULL, - '2026-03-01T00:00:03.000Z' - ), - ( - 'user-input-closed-resolution', 'thread-w', NULL, 'info', 'user-input.resolved', - 'Closed question', '{"requestId":"input-closed"}', NULL, - '2026-03-01T00:00:04.000Z' - ), - ( - 'user-input-tied-z-request', 'thread-w', NULL, 'approval', 'user-input.requested', - 'Tied open question', '{"requestId":"input-tied-open"}', NULL, - '2026-03-01T00:00:05.000Z' - ), - ( - 'user-input-tied-a-resolution', 'thread-w', NULL, 'info', 'user-input.resolved', - 'Tied open question', '{"requestId":"input-tied-open"}', NULL, - '2026-03-01T00:00:05.000Z' - ) - `; - yield* sql` - INSERT INTO projection_pending_approvals ( - request_id, thread_id, turn_id, status, decision, created_at, resolved_at - ) - VALUES ( - 'approval-1', 'thread-w', NULL, 'pending', NULL, - '2026-03-01T00:00:01.000Z', NULL - ) - `; + yield* sql`INSERT INTO projection_thread_activities ${sql.insert([ + { + activity_id: "approval-old", + thread_id: "thread-w", + turn_id: null, + tone: "approval", + kind: "approval.requested", + summary: "Approve old command", + payload_json: '{"requestId":"approval-1"}', + sequence: null, + created_at: "2026-03-01T00:00:01.000Z", + }, + { + activity_id: "user-input-old", + thread_id: "thread-w", + turn_id: null, + tone: "approval", + kind: "user-input.requested", + summary: "Answer old question", + payload_json: '{"requestId":"input-1"}', + sequence: null, + created_at: "2026-03-01T00:00:02.000Z", + }, + { + activity_id: "user-input-closed", + thread_id: "thread-w", + turn_id: null, + tone: "approval", + kind: "user-input.requested", + summary: "Closed question", + payload_json: '{"requestId":"input-closed"}', + sequence: null, + created_at: "2026-03-01T00:00:03.000Z", + }, + { + activity_id: "user-input-closed-resolution", + thread_id: "thread-w", + turn_id: null, + tone: "info", + kind: "user-input.resolved", + summary: "Closed question", + payload_json: '{"requestId":"input-closed"}', + sequence: null, + created_at: "2026-03-01T00:00:04.000Z", + }, + { + activity_id: "user-input-tied-z-request", + thread_id: "thread-w", + turn_id: null, + tone: "approval", + kind: "user-input.requested", + summary: "Tied open question", + payload_json: '{"requestId":"input-tied-open"}', + sequence: null, + created_at: "2026-03-01T00:00:05.000Z", + }, + { + activity_id: "user-input-tied-a-resolution", + thread_id: "thread-w", + turn_id: null, + tone: "info", + kind: "user-input.resolved", + summary: "Tied open question", + payload_json: '{"requestId":"input-tied-open"}', + sequence: null, + created_at: "2026-03-01T00:00:05.000Z", + }, + ])}`; + yield* sql`INSERT INTO projection_pending_approvals ${sql.insert({ + request_id: "approval-1", + thread_id: "thread-w", + turn_id: null, + status: "pending", + decision: null, + created_at: "2026-03-01T00:00:01.000Z", + resolved_at: null, + })}`; yield* sql` UPDATE projection_threads SET pending_approval_count = 1, pending_user_input_count = 1 @@ -2736,35 +2355,45 @@ projectionSnapshotLayer("ProjectionSnapshotQuery windowed thread detail", (it) = yield* sql`DELETE FROM projection_thread_activities`; yield* sql`DELETE FROM projection_state`; - yield* sql` - INSERT INTO projection_projects ( - project_id, title, workspace_root, scripts_json, created_at, updated_at, deleted_at - ) - VALUES ('project-e', 'Empty', '/tmp/project-e', '[]', - '2026-03-02T00:00:00.000Z', '2026-03-02T00:00:00.000Z', NULL) - `; - yield* sql` - INSERT INTO projection_threads ( - thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, - pending_approval_count, pending_user_input_count, has_actionable_proposed_plan, - created_at, updated_at, deleted_at - ) - VALUES ('thread-e', 'project-e', 'Turnless thread', - '{"provider":"codex","model":"gpt-5-codex"}', 'full-access', 'default', - 0, 0, 0, '2026-03-02T00:00:00.000Z', '2026-03-02T00:00:00.000Z', NULL) - `; - yield* sql` - INSERT INTO projection_thread_messages ( - message_id, thread_id, turn_id, role, text, is_streaming, created_at, updated_at - ) - VALUES ('pre-turn-msg', 'thread-e', NULL, 'user', 'first prompt', 0, - '2026-03-02T00:00:01.000Z', '2026-03-02T00:00:01.000Z') - `; + yield* sql`INSERT INTO projection_projects ${sql.insert({ + project_id: "project-e", + title: "Empty", + workspace_root: "/tmp/project-e", + scripts_json: "[]", + created_at: "2026-03-02T00:00:00.000Z", + updated_at: "2026-03-02T00:00:00.000Z", + deleted_at: null, + })}`; + yield* sql`INSERT INTO projection_threads ${sql.insert({ + thread_id: "thread-e", + project_id: "project-e", + title: "Turnless thread", + model_selection_json: '{"provider":"codex","model":"gpt-5-codex"}', + runtime_mode: "full-access", + interaction_mode: "default", + pending_approval_count: 0, + pending_user_input_count: 0, + has_actionable_proposed_plan: 0, + created_at: "2026-03-02T00:00:00.000Z", + updated_at: "2026-03-02T00:00:00.000Z", + deleted_at: null, + })}`; + yield* sql`INSERT INTO projection_thread_messages ${sql.insert({ + message_id: "pre-turn-msg", + thread_id: "thread-e", + turn_id: null, + role: "user", + text: "first prompt", + is_streaming: 0, + created_at: "2026-03-02T00:00:01.000Z", + updated_at: "2026-03-02T00:00:01.000Z", + })}`; for (const projector of Object.values(ORCHESTRATION_PROJECTOR_NAMES)) { - yield* sql` - INSERT INTO projection_state (projector, last_applied_sequence, updated_at) - VALUES (${projector}, 7, '2026-03-02T00:00:01.000Z') - `; + yield* sql`INSERT INTO projection_state ${sql.insert({ + projector: projector, + last_applied_sequence: 7, + updated_at: "2026-03-02T00:00:01.000Z", + })}`; } const snapshot = yield* snapshotQuery.getThreadDetailSnapshot(ThreadId.make("thread-e"), { diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index 91d09bf57193..a80a76867447 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -405,6 +405,104 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { const threadBackgroundLiveness = yield* ThreadBackgroundLivenessService; const threadPlanProgress = yield* ThreadPlanProgressService; const sql = yield* SqlClient.SqlClient; + + const projectColumns = sql` + project_id AS "projectId", + title, + workspace_root AS "workspaceRoot", + default_model_selection_json AS "defaultModelSelection", + default_thread_env_mode AS "defaultThreadEnvMode", + auto_pull AS "autoPull", + favicon_path AS "faviconPath", + project_icon_json AS "projectIcon", + scripts_json AS "scripts", + created_at AS "createdAt", + updated_at AS "updatedAt", + deleted_at AS "deletedAt" + `; + const threadColumns = sql` + thread_id AS "threadId", + project_id AS "projectId", + title, + model_selection_json AS "modelSelection", + runtime_mode AS "runtimeMode", + interaction_mode AS "interactionMode", + branch, + worktree_path AS "worktreePath", + linked_pull_request_json AS "linkedPullRequest", + latest_turn_id AS "latestTurnId", + created_at AS "createdAt", + updated_at AS "updatedAt", + archived_at AS "archivedAt", + settled_override AS "settledOverride", + settled_at AS "settledAt", + unsettled_at AS "unsettledAt", + snoozed_until AS "snoozedUntil", + snoozed_at AS "snoozedAt", + pinned_at AS "pinnedAt", + pin_order_key AS "pinOrderKey", + title_regeneration_request_id AS "titleRegenerationRequestId", + title_regeneration_started_at AS "titleRegenerationStartedAt", + latest_user_message_at AS "latestUserMessageAt", + pending_approval_count AS "pendingApprovalCount", + pending_user_input_count AS "pendingUserInputCount", + has_actionable_proposed_plan AS "hasActionableProposedPlan", + deleted_at AS "deletedAt" + `; + const messageColumns = sql` + message_id AS "messageId", + thread_id AS "threadId", + turn_id AS "turnId", + role, + text, + attachments_json AS "attachments", + is_streaming AS "isStreaming", + created_at AS "createdAt", + updated_at AS "updatedAt" + `; + const proposedPlanColumns = sql` + plan_id AS "planId", + thread_id AS "threadId", + turn_id AS "turnId", + plan_markdown AS "planMarkdown", + implemented_at AS "implementedAt", + implementation_thread_id AS "implementationThreadId", + created_at AS "createdAt", + updated_at AS "updatedAt" + `; + const activityColumns = sql` + activity_id AS "activityId", + thread_id AS "threadId", + turn_id AS "turnId", + tone, + kind, + summary, + payload_json AS "payload", + sequence, + created_at AS "createdAt" + `; + const turnColumns = sql` + thread_id AS "threadId", + turn_id AS "turnId", + checkpoint_turn_count AS "checkpointTurnCount", + checkpoint_ref AS "checkpointRef", + checkpoint_status AS "status", + checkpoint_files_json AS "files", + assistant_message_id AS "assistantMessageId", + completed_at AS "completedAt" + `; + const activityStorageColumns = sql` + activity_id, + thread_id, + turn_id, + tone, + kind, + summary, + payload_json, + sequence, + created_at + `; + const repositoryIdentityResolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver; const repositoryIdentityResolutionConcurrency = 4; const resolveRepositoryIdentitiesForProjects = Effect.fn( @@ -444,19 +542,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionProjectDbRowSchema, execute: () => sql` - SELECT - project_id AS "projectId", - title, - workspace_root AS "workspaceRoot", - default_model_selection_json AS "defaultModelSelection", - default_thread_env_mode AS "defaultThreadEnvMode", - auto_pull AS "autoPull", - favicon_path AS "faviconPath", - project_icon_json AS "projectIcon", - scripts_json AS "scripts", - created_at AS "createdAt", - updated_at AS "updatedAt", - deleted_at AS "deletedAt" + SELECT ${projectColumns} FROM projection_projects ORDER BY created_at ASC, project_id ASC `, @@ -467,34 +553,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadDbRowSchema, execute: () => sql` - SELECT - thread_id AS "threadId", - project_id AS "projectId", - title, - model_selection_json AS "modelSelection", - runtime_mode AS "runtimeMode", - interaction_mode AS "interactionMode", - branch, - worktree_path AS "worktreePath", - linked_pull_request_json AS "linkedPullRequest", - latest_turn_id AS "latestTurnId", - created_at AS "createdAt", - updated_at AS "updatedAt", - archived_at AS "archivedAt", - settled_override AS "settledOverride", - settled_at AS "settledAt", - unsettled_at AS "unsettledAt", - snoozed_until AS "snoozedUntil", - snoozed_at AS "snoozedAt", - pinned_at AS "pinnedAt", - pin_order_key AS "pinOrderKey", - title_regeneration_request_id AS "titleRegenerationRequestId", - title_regeneration_started_at AS "titleRegenerationStartedAt", - latest_user_message_at AS "latestUserMessageAt", - pending_approval_count AS "pendingApprovalCount", - pending_user_input_count AS "pendingUserInputCount", - has_actionable_proposed_plan AS "hasActionableProposedPlan", - deleted_at AS "deletedAt" + SELECT ${threadColumns} FROM projection_threads ORDER BY created_at ASC, thread_id ASC `, @@ -505,34 +564,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadDbRowSchema, execute: () => sql` - SELECT - thread_id AS "threadId", - project_id AS "projectId", - title, - model_selection_json AS "modelSelection", - runtime_mode AS "runtimeMode", - interaction_mode AS "interactionMode", - branch, - worktree_path AS "worktreePath", - linked_pull_request_json AS "linkedPullRequest", - latest_turn_id AS "latestTurnId", - created_at AS "createdAt", - updated_at AS "updatedAt", - archived_at AS "archivedAt", - settled_override AS "settledOverride", - settled_at AS "settledAt", - unsettled_at AS "unsettledAt", - snoozed_until AS "snoozedUntil", - snoozed_at AS "snoozedAt", - pinned_at AS "pinnedAt", - pin_order_key AS "pinOrderKey", - title_regeneration_request_id AS "titleRegenerationRequestId", - title_regeneration_started_at AS "titleRegenerationStartedAt", - latest_user_message_at AS "latestUserMessageAt", - pending_approval_count AS "pendingApprovalCount", - pending_user_input_count AS "pendingUserInputCount", - has_actionable_proposed_plan AS "hasActionableProposedPlan", - deleted_at AS "deletedAt" + SELECT ${threadColumns} FROM projection_threads WHERE deleted_at IS NULL AND archived_at IS NULL @@ -545,34 +577,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadDbRowSchema, execute: () => sql` - SELECT - thread_id AS "threadId", - project_id AS "projectId", - title, - model_selection_json AS "modelSelection", - runtime_mode AS "runtimeMode", - interaction_mode AS "interactionMode", - branch, - worktree_path AS "worktreePath", - linked_pull_request_json AS "linkedPullRequest", - latest_turn_id AS "latestTurnId", - created_at AS "createdAt", - updated_at AS "updatedAt", - archived_at AS "archivedAt", - settled_override AS "settledOverride", - settled_at AS "settledAt", - unsettled_at AS "unsettledAt", - snoozed_until AS "snoozedUntil", - snoozed_at AS "snoozedAt", - pinned_at AS "pinnedAt", - pin_order_key AS "pinOrderKey", - title_regeneration_request_id AS "titleRegenerationRequestId", - title_regeneration_started_at AS "titleRegenerationStartedAt", - latest_user_message_at AS "latestUserMessageAt", - pending_approval_count AS "pendingApprovalCount", - pending_user_input_count AS "pendingUserInputCount", - has_actionable_proposed_plan AS "hasActionableProposedPlan", - deleted_at AS "deletedAt" + SELECT ${threadColumns} FROM projection_threads WHERE deleted_at IS NULL AND archived_at IS NOT NULL @@ -585,16 +590,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadMessageDbRowSchema, execute: () => sql` - SELECT - message_id AS "messageId", - thread_id AS "threadId", - turn_id AS "turnId", - role, - text, - attachments_json AS "attachments", - is_streaming AS "isStreaming", - created_at AS "createdAt", - updated_at AS "updatedAt" + SELECT ${messageColumns} FROM projection_thread_messages ORDER BY thread_id ASC, created_at ASC, message_id ASC `, @@ -605,15 +601,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadProposedPlanDbRowSchema, execute: () => sql` - SELECT - plan_id AS "planId", - thread_id AS "threadId", - turn_id AS "turnId", - plan_markdown AS "planMarkdown", - implemented_at AS "implementedAt", - implementation_thread_id AS "implementationThreadId", - created_at AS "createdAt", - updated_at AS "updatedAt" + SELECT ${proposedPlanColumns} FROM projection_thread_proposed_plans ORDER BY thread_id ASC, created_at ASC, plan_id ASC `, @@ -624,16 +612,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadActivityDbRowSchema, execute: () => sql` - SELECT - activity_id AS "activityId", - thread_id AS "threadId", - turn_id AS "turnId", - tone, - kind, - summary, - payload_json AS "payload", - sequence, - created_at AS "createdAt" + SELECT ${activityColumns} FROM projection_thread_activities ORDER BY thread_id ASC, @@ -719,15 +698,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionCheckpointDbRowSchema, execute: () => sql` - SELECT - thread_id AS "threadId", - turn_id AS "turnId", - checkpoint_turn_count AS "checkpointTurnCount", - checkpoint_ref AS "checkpointRef", - checkpoint_status AS "status", - checkpoint_files_json AS "files", - assistant_message_id AS "assistantMessageId", - completed_at AS "completedAt" + SELECT ${turnColumns} FROM projection_turns WHERE checkpoint_turn_count IS NOT NULL ORDER BY thread_id ASC, checkpoint_turn_count ASC @@ -921,19 +892,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionProjectLookupRowSchema, execute: ({ workspaceRoot }) => sql` - SELECT - project_id AS "projectId", - title, - workspace_root AS "workspaceRoot", - default_model_selection_json AS "defaultModelSelection", - default_thread_env_mode AS "defaultThreadEnvMode", - auto_pull AS "autoPull", - favicon_path AS "faviconPath", - project_icon_json AS "projectIcon", - scripts_json AS "scripts", - created_at AS "createdAt", - updated_at AS "updatedAt", - deleted_at AS "deletedAt" + SELECT ${projectColumns} FROM projection_projects WHERE workspace_root = ${workspaceRoot} AND deleted_at IS NULL @@ -947,19 +906,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionProjectLookupRowSchema, execute: ({ projectId }) => sql` - SELECT - project_id AS "projectId", - title, - workspace_root AS "workspaceRoot", - default_model_selection_json AS "defaultModelSelection", - default_thread_env_mode AS "defaultThreadEnvMode", - auto_pull AS "autoPull", - favicon_path AS "faviconPath", - project_icon_json AS "projectIcon", - scripts_json AS "scripts", - created_at AS "createdAt", - updated_at AS "updatedAt", - deleted_at AS "deletedAt" + SELECT ${projectColumns} FROM projection_projects WHERE project_id = ${projectId} AND deleted_at IS NULL @@ -1007,34 +954,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadDbRowSchema, execute: ({ threadId }) => sql` - SELECT - thread_id AS "threadId", - project_id AS "projectId", - title, - model_selection_json AS "modelSelection", - runtime_mode AS "runtimeMode", - interaction_mode AS "interactionMode", - branch, - worktree_path AS "worktreePath", - linked_pull_request_json AS "linkedPullRequest", - latest_turn_id AS "latestTurnId", - created_at AS "createdAt", - updated_at AS "updatedAt", - archived_at AS "archivedAt", - settled_override AS "settledOverride", - settled_at AS "settledAt", - unsettled_at AS "unsettledAt", - snoozed_until AS "snoozedUntil", - snoozed_at AS "snoozedAt", - pinned_at AS "pinnedAt", - pin_order_key AS "pinOrderKey", - title_regeneration_request_id AS "titleRegenerationRequestId", - title_regeneration_started_at AS "titleRegenerationStartedAt", - latest_user_message_at AS "latestUserMessageAt", - pending_approval_count AS "pendingApprovalCount", - pending_user_input_count AS "pendingUserInputCount", - has_actionable_proposed_plan AS "hasActionableProposedPlan", - deleted_at AS "deletedAt" + SELECT ${threadColumns} FROM projection_threads WHERE thread_id = ${threadId} AND deleted_at IS NULL @@ -1082,16 +1002,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadMessageDbRowSchema, execute: ({ threadId }) => sql` - SELECT - message_id AS "messageId", - thread_id AS "threadId", - turn_id AS "turnId", - role, - text, - attachments_json AS "attachments", - is_streaming AS "isStreaming", - created_at AS "createdAt", - updated_at AS "updatedAt" + SELECT ${messageColumns} FROM projection_thread_messages WHERE thread_id = ${threadId} ORDER BY created_at ASC, message_id ASC @@ -1103,15 +1014,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadProposedPlanDbRowSchema, execute: ({ threadId }) => sql` - SELECT - plan_id AS "planId", - thread_id AS "threadId", - turn_id AS "turnId", - plan_markdown AS "planMarkdown", - implemented_at AS "implementedAt", - implementation_thread_id AS "implementationThreadId", - created_at AS "createdAt", - updated_at AS "updatedAt" + SELECT ${proposedPlanColumns} FROM projection_thread_proposed_plans WHERE thread_id = ${threadId} ORDER BY created_at ASC, plan_id ASC @@ -1123,28 +1026,10 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadActivityDbRowSchema, execute: ({ threadId }) => sql` - SELECT - activity_id AS "activityId", - thread_id AS "threadId", - turn_id AS "turnId", - tone, - kind, - summary, - payload_json AS "payload", - sequence, - created_at AS "createdAt" + SELECT ${activityColumns} FROM ( - SELECT - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - sequence, - created_at - FROM projection_thread_activities + SELECT ${activityStorageColumns} + FROM projection_thread_activities WHERE thread_id = ${threadId} ORDER BY sequence DESC, @@ -1163,17 +1048,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Request: Schema.Struct({ threadId: ThreadId, requestId: ApprovalRequestId }), Result: ProjectionThreadActivityDbRowSchema, execute: ({ threadId, requestId }) => sql` - SELECT - activity_id AS "activityId", - thread_id AS "threadId", - turn_id AS "turnId", - tone, - kind, - summary, - payload_json AS "payload", - sequence, - created_at AS "createdAt" - FROM projection_thread_activities + SELECT ${activityColumns} + FROM projection_thread_activities WHERE thread_id = ${threadId} AND kind IN ('user-input.requested', 'user-input.resolved') AND json_extract(payload_json, '$.requestId') = ${requestId} @@ -1214,16 +1090,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadActivityDbRowSchema, execute: ({ activityIds }) => sql` - SELECT - activity_id AS "activityId", - thread_id AS "threadId", - turn_id AS "turnId", - tone, - kind, - summary, - payload_json AS "payload", - sequence, - created_at AS "createdAt" + SELECT ${activityColumns} FROM projection_thread_activities -- The selectors already scoped these globally unique ids to the -- thread inside this transaction. Keep this as a primary-key lookup. @@ -1236,28 +1103,10 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadActivityDbRowSchema, execute: ({ threadId, activityKinds }) => sql` - SELECT - activity_id AS "activityId", - thread_id AS "threadId", - turn_id AS "turnId", - tone, - kind, - summary, - payload_json AS "payload", - sequence, - created_at AS "createdAt" + SELECT ${activityColumns} FROM ( - SELECT - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - sequence, - created_at - FROM projection_thread_activities + SELECT ${activityStorageColumns} + FROM projection_thread_activities WHERE thread_id = ${threadId} AND ${sql.in("kind", activityKinds)} ORDER BY @@ -1324,15 +1173,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionCheckpointDbRowSchema, execute: ({ threadId }) => sql` - SELECT - thread_id AS "threadId", - turn_id AS "turnId", - checkpoint_turn_count AS "checkpointTurnCount", - checkpoint_ref AS "checkpointRef", - checkpoint_status AS "status", - checkpoint_files_json AS "files", - assistant_message_id AS "assistantMessageId", - completed_at AS "completedAt" + SELECT ${turnColumns} FROM projection_turns WHERE thread_id = ${threadId} AND checkpoint_turn_count IS NOT NULL @@ -1439,16 +1280,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { Result: ProjectionThreadMessageDbRowSchema, execute: ({ threadId, minAnchorAt, minTurnKey, beforeAnchorAt, beforeTurnKey }) => sql` - SELECT - message_id AS "messageId", - thread_id AS "threadId", - turn_id AS "turnId", - role, - text, - attachments_json AS "attachments", - is_streaming AS "isStreaming", - created_at AS "createdAt", - updated_at AS "updatedAt" + SELECT ${messageColumns} FROM projection_thread_messages WHERE thread_id = ${threadId} AND ( @@ -1590,28 +1422,10 @@ pending_approval_requests AS ( Result: ProjectionThreadActivityDbRowSchema, execute: ({ threadId, minAnchorAt, minTurnKey, beforeAnchorAt, beforeTurnKey }) => sql` - SELECT - activity_id AS "activityId", - thread_id AS "threadId", - turn_id AS "turnId", - tone, - kind, - summary, - payload_json AS "payload", - sequence, - created_at AS "createdAt" + SELECT ${activityColumns} FROM ( - SELECT - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - sequence, - created_at - FROM projection_thread_activities + SELECT ${activityStorageColumns} + FROM projection_thread_activities WHERE thread_id = ${threadId} AND ( turn_id IN ( diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index b336053ac9e1..522fc40617db 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -116,12 +116,9 @@ function hasQueuedTurnStartForThread( } function withEventBase( - input: Pick & { - readonly aggregateKind: OrchestrationEvent["aggregateKind"]; - readonly aggregateId: OrchestrationEvent["aggregateId"]; - readonly occurredAt: string; - readonly metadata?: OrchestrationEvent["metadata"]; - }, + command: OrchestrationCommand, + occurredAt: string, + metadata: OrchestrationEvent["metadata"] = {}, ): Effect.Effect< Omit, PlatformError.PlatformError, @@ -132,13 +129,13 @@ function withEventBase( crypto.randomUUIDv4.pipe( Effect.map((eventId) => ({ eventId: EventId.make(eventId), - aggregateKind: input.aggregateKind, - aggregateId: input.aggregateId, - occurredAt: input.occurredAt, - commandId: input.commandId, + aggregateKind: "threadId" in command ? "thread" : "project", + aggregateId: "threadId" in command ? command.threadId : command.projectId, + occurredAt, + commandId: command.commandId, causationEventId: null, - correlationId: input.commandId, - metadata: input.metadata ?? {}, + correlationId: command.commandId, + metadata, })), ), ), @@ -213,12 +210,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); return { - ...(yield* withEventBase({ - aggregateKind: "project", - aggregateId: command.projectId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "project.created", payload: { projectId: command.projectId, @@ -253,12 +245,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "project", - aggregateId: command.projectId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "project.meta-updated", payload: { projectId: command.projectId, @@ -316,12 +303,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "project", - aggregateId: command.projectId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "project.deleted" as const, payload: { projectId: command.projectId, @@ -342,12 +324,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.created", payload: { threadId: command.threadId, @@ -372,12 +349,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.deleted", payload: { threadId: command.threadId, @@ -394,12 +366,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.archived", payload: { threadId: command.threadId, @@ -417,12 +384,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unarchived", payload: { threadId: command.threadId, @@ -467,12 +429,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" // double-click must stay silent no-ops rather than surface errors. const alreadySettled = thread.settledOverride === "settled" && thread.settledAt !== null; const settledEvent = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.settled" as const, payload: { threadId: command.threadId, @@ -492,12 +449,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const companionEvents: Array> = []; if (thread.pinnedAt != null) { companionEvents.push({ - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unpinned" as const, payload: { threadId: command.threadId, @@ -507,12 +459,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } if (thread.snoozedUntil != null) { companionEvents.push({ - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unsnoozed", payload: { threadId: command.threadId, @@ -536,12 +483,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const alreadyPinnedActive = thread.settledOverride === "active"; const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unsettled", payload: { threadId: command.threadId, @@ -605,12 +547,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ? thread.snoozedAt : null; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.snoozed", payload: { threadId: command.threadId, @@ -633,12 +570,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const alreadyAwake = thread.snoozedUntil == null; const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unsnoozed", payload: { threadId: command.threadId, @@ -661,12 +593,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" // only ever promotes visibility, so it can never hide pending work. const existingPinnedAt = thread.pinnedAt ?? null; const pinnedEvent = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.pinned" as const, payload: { threadId: command.threadId, @@ -687,12 +614,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const promotionEvents: Array> = []; if (thread.settledOverride === "settled") { promotionEvents.push({ - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unsettled", payload: { threadId: command.threadId, @@ -703,12 +625,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } if (thread.snoozedUntil != null) { promotionEvents.push({ - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unsnoozed", payload: { threadId: command.threadId, @@ -732,12 +649,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const alreadyUnpinned = thread.pinnedAt == null; const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.unpinned", payload: { threadId: command.threadId, @@ -768,12 +680,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const keyUnchanged = thread.pinOrderKey === command.orderKey; const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.pin-reordered", payload: { threadId: command.threadId, @@ -797,12 +704,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" : command.branch; const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.meta-updated", payload: { threadId: command.threadId, @@ -842,12 +744,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const requestIsCurrent = thread.titleRegeneration?.requestId === command.requestId; const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.meta-updated", payload: { threadId: command.threadId, @@ -866,12 +763,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.runtime-mode-set", payload: { threadId: command.threadId, @@ -889,12 +781,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, occurredAt)), type: "thread.interaction-mode-set", payload: { threadId: command.threadId, @@ -935,12 +822,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); } const userMessageEvent: Omit = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.message-sent", payload: { threadId: command.threadId, @@ -955,12 +837,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }, }; const turnStartRequestedEvent: Omit = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), causationEventId: userMessageEvent.eventId, type: "thread.turn-start-requested", payload: { @@ -984,12 +861,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const lifecycleResetEvents: Array> = []; if (targetThread.settledOverride !== null) { lifecycleResetEvents.push({ - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.unsettled", payload: { threadId: command.threadId, @@ -1000,12 +872,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } if (targetThread.snoozedUntil != null) { lifecycleResetEvents.push({ - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.unsnoozed", payload: { threadId: command.threadId, @@ -1024,12 +891,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.turn-interrupt-requested", payload: { threadId: command.threadId, @@ -1046,14 +908,8 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - metadata: { - requestId: command.requestId, - }, + ...(yield* withEventBase(command, command.createdAt, { + requestId: command.requestId, })), type: "thread.approval-response-requested", payload: { @@ -1137,14 +993,8 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); } return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - metadata: { - requestId: command.requestId, - }, + ...(yield* withEventBase(command, command.createdAt, { + requestId: command.requestId, })), type: "thread.user-input-response-requested", payload: { @@ -1163,12 +1013,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.checkpoint-revert-requested", payload: { threadId: command.threadId, @@ -1206,12 +1051,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } } return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.session-stop-requested", payload: { threadId: command.threadId, @@ -1227,13 +1067,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); const sessionSetEvent: Omit = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - metadata: {}, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.session-set", payload: { threadId: command.threadId, @@ -1255,12 +1089,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" return sessionSetEvent; } const unsettledEvent: Omit = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.unsettled", payload: { threadId: command.threadId, @@ -1278,12 +1107,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.message-sent", payload: { threadId: command.threadId, @@ -1305,12 +1129,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.message-sent", payload: { threadId: command.threadId, @@ -1332,12 +1151,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.proposed-plan-upserted", payload: { threadId: command.threadId, @@ -1353,12 +1167,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.turn-diff-completed", payload: { threadId: command.threadId, @@ -1380,12 +1189,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.reverted", payload: { threadId: command.threadId, @@ -1409,13 +1213,11 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" .requestId as OrchestrationEvent["metadata"]["requestId"]) : undefined; const activityAppendedEvent: Omit = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - ...(requestId !== undefined ? { metadata: { requestId } } : {}), - })), + ...(yield* withEventBase( + command, + command.createdAt, + requestId !== undefined ? { requestId } : {}, + )), type: "thread.activity-appended", payload: { threadId: command.threadId, @@ -1432,12 +1234,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" return activityAppendedEvent; } const unsettledEvent: Omit = { - ...(yield* withEventBase({ - aggregateKind: "thread", - aggregateId: command.threadId, - occurredAt: command.createdAt, - commandId: command.commandId, - })), + ...(yield* withEventBase(command, command.createdAt)), type: "thread.unsettled", payload: { threadId: command.threadId, diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 84cc03b75a6d..7fad4f909d03 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -197,23 +197,26 @@ function makeHarness(config?: { : {}), }; + const layer = Layer.effect( + ClaudeAdapter, + Effect.gen(function* () { + const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {}); + return yield* makeClaudeAdapter(claudeConfig, adapterOptions); + }), + ).pipe( + Layer.provideMerge( + ServerConfig.layerTest(config?.cwd ?? "/tmp/claude-adapter-test", config?.baseDir ?? "/tmp"), + ), + Layer.provideMerge(ServerSettingsService.layerTest()), + Layer.provideMerge(NodeServices.layer), + ); return { - layer: Layer.effect( - ClaudeAdapter, - Effect.gen(function* () { - const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {}); - return yield* makeClaudeAdapter(claudeConfig, adapterOptions); - }), - ).pipe( - Layer.provideMerge( - ServerConfig.layerTest( - config?.cwd ?? "/tmp/claude-adapter-test", - config?.baseDir ?? "/tmp", - ), + layer, + provide: (effect: Effect.Effect) => + effect.pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(layer), ), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(NodeServices.layer), - ), query, getLastCreateQueryInput: () => createInput, }; @@ -331,10 +334,7 @@ describe("ClaudeAdapterLive", () => { issue: "Expected provider 'claudeAgent' but received 'codex'.", }), ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("retains Claude session startup causes without exposing their messages", () => { @@ -395,10 +395,7 @@ describe("ClaudeAdapterLive", () => { }); assert.equal(createInput?.options.permissionMode, "bypassPermissions"); assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("derives auto permission mode from auto runtime policy without skip flag", () => { @@ -414,10 +411,7 @@ describe("ClaudeAdapterLive", () => { const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.permissionMode, "auto"); assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("loads Claude filesystem settings sources for SDK sessions", () => { @@ -434,10 +428,7 @@ describe("ClaudeAdapterLive", () => { assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]); assert.equal(createInput?.options.permissionMode, undefined); assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("uses bypass permissions for full-access claude sessions", () => { @@ -453,10 +444,7 @@ describe("ClaudeAdapterLive", () => { const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.permissionMode, "bypassPermissions"); assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("passes the configured auto-compaction window to Claude", () => { @@ -472,10 +460,7 @@ describe("ClaudeAdapterLive", () => { const options = harness.getLastCreateQueryInput()?.options; assert.deepEqual(options?.settings, { autoCompactWindow: 300000 }); assert.deepEqual(options?.supportedDialogKinds, ["resume_return"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("forwards claude effort levels into query options", () => { @@ -495,10 +480,7 @@ describe("ClaudeAdapterLive", () => { const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.effort, "max"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("runs Claude SDK sessions with the configured CLAUDE_CONFIG_DIR", () => { @@ -520,10 +502,7 @@ describe("ClaudeAdapterLive", () => { createInput?.options.env?.CLAUDE_CONFIG_DIR, NodePath.join(NodeOS.homedir(), ".claude-work"), ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("forwards Claude thinking toggle for models that support it", () => { @@ -545,10 +524,7 @@ describe("ClaudeAdapterLive", () => { assert.deepEqual(createInput?.options.settings, { alwaysThinkingEnabled: false, }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("ignores Claude thinking toggle for models without it", () => { @@ -568,10 +544,7 @@ describe("ClaudeAdapterLive", () => { const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.settings, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("forwards claude fast mode into SDK settings", () => { @@ -593,10 +566,7 @@ describe("ClaudeAdapterLive", () => { assert.deepEqual(createInput?.options.settings, { fastMode: true, }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("ignores claude fast mode for models without it", () => { @@ -616,10 +586,7 @@ describe("ClaudeAdapterLive", () => { const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.settings, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect( @@ -642,10 +609,7 @@ describe("ClaudeAdapterLive", () => { runtimeMode: "full-access", }); return harness.getLastCreateQueryInput()!.options; - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); const runCustomFlow = Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ @@ -743,10 +707,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(createInput?.options.effort, "high"); const promptText = yield* Effect.promise(() => readFirstPromptText(createInput)); assert.equal(promptText, "Ultrathink:\nInvestigate the edge cases"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("keeps compact commands intact when ultrathink is selected", () => { @@ -776,10 +737,7 @@ describe("ClaudeAdapterLive", () => { readFirstPromptText(harness.getLastCreateQueryInput()), ); assert.equal(promptText, "/compact"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("embeds image attachments in Claude user messages", () => { @@ -841,10 +799,7 @@ describe("ClaudeAdapterLive", () => { text: "What's in this image?", }, ]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); // The Claude CLI reads a streamed user message as a slash-command invocation @@ -936,10 +891,7 @@ describe("ClaudeAdapterLive", () => { // through the path line ProviderService writes into the prompt, so the // text block stays last on its own. assert.deepEqual(prompts[2]?.message.content, [commandBlock]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("dispatches a $skill mention as a trailing slash command block", () => { @@ -976,10 +928,7 @@ describe("ClaudeAdapterLive", () => { { type: "text", text: "ok, now" }, { type: "text", text: "/implement all the tickets\nstart with auth" }, ]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("keeps the skill command block after image attachments", () => { @@ -1030,10 +979,7 @@ describe("ClaudeAdapterLive", () => { blocks.map((block) => (block.type === "text" ? block.text : block.type)), ["image", "/review this screenshot"], ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("leaves a $ mention of an unknown or disabled skill as prose", () => { @@ -1068,10 +1014,7 @@ describe("ClaudeAdapterLive", () => { readFirstPromptText(harness.getLastCreateQueryInput()), ); assert.equal(promptText, "run $deploy and echo $HOME"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => { @@ -1245,10 +1188,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(String(turnCompleted.turnId), String(turn.turnId)); assert.equal(turnCompleted.payload.state, "completed"); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("places overage-included rate-limit events on the bucket the probe named", () => { @@ -1323,10 +1263,7 @@ describe("ClaudeAdapterLive", () => { ], }, ]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("does not emit turn.completed for a result with no active turn", () => { @@ -1392,10 +1329,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(String(completed.turnId), String(turn.turnId)); assert.equal(completed.payload.state, "completed"); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("steers a running turn instead of opening a new one on mid-turn sendTurn", () => { @@ -1459,10 +1393,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(String(turnStartedEvents[0]?.turnId), String(turn.turnId)); assert.equal(turnCompletedEvents.length, 1); assert.equal(String(turnCompletedEvents[0]?.turnId), String(turn.turnId)); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("maps Claude reasoning deltas, streamed tool inputs, and tool results", () => { @@ -1638,10 +1569,7 @@ describe("ClaudeAdapterLive", () => { "src/example.ts:1:foo", ); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("classifies only streamed Read image inputs as image views", () => { @@ -1799,10 +1727,7 @@ describe("ClaudeAdapterLive", () => { textEvents.map((event) => event.payload.itemType), ["dynamic_tool_call", "dynamic_tool_call", "dynamic_tool_call"], ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("falls back to a default plan step label for blank TodoWrite content", () => { @@ -1890,10 +1815,7 @@ describe("ClaudeAdapterLive", () => { { step: "Ship it", status: "completed" }, ]); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("classifies Claude Task tool invocations as collaboration agent work", () => { @@ -1989,10 +1911,7 @@ describe("ClaudeAdapterLive", () => { if (completed?.type === "turn.completed") { assert.equal(completed.payload.tokenUsage?.hasSubagents, true); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("treats user-aborted Claude results as interrupted without a runtime error", () => { @@ -2065,10 +1984,7 @@ describe("ClaudeAdapterLive", () => { hasSubagents: false, }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("treats aborted_tools results as interrupted and hides ede_diagnostic errors", () => { @@ -2126,10 +2042,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(turnCompleted.payload.state, "interrupted"); assert.equal(turnCompleted.payload.errorMessage, undefined); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("fails a turn when the result carries a give-up terminal_reason", () => { @@ -2193,10 +2106,7 @@ describe("ClaudeAdapterLive", () => { "Claude gave up after repeated API errors.", ); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("fails a turn for every dead-turn terminal_reason", () => { @@ -2245,10 +2155,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(completed.value.payload.state, "failed", reason); assert.ok(completed.value.payload.errorMessage, `${reason} carries an error message`); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }; return Effect.forEach(reasons, runDeadTurn, { discard: true }); }); @@ -2297,10 +2204,7 @@ describe("ClaudeAdapterLive", () => { "Claude API is overloaded (529). Try again shortly.", ); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("interruptTurn settles live tasks and closes the provider session", () => { @@ -2385,10 +2289,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(stoppedTaskEvent.payload.taskType, "local_agent"); assert.equal(stoppedTaskEvent.payload.title, "Agent A"); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("keeps the session available when process close fails", () => { @@ -2411,10 +2312,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(harness.query.closeCalls, 1); assert.equal(yield* adapter.hasSession(session.threadId), true); assert.equal((yield* adapter.listSessions())[0]?.status, "ready"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("stopAll attempts every session when one process close fails", () => { @@ -2592,10 +2490,7 @@ describe("ClaudeAdapterLive", () => { hasSubagents: false, }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("treats omitted Claude cache counters as zero contributions", () => { @@ -2644,10 +2539,7 @@ describe("ClaudeAdapterLive", () => { hasSubagents: false, }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("uses per-turn result usage across consecutive Claude turns", () => { @@ -2777,10 +2669,7 @@ describe("ClaudeAdapterLive", () => { hasSubagents: false, }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("preserves compacted usage when completion follows an older assistant frame", () => { @@ -2880,10 +2769,7 @@ describe("ClaudeAdapterLive", () => { runtimeEvents.find((event) => event.type === "turn.completed")?.type, "turn.completed", ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("workflow member coalescing: identical snapshots suppress, changes emit", () => { @@ -2968,10 +2854,7 @@ describe("ClaudeAdapterLive", () => { // tick 3 unchanged). assert.equal(byMember.get("wf-coalesce:wf:0"), 2); assert.equal(byMember.get("wf-coalesce:wf:1"), 1); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("task.started carries model/effort; subagent snapshots refine the model", () => { @@ -3049,10 +2932,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(progress.payload.model, SYNTHETIC_SUBAGENT_MODEL); assert.equal(progress.payload.effort, "max"); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("a subagent snapshot that beats task_started still wins over the seed", () => { @@ -3127,10 +3007,7 @@ describe("ClaudeAdapterLive", () => { if (progress?.type === "task.progress") { assert.equal(progress.payload.model, SYNTHETIC_SUBAGENT_MODEL); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("closes the session when the Claude stream aborts after a turn starts", () => { @@ -3195,10 +3072,7 @@ describe("ClaudeAdapterLive", () => { const sessions = yield* adapter.listSessions(); assert.equal(sessions.length, 0); assert.equal(harness.query.closeCalls, 1); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("keeps Claude stream failure events structural", () => { @@ -3246,10 +3120,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(completed.payload.state, "failed"); assert.equal(completed.payload.errorMessage, "Claude runtime stream failed."); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("closes the previous session before replacing an existing thread session", () => { @@ -3440,10 +3311,7 @@ describe("ClaudeAdapterLive", () => { ); assert.equal(progressEvent.payload.description, "Running background teammate"); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("consumes undeclared and UX-internal system subtypes without warning rows", () => { @@ -3657,10 +3525,7 @@ describe("ClaudeAdapterLive", () => { event.payload.reason.startsWith("api_retry:"), ); assert.equal(heartbeat?.type, "session.state.changed"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); const observeUsageLimitEvents = (adapter: ClaudeAdapterShape, query: FakeClaudeQuery) => @@ -3787,10 +3652,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(usageLimitRows().length, 2); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("keeps allowed and malformed Claude rate-limit events out of the work log", () => { @@ -3834,10 +3696,7 @@ describe("ClaudeAdapterLive", () => { ); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("stays quiet when no turn is parked by the Claude limit", () => { @@ -3905,10 +3764,7 @@ describe("ClaudeAdapterLive", () => { ); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("still surfaces the pause when overage is exhausted too", () => { @@ -3948,10 +3804,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(runtimeEvents.filter((event) => event.type === "runtime.warning").length, 1); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("keeps one row per window when two Claude limits interleave", () => { @@ -4000,10 +3853,7 @@ describe("ClaudeAdapterLive", () => { ); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("re-announces a Claude limit for a synthetic turn", () => { @@ -4063,10 +3913,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(runtimeEvents.filter((event) => event.type === "runtime.warning").length, 2); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("drops an unusable Claude reset time, not the row or the session", () => { @@ -4117,10 +3964,7 @@ describe("ClaudeAdapterLive", () => { yield* adapter.sendTurn({ threadId: THREAD_ID, input: "still here", attachments: [] }); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("warns for unmapped Claude limits and names the probed model bucket", () => { @@ -4180,10 +4024,7 @@ describe("ClaudeAdapterLive", () => { 1, ); runtimeEventsFiber.interruptUnsafe(); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("consumes Claude command lifecycle notifications silently", () => { @@ -4251,10 +4092,7 @@ describe("ClaudeAdapterLive", () => { if (warning?.type === "runtime.warning") { assert.equal(warning.payload.message, processedMessage); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("emits thread token usage updates from Claude task progress", () => { @@ -4305,10 +4143,7 @@ describe("ClaudeAdapterLive", () => { if (usageEvent && progressEvent) { assert.notStrictEqual(usageEvent.eventId, progressEvent.eventId); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("emits Claude context window on result completion usage snapshots", () => { @@ -4372,10 +4207,7 @@ describe("ClaudeAdapterLive", () => { }, }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("clamps oversized Claude usage to the reported context window", () => { @@ -4435,10 +4267,7 @@ describe("ClaudeAdapterLive", () => { }, }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect( @@ -4515,10 +4344,7 @@ describe("ClaudeAdapterLive", () => { }, }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }, ); @@ -4608,10 +4434,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(deltaEvent.payload.delta, "Late text"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }, ); @@ -4775,10 +4598,7 @@ describe("ClaudeAdapterLive", () => { String(assistantCompletions[0]?.itemId), String(assistantCompletions[1]?.itemId), ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("falls back to assistant payload text when stream deltas are absent", () => { @@ -4844,10 +4664,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(deltaEvent.payload.delta, "Fallback hello"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("segments Claude assistant text blocks around tool calls", () => { @@ -5066,10 +4883,7 @@ describe("ClaudeAdapterLive", () => { toolStartedIndex < secondAssistantDeltaIndex, true, ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("does not fabricate provider thread ids before first SDK session_id", () => { @@ -5144,10 +4958,7 @@ describe("ClaudeAdapterLive", () => { providerThreadId: "sdk-thread-real", }); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("bridges approval request/response lifecycle through canUseTool", () => { @@ -5254,10 +5065,7 @@ describe("ClaudeAdapterLive", () => { const permissionResult = yield* Effect.promise(() => permissionPromise); assert.equal((permissionResult as PermissionResult).behavior, "allow"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("acceptForSession returns session-scoped permission updates", () => { @@ -5368,10 +5176,7 @@ describe("ClaudeAdapterLive", () => { destination: "session", }, ]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("classifies Agent tools and read-only Claude tools correctly for approvals", () => { @@ -5443,10 +5248,7 @@ describe("ClaudeAdapterLive", () => { ); yield* Stream.runHead(adapter.streamEvents); yield* Effect.promise(() => grepPermissionPromise); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("passes Claude resume ids without pinning a stale assistant checkpoint", () => { @@ -5478,10 +5280,7 @@ describe("ClaudeAdapterLive", () => { assert.equal(createInput?.options.resume, "550e8400-e29b-41d4-a716-446655440000"); assert.equal(createInput?.options.sessionId, undefined); assert.equal(createInput?.options.resumeSessionAt, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("preserves durable resume ids across Claude resume hooks", () => { @@ -5568,10 +5367,7 @@ describe("ClaudeAdapterLive", () => { } | undefined; assert.equal(resumeCursor?.resume, durableSessionId); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("uses an app-generated Claude session id for fresh sessions", () => { @@ -5600,10 +5396,7 @@ describe("ClaudeAdapterLive", () => { ); assert.equal(createInput?.options.resume, undefined); assert.equal(createInput?.options.sessionId, sessionResumeCursor.resume); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect( @@ -5681,10 +5474,7 @@ describe("ClaudeAdapterLive", () => { const threadAfterRollback = yield* adapter.readThread(session.threadId); assert.equal(threadAfterRollback.turns.length, 1); assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }, ); @@ -5711,10 +5501,7 @@ describe("ClaudeAdapterLive", () => { assert.deepEqual(harness.query.setModelCalls, [ `${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded]`, ]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("updates model on sendTurn for the adapter's bound custom instance id", () => { @@ -5739,10 +5526,7 @@ describe("ClaudeAdapterLive", () => { }); assert.deepEqual(harness.query.setModelCalls, ["openai/gpt-5.5"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect( @@ -5777,10 +5561,7 @@ describe("ClaudeAdapterLive", () => { }); assert.deepEqual(harness.query.setModelCalls, []); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }, ); @@ -5820,10 +5601,7 @@ describe("ClaudeAdapterLive", () => { `${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded]`, SYNTHETIC_CLAUDE_CAPABLE_MODEL, ]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("sets plan permission mode on sendTurn when interactionMode is plan", () => { @@ -5844,10 +5622,7 @@ describe("ClaudeAdapterLive", () => { }); assert.deepEqual(harness.query.setPermissionModeCalls, ["plan"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect.each<{ runtimeMode: RuntimeMode; expectedBase: PermissionMode }>([ @@ -5901,10 +5676,7 @@ describe("ClaudeAdapterLive", () => { }); assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", expectedBase]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }, ); @@ -5925,10 +5697,7 @@ describe("ClaudeAdapterLive", () => { }); assert.deepEqual(harness.query.setPermissionModeCalls, []); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("captures ExitPlanMode as a proposed plan and denies auto-exit", () => { @@ -5992,10 +5761,7 @@ describe("ClaudeAdapterLive", () => { message?: string; }; assert.equal(deniedResult.message?.includes("captured your proposed plan"), true); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("extracts proposed plans from assistant ExitPlanMode snapshots", () => { @@ -6063,10 +5829,7 @@ describe("ClaudeAdapterLive", () => { assert.deepEqual(proposedEvent.value.providerRefs, { providerItemId: ProviderItemId.make("tool-exit-2"), }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("routes Claude resume compaction through the shared user-input UI", () => { @@ -6119,10 +5882,7 @@ describe("ClaudeAdapterLive", () => { behavior: "completed", result: "compact", }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("handles AskUserQuestion via user-input.requested/resolved lifecycle", () => { @@ -6273,10 +6033,7 @@ describe("ClaudeAdapterLive", () => { .join(", "); assert.notEqual(v121Rendered, "", "Expected non-empty SDK 2.1.121 tool_result (#2388)"); assert.equal(v121Rendered, '"Which framework?"="React"'); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("routes AskUserQuestion through user-input flow even in full-access mode", () => { @@ -6342,10 +6099,7 @@ describe("ClaudeAdapterLive", () => { const updatedInput = (permissionResult as { updatedInput: Record }) .updatedInput; assert.deepEqual(updatedInput.answers, { "Deploy to which env?": "Staging" }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("denies AskUserQuestion when the waiting turn is aborted", () => { @@ -6411,10 +6165,7 @@ describe("ClaudeAdapterLive", () => { behavior: "deny", message: "User cancelled tool execution.", } satisfies PermissionResult); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("denies AskUserQuestion when the signal aborted before the listener registered", () => { @@ -6479,10 +6230,7 @@ describe("ClaudeAdapterLive", () => { if (resolvedEvent?.type === "user-input.resolved") { assert.deepEqual(resolvedEvent.payload.answers, {}); } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("stopping a session settles pending user-input waits", () => { @@ -6544,10 +6292,7 @@ describe("ClaudeAdapterLive", () => { behavior: "deny", message: "User cancelled tool execution.", } satisfies PermissionResult); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); it.effect("writes provider-native observability records when enabled", () => { @@ -6646,9 +6391,6 @@ describe("ClaudeAdapterLive", () => { nativeThreadIds.every((threadId) => threadId === String(THREAD_ID)), true, ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); + }).pipe(harness.provide); }); }); diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index f5b9be91650f..6397da8ca259 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -81,6 +81,18 @@ const serverConfigTestLayer = ServerConfig.layerTest(process.cwd(), process.cwd( Layer.provide(NodeServices.layer), ); +const provideTestServices = (layer: Layer.Layer) => + layer.pipe( + Layer.provide(serverConfigTestLayer), + Layer.provide(AnalyticsService.layerTest), + Layer.provide( + Layer.succeed( + ProviderEventLoggers.ProviderEventLoggers, + ProviderEventLoggers.NoOpProviderEventLoggers, + ), + ), + ); + // startSession verifies the workspace folder exists before dispatching to an // adapter, so session cwd fixtures must be real directories. const fixtureCwdRoot = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "provider-service-test-")); @@ -501,14 +513,7 @@ for (const [enabled, completed] of [ ), ), Layer.provide(ServerSettings.layerTest({ continueThreadsAfterServerUpdate: enabled })), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ), ).pipe(Scope.provide(scope)); const provider = yield* ProviderService.ProviderService.pipe(Effect.provide(services)); @@ -794,14 +799,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled providers", () Layer.provide(providerAdapterLayer), Layer.provide(directoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); const failure = yield* Effect.flip( @@ -878,14 +876,7 @@ it.effect( Layer.provide(providerAdapterLayer), Layer.provide(directoryLayer), Layer.provide(serverSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); const session = yield* Effect.gen(function* () { @@ -948,14 +939,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance Layer.provide(providerAdapterLayer), Layer.provide(directoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); const failure = yield* Effect.flip( @@ -1121,14 +1105,7 @@ it.effect( Layer.provide(Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, registry)), Layer.provide(directoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); yield* Effect.gen(function* () { @@ -1180,14 +1157,7 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se Layer.provide(Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, registry)), Layer.provide(directoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); yield* Effect.gen(function* () { @@ -1242,14 +1212,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( Layer.provide(Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, registry)), Layer.provide(directoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); yield* ProviderService.ProviderService.pipe(Effect.provide(providerLayer)); @@ -1310,14 +1273,7 @@ it.effect( ), Layer.provide(firstDirectoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); const updatedResumeCursor = { threadId: asThreadId("thread-1"), @@ -1371,14 +1327,7 @@ it.effect( ), Layer.provide(secondDirectoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); secondCodex.startSession.mockClear(); @@ -2452,14 +2401,7 @@ routing.layer("ProviderServiceLive routing", (it) => { ), Layer.provide(firstDirectoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); const initial = yield* Effect.gen(function* () { @@ -2492,14 +2434,7 @@ routing.layer("ProviderServiceLive routing", (it) => { ), Layer.provide(secondDirectoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); secondClaude.startSession.mockClear(); @@ -2562,14 +2497,7 @@ routing.layer("ProviderServiceLive routing", (it) => { ), Layer.provide(firstDirectoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); const initial = yield* Effect.gen(function* () { @@ -2597,14 +2525,7 @@ routing.layer("ProviderServiceLive routing", (it) => { ), Layer.provide(secondDirectoryLayer), Layer.provide(defaultServerSettingsLayer), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); secondClaude.startSession.mockClear(); @@ -4329,14 +4250,7 @@ describe("agent browser access", () => { Layer.provide(providerAdapterLayer), Layer.provide(directoryLayer), Layer.provide(ServerSettings.ServerSettingsService.layerTest({ enableAgentBrowserAccess })), - Layer.provide(serverConfigTestLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), + provideTestServices, ); yield* Effect.gen(function* () { diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index 9663aeacb63f..53c6bd20105b 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -105,20 +105,6 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman return result; }).pipe(Effect.scoped); -export function detailFromResult( - result: CommandResult & { readonly timedOut?: boolean }, -): string | undefined { - if (result.timedOut) return "Timed out while running command."; - const stderr = nonEmptyTrimmed(result.stderr); - if (stderr) return stderr; - const stdout = nonEmptyTrimmed(result.stdout); - if (stdout) return stdout; - if (result.code !== 0) { - return `Command exited with code ${result.code}.`; - } - return undefined; -} - export function extractAuthBoolean(value: unknown): boolean | undefined { if (globalThis.Array.isArray(value)) { for (const entry of value) { diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index bebde7eded76..59d5e1229707 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -1523,6 +1523,31 @@ const getAuthenticatedSessionCookieHeader = (credential = defaultDesktopBootstra return cookie.split(";")[0] ?? cookie; }); +const configureCloudRelay = Effect.fn("configureCloudRelay")(function* () { + const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { + privateKeyEncoding: { format: "pem", type: "pkcs8" }, + publicKeyEncoding: { format: "pem", type: "spki" }, + }); + const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); + const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); + const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { + method: "POST", + headers: { + cookie: ownerCookie, + "content-type": "application/json", + }, + body: jsonRequestBody({ + relayUrl: "https://relay.example.test", + cloudUserId: "user_123", + environmentCredential: "t3env_test_credential", + cloudMintPublicKey: cloudKeyPair.publicKey, + endpointRuntime: null, + }), + }); + assert.equal(relayConfigResponse.status, 200); + return cloudKeyPair; +}); + const getAuthenticatedBearerSessionToken = (credential = defaultDesktopBootstrapToken) => Effect.gen(function* () { const { response, body } = yield* exchangeAccessToken(credential); @@ -3311,27 +3336,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.gen(function* () { yield* buildAppUnderTest(); - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); + const cloudKeyPair = yield* configureCloudRelay(); const now = yield* DateTime.now; const request = makeCloudMintCredentialRequest({ @@ -3370,27 +3375,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.gen(function* () { yield* buildAppUnderTest(); - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); + const cloudKeyPair = yield* configureCloudRelay(); const now = yield* DateTime.now; const request = makeCloudMintCredentialRequest({ @@ -3429,27 +3414,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.gen(function* () { yield* buildAppUnderTest(); - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); + const cloudKeyPair = yield* configureCloudRelay(); const now = yield* DateTime.now; const request = makeCloudEnvironmentHealthRequest({ @@ -3489,27 +3454,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.gen(function* () { yield* buildAppUnderTest(); - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); + const cloudKeyPair = yield* configureCloudRelay(); const now = yield* DateTime.now; const request = makeCloudEnvironmentHealthRequest({ diff --git a/apps/server/src/sourceControl/AzureDevOpsCli.ts b/apps/server/src/sourceControl/AzureDevOpsCli.ts index f05f4a7588c4..4696181acee3 100644 --- a/apps/server/src/sourceControl/AzureDevOpsCli.ts +++ b/apps/server/src/sourceControl/AzureDevOpsCli.ts @@ -203,8 +203,6 @@ export const AzureDevOpsCliError = Schema.Union([ ]); export type AzureDevOpsCliError = typeof AzureDevOpsCliError.Type; -export const isAzureDevOpsCliError = Schema.is(AzureDevOpsCliError); - export interface AzureDevOpsRepositoryCloneUrls { readonly nameWithOwner: string; readonly url: string; diff --git a/apps/server/src/sourceControl/GitLabCli.ts b/apps/server/src/sourceControl/GitLabCli.ts index ab8dfbb5f334..b222cec21a16 100644 --- a/apps/server/src/sourceControl/GitLabCli.ts +++ b/apps/server/src/sourceControl/GitLabCli.ts @@ -237,7 +237,6 @@ export const GitLabCliError = Schema.Union([ GitLabNamespaceDecodeError, ]); export type GitLabCliError = typeof GitLabCliError.Type; -export const isGitLabCliError = Schema.is(GitLabCliError); export interface GitLabMergeRequestSummary { readonly number: number; diff --git a/apps/server/src/sourceControl/azureDevOpsPullRequests.ts b/apps/server/src/sourceControl/azureDevOpsPullRequests.ts index 8ac682399e1d..9d3b7dfbe489 100644 --- a/apps/server/src/sourceControl/azureDevOpsPullRequests.ts +++ b/apps/server/src/sourceControl/azureDevOpsPullRequests.ts @@ -5,7 +5,7 @@ import * as Option from "effect/Option"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts"; -import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; +import { decodeJsonResult } from "@t3tools/shared/schemaJson"; export interface NormalizedAzureDevOpsPullRequestRecord { readonly number: number; @@ -181,8 +181,6 @@ const decodeAzureDevOpsPullRequestList = decodeJsonResult(Schema.Array(Schema.Un const decodeAzureDevOpsPullRequest = decodeJsonResult(AzureDevOpsPullRequestSchema); const decodeAzureDevOpsPullRequestEntry = Schema.decodeUnknownExit(AzureDevOpsPullRequestSchema); -export const formatAzureDevOpsJsonDecodeError = formatSchemaError; - export function decodeAzureDevOpsPullRequestListJson( raw: string, ): Result.Result< diff --git a/apps/server/src/sourceControl/gitHubPullRequests.ts b/apps/server/src/sourceControl/gitHubPullRequests.ts index 9e4f282e1c8a..e557646f1955 100644 --- a/apps/server/src/sourceControl/gitHubPullRequests.ts +++ b/apps/server/src/sourceControl/gitHubPullRequests.ts @@ -5,7 +5,7 @@ import * as Option from "effect/Option"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts"; -import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; +import { decodeJsonResult } from "@t3tools/shared/schemaJson"; export interface NormalizedGitHubPullRequestRecord { readonly number: number; @@ -109,8 +109,6 @@ const decodeGitHubPullRequestList = decodeJsonResult(Schema.Array(Schema.Unknown const decodeGitHubPullRequest = decodeJsonResult(GitHubPullRequestSchema); const decodeGitHubPullRequestEntry = Schema.decodeUnknownExit(GitHubPullRequestSchema); -export const formatGitHubJsonDecodeError = formatSchemaError; - export function decodeGitHubPullRequestListJson( raw: string, ): Result.Result< diff --git a/apps/server/src/sourceControl/gitLabMergeRequests.ts b/apps/server/src/sourceControl/gitLabMergeRequests.ts index 3b032e245bbc..f29d2bc1f271 100644 --- a/apps/server/src/sourceControl/gitLabMergeRequests.ts +++ b/apps/server/src/sourceControl/gitLabMergeRequests.ts @@ -5,7 +5,7 @@ import * as Option from "effect/Option"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts"; -import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; +import { decodeJsonResult } from "@t3tools/shared/schemaJson"; export interface NormalizedGitLabMergeRequestRecord { readonly number: number; @@ -123,8 +123,6 @@ const decodeGitLabMergeRequestList = decodeJsonResult(Schema.Array(Schema.Unknow const decodeGitLabMergeRequest = decodeJsonResult(GitLabMergeRequestSchema); const decodeGitLabMergeRequestEntry = Schema.decodeUnknownExit(GitLabMergeRequestSchema); -export const formatGitLabJsonDecodeError = formatSchemaError; - export function decodeGitLabMergeRequestListJson( raw: string, ): Result.Result< diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index b0dd3febebcb..cfba6ffd0f1f 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -6,6 +6,13 @@ import { scopeThreadRef, } from "@t3tools/client-runtime/environment"; import { + addProjectRemoteSourceLabel as remoteProjectSourceLabel, + addProjectRemoteSourcePathHint as remoteProjectSourcePathHint, + addProjectRemoteSourceProvider as remoteProjectSourceProvider, + type AddProjectRemoteProviderKind, + type AddProjectRemoteSource, + type AddProjectCloneFlow, + type AddProjectRemoteSourceReadiness, canCreateProjectInEnvironment, getCloneDestinationBrowsePath, getCloneDestinationPath, @@ -34,8 +41,6 @@ import { type FilesystemBrowseResult, type ProjectId, type SourceControlDiscoveryResult, - type SourceControlProviderKind, - type SourceControlRepositoryInfo, PRIMARY_LOCAL_ENVIRONMENT_ID, resolveEnvironmentMachineKind, } from "@t3tools/contracts"; @@ -214,27 +219,6 @@ interface AddProjectEnvironmentOption { readonly status: string; } -type AddProjectRemoteProviderKind = Extract< - SourceControlProviderKind, - "github" | "gitlab" | "bitbucket" | "azure-devops" ->; -type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; - -type AddProjectCloneFlow = - | { - readonly step: "repository"; - readonly environmentId: EnvironmentId; - readonly source: AddProjectRemoteSource; - } - | { - readonly step: "confirm"; - readonly environmentId: EnvironmentId; - readonly source: AddProjectRemoteSource; - readonly repositoryInput: string; - readonly repository: SourceControlRepositoryInfo | null; - readonly remoteUrl: string; - }; - const REMOTE_PROJECT_SOURCES: ReadonlyArray = [ "url", "github", @@ -249,42 +233,6 @@ const REMOTE_PROJECT_PROVIDER_SOURCES: ReadonlyArray { @@ -326,11 +270,6 @@ function sortAddProjectProviderSources( }); } -type AddProjectRemoteSourceReadiness = Record< - AddProjectRemoteSource, - { readonly ready: boolean; readonly hint: string | null } ->; - function buildAddProjectRemoteSourceReadiness( discovery: SourceControlDiscoveryResult | null, ): AddProjectRemoteSourceReadiness { @@ -356,7 +295,7 @@ function buildAddProjectRemoteSourceReadiness( const readiness = { ...defaultReadiness }; for (const source of REMOTE_PROJECT_SOURCES) { - const kind = sourceProviderKind(source); + const kind = remoteProjectSourceProvider(source); if (!kind) continue; const provider = providerByKind.get(kind); if (!provider) { diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 96f7af794ace..f3ef7ba95d1f 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -1,9 +1,8 @@ -import type { - GitRunStackedActionResult, - GitStackedAction, - VcsStatusResult, -} from "@t3tools/contracts"; -import { isTemporaryWorktreeBranch } from "@t3tools/shared/git"; +import type { GitStackedAction, VcsStatusResult } from "@t3tools/contracts"; +export { + resolveThreadBranchUpdate, + resolveLiveThreadBranchUpdate, +} from "@t3tools/client-runtime/state/vcs"; import { DEFAULT_CHANGE_REQUEST_TERMINOLOGY, getChangeRequestTerminology, @@ -361,18 +360,6 @@ export function resolveDefaultBranchActionDialogCopy(input: { }; } -export function resolveThreadBranchUpdate( - result: GitRunStackedActionResult, -): { branch: string } | null { - if (result.branch.status !== "created" || !result.branch.name) { - return null; - } - - return { - branch: result.branch.name, - }; -} - export function resolveThreadBranchMetadataPatch( branch: string | null, expectedBranch: string | null, @@ -383,35 +370,5 @@ export function resolveThreadBranchMetadataPatch( return { branch, expectedBranch }; } -export function resolveLiveThreadBranchUpdate(input: { - threadBranch: string | null; - gitStatus: VcsStatusResult | null; -}): { branch: string | null } | null { - if (!input.gitStatus) { - return null; - } - - if (input.gitStatus.refName === null && input.threadBranch !== null) { - return null; - } - - if (input.threadBranch === input.gitStatus.refName) { - return null; - } - - if ( - input.threadBranch !== null && - input.gitStatus.refName !== null && - !isTemporaryWorktreeBranch(input.threadBranch) && - isTemporaryWorktreeBranch(input.gitStatus.refName) - ) { - return null; - } - - return { - branch: input.gitStatus.refName, - }; -} - // Re-export from shared for backwards compatibility in this module's exports export { resolveAutoFeatureBranchName } from "@t3tools/shared/git"; diff --git a/apps/web/src/components/ProviderUpdateEnvironmentRows.test.tsx b/apps/web/src/components/ProviderUpdateEnvironmentRows.test.tsx index b34f82a775ac..575825029ee8 100644 --- a/apps/web/src/components/ProviderUpdateEnvironmentRows.test.tsx +++ b/apps/web/src/components/ProviderUpdateEnvironmentRows.test.tsx @@ -1,4 +1,5 @@ -import type { Dispatch, ReactElement, SetStateAction } from "react"; +import type { ReactElement } from "react"; +import { reactHookHarness as hooks } from "../test/reactHookHarness"; import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { type EnvironmentId, @@ -19,60 +20,9 @@ const testState = vi.hoisted(() => ({ updateProvider: vi.fn(), })); -const hooks = vi.hoisted(() => { - let cursor = 0; - let slots: unknown[] = []; - - const nextIndex = () => cursor++; - - return { - beginRender() { - cursor = 0; - }, - reset() { - cursor = 0; - slots = []; - }, - useCallback(callback: T): T { - nextIndex(); - return callback; - }, - useMemo(factory: () => T): T { - nextIndex(); - return factory(); - }, - useMemoCache(size: number): unknown[] { - const index = nextIndex(); - if (!slots[index]) { - slots[index] = Array.from({ length: size }, () => Symbol.for("react.memo_cache_sentinel")); - } - return slots[index] as unknown[]; - }, - useRef(initialValue: T): { current: T } { - const index = nextIndex(); - if (!slots[index]) { - slots[index] = { current: initialValue }; - } - return slots[index] as { current: T }; - }, - useState(initialValue: T | (() => T)): [T, Dispatch>] { - const index = nextIndex(); - if (index >= slots.length) { - slots[index] = - typeof initialValue === "function" ? (initialValue as () => T)() : initialValue; - } - const setValue: Dispatch> = (nextValue) => { - const previous = slots[index] as T; - slots[index] = - typeof nextValue === "function" ? (nextValue as (value: T) => T)(previous) : nextValue; - }; - return [slots[index] as T, setValue]; - }, - }; -}); - vi.mock("react", async (importOriginal) => { const actual = await importOriginal(); + const { reactHookHarness: hooks } = await import("../test/reactHookHarness"); return { ...actual, useCallback: hooks.useCallback, @@ -82,9 +32,10 @@ vi.mock("react", async (importOriginal) => { }; }); -vi.mock("react/compiler-runtime", () => ({ - c: hooks.useMemoCache, -})); +vi.mock("react/compiler-runtime", async () => { + const { reactHookHarness } = await import("../test/reactHookHarness"); + return { c: reactHookHarness.useMemoCache }; +}); vi.mock("~/state/server", () => ({ serverEnvironment: { updateProvider: Symbol("updateProvider") }, diff --git a/apps/web/src/components/color-selector.tsx b/apps/web/src/components/color-selector.tsx deleted file mode 100644 index d6898b009eeb..000000000000 --- a/apps/web/src/components/color-selector.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { cn } from "~/lib/utils"; - -interface ColorSelectorProps { - colors: string[]; - size?: "default" | "sm" | "lg"; - defaultValue: string; - name?: string; - onColorSelect?: (color: string) => void; - className?: string; -} - -const colorMap = { - default: "var(--contrast-foreground)", - red: "var(--color-red-500)", - green: "var(--color-green-500)", - blue: "var(--color-blue-500)", - yellow: "var(--color-yellow-500)", - purple: "var(--color-purple-500)", - pink: "var(--color-pink-500)", - indigo: "var(--color-indigo-500)", - orange: "var(--color-orange-500)", - teal: "var(--color-teal-500)", - cyan: "var(--color-cyan-500)", - lime: "var(--color-lime-500)", - emerald: "var(--color-emerald-500)", - violet: "var(--color-violet-500)", - fuchsia: "var(--color-fuchsia-500)", - rose: "var(--color-rose-500)", - sky: "var(--color-sky-500)", - amber: "var(--color-amber-500)", -} as const; - -function getSizeClass(size: "default" | "sm" | "lg") { - switch (size) { - case "sm": - return "size-4"; - case "default": - return "size-5"; - case "lg": - return "size-6"; - default: - return "size-5"; - } -} - -function getColorValue(color: string): string { - return colorMap[color as keyof typeof colorMap] || color; -} - -export function ColorSelector({ - colors, - size = "default", - defaultValue, - name, - onColorSelect, - className, -}: ColorSelectorProps) { - const [selectedColor, setSelectedColor] = useState(defaultValue); - - const handleColorSelect = (color: string) => { - setSelectedColor(color); - onColorSelect?.(color); - }; - - const sizeClass = getSizeClass(size); - - return ( -
- {name && } - {colors.map((color) => { - const colorValue = getColorValue(color); - return ( -
handleColorSelect(color)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleColorSelect(color); - } - }} - tabIndex={0} - role="button" - aria-label={`Select ${color} color`} - aria-pressed={selectedColor === color} - /> - ); - })} -
- ); -} diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index 5228aa38e769..1a0ce7be2826 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -1,3 +1,17 @@ +import { + isNoContentRuntimeWarning, + isPlanBoundaryToolActivity, + mergeChangedFiles, + asRecord, + asTrimmedString, + extractToolCommand, + extractToolTitle, + stripTrailingExitCode, + extractWorkLogItemType, + extractChangedFiles, + compareActivityLifecycleRank, + parseUserInputQuestions, +} from "@t3tools/client-runtime/work-log/activity-parsing"; import * as Option from "effect/Option"; import * as Arr from "effect/Array"; import * as Schema from "effect/Schema"; @@ -462,60 +476,6 @@ export function derivePendingApprovals( ); } -function parseUserInputQuestions( - payload: Record | null, -): ReadonlyArray | null { - const questions = payload?.questions; - if (!Array.isArray(questions)) { - return null; - } - const parsed = questions - .map((entry) => { - if (!entry || typeof entry !== "object") return null; - const question = entry as Record; - if ( - typeof question.id !== "string" || - typeof question.header !== "string" || - typeof question.question !== "string" || - !Array.isArray(question.options) - ) { - return null; - } - const options = question.options - .map((option) => { - if (!option || typeof option !== "object") return null; - const optionRecord = option as Record; - if ( - typeof optionRecord.label !== "string" || - typeof optionRecord.description !== "string" - ) { - return null; - } - return { - label: optionRecord.label, - description: optionRecord.description, - ...(typeof optionRecord.value === "string" ? { value: optionRecord.value } : {}), - }; - }) - .filter((option): option is UserInputQuestion["options"][number] => option !== null); - if (options.length === 0 && question.allowCustomAnswer === false) { - return null; - } - return { - id: question.id, - header: question.header, - question: question.question, - options, - multiSelect: question.multiSelect === true, - ...(typeof question.allowCustomAnswer === "boolean" - ? { allowCustomAnswer: question.allowCustomAnswer } - : {}), - }; - }) - .filter((question): question is UserInputQuestion => question !== null); - return parsed.length > 0 ? parsed : null; -} - export function derivePendingUserInputs( activities: ReadonlyArray, ): PendingUserInput[] { @@ -815,29 +775,6 @@ export function deriveWorkLogEntries( return collapseDerivedWorkLogEntries(entries); } -/** Adapters forward unknown wire-only SDK messages (background_tasks_changed, - * commands_changed, ...) as runtime warnings. The suffix comes from - * describeUnknownSdkMessage in the Claude adapter; a row with no displayable - * text carries nothing a user can act on, so it does not render. */ -function isNoContentRuntimeWarning(activity: OrchestrationThreadActivity): boolean { - return ( - activity.kind === "runtime.warning" && - activity.summary.endsWith("(no displayable text content)") - ); -} - -function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { - if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { - return false; - } - - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - return typeof payload?.detail === "string" && payload.detail.startsWith("ExitPlanMode:"); -} - function extractWorkLogToolLifecycleStatus( payload: Record | null, ): WorkLogToolLifecycleStatus | undefined { @@ -1198,17 +1135,6 @@ function mergeDerivedWorkLogEntries( }; } -function mergeChangedFiles( - previous: ReadonlyArray | undefined, - next: ReadonlyArray | undefined, -): string[] { - const merged = [...(previous ?? []), ...(next ?? [])]; - if (merged.length === 0) { - return []; - } - return [...new Set(merged)]; -} - function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | undefined { // Subagent lifecycle rows collapse by agent identity: one row per agent, // progress ticks fold into it, the terminal row wins the label. @@ -1252,214 +1178,10 @@ function toLatestProposedPlanState(proposedPlan: ProposedPlan): LatestProposedPl }; } -function asRecord(value: unknown): Record | null { - return value && typeof value === "object" ? (value as Record) : null; -} - -function asTrimmedString(value: unknown): string | null { - if (typeof value !== "string") { - return null; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - function asNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } -function trimMatchingOuterQuotes(value: string): string { - const trimmed = value.trim(); - if ( - (trimmed.startsWith("'") && trimmed.endsWith("'")) || - (trimmed.startsWith('"') && trimmed.endsWith('"')) - ) { - const unquoted = trimmed.slice(1, -1).trim(); - return unquoted.length > 0 ? unquoted : trimmed; - } - return trimmed; -} - -function executableBasename(value: string): string | null { - const trimmed = trimMatchingOuterQuotes(value); - if (trimmed.length === 0) { - return null; - } - const normalized = trimmed.replace(/\\/g, "/"); - const segments = normalized.split("/"); - const last = segments.at(-1)?.trim() ?? ""; - return last.length > 0 ? last.toLowerCase() : null; -} - -function splitExecutableAndRest(value: string): { executable: string; rest: string } | null { - const trimmed = value.trim(); - if (trimmed.length === 0) { - return null; - } - - if (trimmed.startsWith('"') || trimmed.startsWith("'")) { - const quote = trimmed.charAt(0); - const closeIndex = trimmed.indexOf(quote, 1); - if (closeIndex <= 0) { - return null; - } - return { - executable: trimmed.slice(0, closeIndex + 1), - rest: trimmed.slice(closeIndex + 1).trim(), - }; - } - - const firstWhitespace = trimmed.search(/\s/); - if (firstWhitespace < 0) { - return { - executable: trimmed, - rest: "", - }; - } - - return { - executable: trimmed.slice(0, firstWhitespace), - rest: trimmed.slice(firstWhitespace).trim(), - }; -} - -const SHELL_WRAPPER_SPECS = [ - { - executables: ["pwsh", "pwsh.exe", "powershell", "powershell.exe"], - wrapperFlagPattern: /(?:^|\s)-command\s+/i, - }, - { - executables: ["cmd", "cmd.exe"], - wrapperFlagPattern: /(?:^|\s)\/c\s+/i, - }, - { - executables: ["bash", "sh", "zsh"], - wrapperFlagPattern: /(?:^|\s)-(?:l)?c\s+/i, - }, -] as const; - -function findShellWrapperSpec(shell: string) { - return SHELL_WRAPPER_SPECS.find((spec) => - (spec.executables as ReadonlyArray).includes(shell), - ); -} - -function unwrapCommandRemainder(value: string, wrapperFlagPattern: RegExp): string | null { - const match = wrapperFlagPattern.exec(value); - if (!match) { - return null; - } - - const command = value.slice(match.index + match[0].length).trim(); - if (command.length === 0) { - return null; - } - - const openingQuote = command[0]; - if ((openingQuote === "'" || openingQuote === '"') && !command.endsWith(openingQuote)) { - return null; - } - - const unwrapped = trimMatchingOuterQuotes(command); - return unwrapped.length > 0 ? unwrapped : null; -} - -function unwrapKnownShellCommandWrapper(value: string): string { - const split = splitExecutableAndRest(value); - if (!split || split.rest.length === 0) { - return value; - } - - const shell = executableBasename(split.executable); - if (!shell) { - return value; - } - - const spec = findShellWrapperSpec(shell); - if (!spec) { - return value; - } - - return unwrapCommandRemainder(split.rest, spec.wrapperFlagPattern) ?? value; -} - -function formatCommandArrayPart(value: string): string { - return /[\s"'`]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; -} - -function formatCommandValue(value: unknown): string | null { - const direct = asTrimmedString(value); - if (direct) { - return direct; - } - if (!Array.isArray(value)) { - return null; - } - const parts: Array = []; - for (const entry of value) { - const part = asTrimmedString(entry); - if (part !== null) { - parts.push(part); - } - } - if (parts.length === 0) { - return null; - } - return parts.map((part) => formatCommandArrayPart(part)).join(" "); -} - -function normalizeCommandValue(value: unknown): string | null { - const formatted = formatCommandValue(value); - return formatted ? unwrapKnownShellCommandWrapper(formatted) : null; -} - -function toRawToolCommand(value: unknown, normalizedCommand: string | null): string | null { - const formatted = formatCommandValue(value); - if (!formatted || normalizedCommand === null) { - return null; - } - return formatted === normalizedCommand ? null : formatted; -} - -function extractToolCommand(payload: Record | null): { - command: string | null; - rawCommand: string | null; -} { - const data = asRecord(payload?.data); - const item = asRecord(data?.item); - const itemResult = asRecord(item?.result); - const itemInput = asRecord(item?.input); - const itemType = asTrimmedString(payload?.itemType); - const detail = asTrimmedString(payload?.detail); - const candidates: unknown[] = [ - item?.command, - itemInput?.command, - itemResult?.command, - data?.command, - itemType === "command_execution" && detail ? stripTrailingExitCode(detail).output : null, - ]; - - for (const candidate of candidates) { - const command = normalizeCommandValue(candidate); - if (!command) { - continue; - } - return { - command, - rawCommand: toRawToolCommand(candidate, command), - }; - } - - return { - command: null, - rawCommand: null, - }; -} - -function extractToolTitle(payload: Record | null): string | null { - return asTrimmedString(payload?.title); -} - function extractToolCallId(payload: Record | null): string | null { const data = asRecord(payload?.data); return asTrimmedString(payload?.toolCallId) ?? asTrimmedString(data?.toolCallId); @@ -1594,36 +1316,6 @@ function extractToolDetail( return null; } -function stripTrailingExitCode(value: string): { - output: string | null; - exitCode?: number | undefined; -} { - const trimmed = value.trim(); - const match = /^(?[\s\S]*?)(?:\s*\d+)>)\s*$/i.exec( - trimmed, - ); - if (!match?.groups) { - return { - output: trimmed.length > 0 ? trimmed : null, - }; - } - const exitCode = Number.parseInt(match.groups.code ?? "", 10); - const normalizedOutput = match.groups.output?.trim() ?? ""; - return { - output: normalizedOutput.length > 0 ? normalizedOutput : null, - ...(Number.isInteger(exitCode) ? { exitCode } : {}), - }; -} - -function extractWorkLogItemType( - payload: Record | null, -): WorkLogEntry["itemType"] | undefined { - if (typeof payload?.itemType === "string" && isToolLifecycleItemType(payload.itemType)) { - return payload.itemType; - } - return undefined; -} - function extractWorkLogRequestKind( payload: Record | null, ): WorkLogEntry["requestKind"] | undefined { @@ -1637,70 +1329,6 @@ function extractWorkLogRequestKind( return requestKindFromRequestType(payload?.requestType) ?? undefined; } -function pushChangedFile(target: string[], seen: Set, value: unknown) { - const normalized = asTrimmedString(value); - if (!normalized || seen.has(normalized)) { - return; - } - seen.add(normalized); - target.push(normalized); -} - -function collectChangedFiles(value: unknown, target: string[], seen: Set, depth: number) { - if (depth > 4 || target.length >= 12) { - return; - } - if (Array.isArray(value)) { - for (const entry of value) { - collectChangedFiles(entry, target, seen, depth + 1); - if (target.length >= 12) { - return; - } - } - return; - } - - const record = asRecord(value); - if (!record) { - return; - } - - pushChangedFile(target, seen, record.path); - pushChangedFile(target, seen, record.filePath); - pushChangedFile(target, seen, record.relativePath); - pushChangedFile(target, seen, record.filename); - pushChangedFile(target, seen, record.newPath); - pushChangedFile(target, seen, record.oldPath); - - for (const nestedKey of [ - "item", - "result", - "input", - "data", - "changes", - "files", - "edits", - "patch", - "patches", - "operations", - ]) { - if (!(nestedKey in record)) { - continue; - } - collectChangedFiles(record[nestedKey], target, seen, depth + 1); - if (target.length >= 12) { - return; - } - } -} - -function extractChangedFiles(payload: Record | null): string[] { - const changedFiles: string[] = []; - const seen = new Set(); - collectChangedFiles(asRecord(payload?.data), changedFiles, seen, 0); - return changedFiles; -} - function compareActivitiesByOrder( left: OrchestrationThreadActivity, right: OrchestrationThreadActivity, @@ -1729,19 +1357,6 @@ function compareActivitiesByOrder( return left.id.localeCompare(right.id); } -function compareActivityLifecycleRank(kind: string): number { - if (kind.endsWith(".started") || kind === "tool.started") { - return 0; - } - if (kind.endsWith(".progress") || kind.endsWith(".updated")) { - return 1; - } - if (kind.endsWith(".completed") || kind.endsWith(".resolved")) { - return 2; - } - return 1; -} - function timelineEntryFromMessage(message: ChatMessage): TimelineEntry { return { id: message.id, diff --git a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts index e34dcc55e37e..961839d115d7 100644 --- a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts +++ b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts @@ -146,6 +146,24 @@ const target: LiveActivities.TargetRow = { last_live_activity_delivery_at: null, }; +type JobInput = Parameters[0]; + +function makeSignedJob( + input: Omit & + Partial>, +) { + return signApnsDeliveryJob({ + secret: config.apnsDeliveryJobSigningSecret, + payload: makeApnsDeliveryJobPayload({ + userId: target.user_id, + deviceId: target.device_id, + createdAt: "1970-01-01T00:00:00.000Z", + expiresAt: "1970-01-01T00:10:00.000Z", + ...input, + }), + }); +} + function makeLayer(input: { readonly attempts: Array; readonly sourceJobClaims?: ReadonlyMap; @@ -450,22 +468,14 @@ describe("ApnsDeliveries", () => { it.effect("sends signed jobs to the device's APNs environment and bundle topic", () => { const attempts: Array = []; const requests: Array = []; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_update", - userId: target.user_id, - deviceId: target.device_id, token: "activity-token", bundleId: "com.t3tools.t3code.preview", apsEnvironment: "sandbox", aggregate, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-routing-1", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.sync(() => { requests.push(request); @@ -876,20 +886,12 @@ describe("ApnsDeliveries", () => { const clearedStarts: Array< Parameters[0] > = []; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_start", - userId: target.user_id, - deviceId: target.device_id, token: target.push_to_start_token ?? "start-token", aggregate, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-start-1", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); return Effect.gen(function* () { const deliveries = yield* ApnsDeliveries.ApnsDeliveries; @@ -926,20 +928,12 @@ describe("ApnsDeliveries", () => { transportErrors.push(error); } }); - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_update", - userId: target.user_id, - deviceId: target.device_id, token: target.activity_push_token ?? "activity-token", aggregate, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-1", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); return Effect.gen(function* () { const deliveries = yield* ApnsDeliveries.ApnsDeliveries; @@ -1054,10 +1048,8 @@ describe("ApnsDeliveries", () => { it.effect("processes signed push notification jobs through APNs and records attempts", () => { const attempts: Array = []; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "push_notification", - userId: target.user_id, - deviceId: target.device_id, token: "apns-device-token", aggregate: null, notification: { @@ -1067,14 +1059,8 @@ describe("ApnsDeliveries", () => { threadId: "thread", deepLink: "/", }, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-push-1", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.succeed(HttpClientResponse.fromWeb(request, new Response("", { status: 200 }))); @@ -1116,10 +1102,8 @@ describe("ApnsDeliveries", () => { it.effect("skips duplicate signed queue jobs before calling APNs", () => { const attempts: Array = []; let executeCount = 0; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "push_notification", - userId: target.user_id, - deviceId: target.device_id, token: "apns-device-token", aggregate: null, notification: { @@ -1129,14 +1113,8 @@ describe("ApnsDeliveries", () => { threadId: "thread", deepLink: "/", }, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-push-duplicate", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.sync(() => { executeCount += 1; @@ -1170,20 +1148,12 @@ describe("ApnsDeliveries", () => { it.effect("skips stale signed Live Activity jobs when the registered token changed", () => { const attempts: Array = []; let executeCount = 0; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_update", - userId: target.user_id, - deviceId: target.device_id, token: "stale-activity-token", aggregate, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-update-stale-token", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.sync(() => { executeCount += 1; @@ -1223,10 +1193,8 @@ describe("ApnsDeliveries", () => { it.effect("skips stale signed push notification jobs when the device token changed", () => { const attempts: Array = []; let executeCount = 0; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "push_notification", - userId: target.user_id, - deviceId: target.device_id, token: "stale-device-token", aggregate: null, notification: { @@ -1236,14 +1204,8 @@ describe("ApnsDeliveries", () => { threadId: "thread", deepLink: "/", }, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-push-stale-token", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.sync(() => { executeCount += 1; @@ -1303,21 +1265,14 @@ describe("ApnsDeliveries", () => { }, ], }; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_update", - userId: target.user_id, - deviceId: target.device_id, token: target.activity_push_token ?? "activity-token", aggregate: completedAggregate, alert: { title: "Thread", body: "Done: Project" }, createdAt: completedAt, - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-update-superseded-by-running", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.sync(() => { executeCount += 1; @@ -1364,10 +1319,8 @@ describe("ApnsDeliveries", () => { const attempts: Array = []; let executeCount = 0; const completedAt = "1970-01-01T00:00:01.000Z"; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "push_notification", - userId: target.user_id, - deviceId: target.device_id, token: "apns-device-token", aggregate: null, notification: { @@ -1380,13 +1333,8 @@ describe("ApnsDeliveries", () => { updatedAt: completedAt, }, createdAt: completedAt, - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-push-superseded-by-running", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.sync(() => { executeCount += 1; @@ -1433,10 +1381,8 @@ describe("ApnsDeliveries", () => { it.effect("retries signed queue jobs that are already claimed but not completed", () => { const attempts: Array = []; let executeCount = 0; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "push_notification", - userId: target.user_id, - deviceId: target.device_id, token: "apns-device-token", aggregate: null, notification: { @@ -1446,14 +1392,8 @@ describe("ApnsDeliveries", () => { threadId: "thread", deepLink: "/", }, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-push-in-flight", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.sync(() => { executeCount += 1; @@ -1487,10 +1427,8 @@ describe("ApnsDeliveries", () => { const invalidatedTokens: Array< Parameters[0] > = []; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "push_notification", - userId: target.user_id, - deviceId: target.device_id, token: "apns-device-token", aggregate: null, notification: { @@ -1500,14 +1438,8 @@ describe("ApnsDeliveries", () => { threadId: "thread", deepLink: "/", }, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-push-bad-token", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.succeed( HttpClientResponse.fromWeb( @@ -1554,20 +1486,12 @@ describe("ApnsDeliveries", () => { const clearedStarts: Array< Parameters[0] > = []; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_start", - userId: target.user_id, - deviceId: target.device_id, token: target.push_to_start_token ?? "start-token", aggregate, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-start-1", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); return Effect.gen(function* () { const deliveries = yield* ApnsDeliveries.ApnsDeliveries; @@ -1597,20 +1521,12 @@ describe("ApnsDeliveries", () => { const invalidatedTokens: Array< Parameters[0] > = []; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_start", - userId: target.user_id, - deviceId: target.device_id, token: target.push_to_start_token ?? "start-token", aggregate, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-start-bad-token", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.succeed( HttpClientResponse.fromWeb( @@ -1652,20 +1568,12 @@ describe("ApnsDeliveries", () => { const invalidatedTokens: Array< Parameters[0] > = []; - const payload = makeApnsDeliveryJobPayload({ + const signed = makeSignedJob({ kind: "live_activity_update", - userId: target.user_id, - deviceId: target.device_id, token: target.activity_push_token ?? "activity-token", aggregate, - createdAt: "1970-01-01T00:00:00.000Z", - expiresAt: "1970-01-01T00:10:00.000Z", jobId: "job-update-unregistered", }); - const signed = signApnsDeliveryJob({ - secret: config.apnsDeliveryJobSigningSecret, - payload, - }); const execute = (request: HttpClientRequest.HttpClientRequest) => Effect.succeed( HttpClientResponse.fromWeb( diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index 68f5bad51474..6382647162f1 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -210,6 +210,10 @@ "./work-log/scroll-anchor": { "types": "./src/work-log/scrollAnchor.ts", "default": "./src/work-log/scrollAnchor.ts" + }, + "./work-log/activity-parsing": { + "types": "./src/work-log/activity-parsing.ts", + "default": "./src/work-log/activity-parsing.ts" } }, "scripts": { diff --git a/packages/client-runtime/src/relay/managedRelay.test.ts b/packages/client-runtime/src/relay/managedRelay.test.ts index 2699c4ad0803..1e83870e2f6e 100644 --- a/packages/client-runtime/src/relay/managedRelay.test.ts +++ b/packages/client-runtime/src/relay/managedRelay.test.ts @@ -46,6 +46,32 @@ function clerkToken(subject: string, nonce: string): string { return `${encode({ alg: "none" })}.${encode({ sub: subject, nonce })}.signature`; } +const tokenExchangeResponse = { + access_token: "relay-token", + issued_token_type: "urn:ietf:params:oauth:token-type:access_token", + token_type: "DPoP", + expires_in: 1_800, + scope: RelayEnvironmentStatusScope, +}; + +const onlineEnvironmentResponse = { + environmentId: "env-1", + endpoint: { + httpBaseUrl: "https://desktop.example.test/", + wsBaseUrl: "wss://desktop.example.test/ws", + providerKind: "cloudflare_tunnel", + }, + status: "online", + checkedAt: "2026-06-05T20:00:00.000Z", + descriptor: { + environmentId: "env-1", + label: "Desktop", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "0.0.0-test", + capabilities: { repositoryIdentity: true }, + }, +}; + describe("ManagedRelayClient", () => { it.effect("owns tracing at service and implementation boundaries", () => { const spanNames: Array = []; @@ -59,35 +85,9 @@ describe("ManagedRelayClient", () => { const fetchFn = ((input) => { const url = String(input); if (url.endsWith("/v1/client/dpop-token")) { - return Promise.resolve( - Response.json({ - access_token: "relay-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 1_800, - scope: RelayEnvironmentStatusScope, - }), - ); + return Promise.resolve(Response.json(tokenExchangeResponse)); } - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - status: "online", - checkedAt: "2026-06-05T20:00:00.000Z", - descriptor: { - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }, - }), - ); + return Promise.resolve(Response.json(onlineEnvironmentResponse)); }) satisfies typeof globalThis.fetch; return Effect.gen(function* () { @@ -156,23 +156,7 @@ describe("ManagedRelayClient", () => { ); } return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - status: "online", - checkedAt: "2026-05-25T00:01:00.000Z", - descriptor: { - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }, - }), + Response.json({ ...onlineEnvironmentResponse, checkedAt: "2026-05-25T00:01:00.000Z" }), ); }) satisfies typeof globalThis.fetch; @@ -312,34 +296,10 @@ describe("ManagedRelayClient", () => { if (url.endsWith("/v1/client/dpop-token")) { tokenExchangeCount += 1; return Promise.resolve( - Response.json({ - access_token: "persisted-relay-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 1_800, - scope: RelayEnvironmentStatusScope, - }), + Response.json({ ...tokenExchangeResponse, access_token: "persisted-relay-token" }), ); } - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - status: "online", - checkedAt: "2026-06-05T20:00:00.000Z", - descriptor: { - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }, - }), - ); + return Promise.resolve(Response.json(onlineEnvironmentResponse)); }) satisfies typeof globalThis.fetch; const statusInput = (token: string) => ({ @@ -395,13 +355,7 @@ describe("ManagedRelayClient", () => { if (url.endsWith("/v1/client/dpop-token")) { tokenExchangeCount += 1; return Promise.resolve( - Response.json({ - access_token: "fresh-relay-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 1_800, - scope: RelayEnvironmentStatusScope, - }), + Response.json({ ...tokenExchangeResponse, access_token: "fresh-relay-token" }), ); } @@ -420,25 +374,7 @@ describe("ManagedRelayClient", () => { ), ); } - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - status: "online", - checkedAt: "2026-06-05T20:00:00.000Z", - descriptor: { - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }, - }), - ); + return Promise.resolve(Response.json(onlineEnvironmentResponse)); }) satisfies typeof globalThis.fetch; return Effect.gen(function* () { @@ -473,35 +409,9 @@ describe("ManagedRelayClient", () => { const fetchFn = ((input) => { const url = String(input); if (url.endsWith("/v1/client/dpop-token")) { - return Promise.resolve( - Response.json({ - access_token: "relay-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 1_800, - scope: RelayEnvironmentStatusScope, - }), - ); + return Promise.resolve(Response.json(tokenExchangeResponse)); } - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - status: "online", - checkedAt: "2026-06-05T20:00:00.000Z", - descriptor: { - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }, - }), - ); + return Promise.resolve(Response.json(onlineEnvironmentResponse)); }) satisfies typeof globalThis.fetch; return Effect.gen(function* () { diff --git a/packages/client-runtime/src/state/runtime.ts b/packages/client-runtime/src/state/runtime.ts index 56489a4ba668..23626d326928 100644 --- a/packages/client-runtime/src/state/runtime.ts +++ b/packages/client-runtime/src/state/runtime.ts @@ -390,31 +390,6 @@ export function createRuntimeCommand( }; } -export function createRuntimeStreamCommand( - runtime: Atom.AtomRuntime, - options: { - readonly label: string; - readonly execute: (input: W, registry: AtomRegistry.AtomRegistry) => Stream.Stream; - readonly scheduler?: AtomCommandScheduler; - readonly concurrency?: AtomCommandConcurrency; - }, -): AtomCommand { - const scheduler = options.scheduler ?? createAtomCommandScheduler(); - const concurrency = options.concurrency ?? { mode: "parallel" as const }; - return { - label: options.label, - run: (registry, input) => - settleAtomCommandResult(() => - scheduler.schedule(registry, concurrency, input, () => { - const atom = runtime - .atom(options.execute(input, registry)) - .pipe(Atom.withLabel(options.label)); - return executeAtomQuery(registry, atom, { reportDefect: false, reportFailure: false }); - }), - ), - }; -} - export function reportAtomCommandResult( result: AtomCommandResult, options: AtomCommandOptions = {}, diff --git a/packages/client-runtime/src/state/threadCommands.ts b/packages/client-runtime/src/state/threadCommands.ts index c540644289df..980fff1fb7dd 100644 --- a/packages/client-runtime/src/state/threadCommands.ts +++ b/packages/client-runtime/src/state/threadCommands.ts @@ -8,26 +8,6 @@ import { createEnvironmentRpcCommand, } from "./runtime.ts"; import { - type ArchiveThreadInput, - type CreateThreadInput, - type DeleteThreadInput, - type InterruptThreadTurnInput, - type RespondToThreadApprovalInput, - type RespondToThreadUserInputInput, - type RevertThreadCheckpointInput, - type SetThreadInteractionModeInput, - type SetThreadRuntimeModeInput, - type PinThreadInput, - type ReorderPinnedThreadInput, - type SettleThreadInput, - type SnoozeThreadInput, - type StartThreadTurnInput, - type StopThreadSessionInput, - type UnarchiveThreadInput, - type UnpinThreadInput, - type UnsettleThreadInput, - type UnsnoozeThreadInput, - type UpdateThreadMetadataInput, archiveThread, createThread, deleteThread, @@ -83,127 +63,37 @@ export function createThreadEnvironmentAtoms( key: ({ environmentId, input }: { environmentId: string; input: { threadId: string } }) => JSON.stringify([environmentId, input.threadId]), }; - return { - create: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:create", - execute: (input: CreateThreadInput) => createThread(input), - scheduler, - concurrency, - }), - delete: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:delete", - execute: (input: DeleteThreadInput) => deleteThread(input), - scheduler, - concurrency, - }), - archive: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:archive", - execute: (input: ArchiveThreadInput) => archiveThread(input), - scheduler, - concurrency, - }), - unarchive: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:unarchive", - execute: (input: UnarchiveThreadInput) => unarchiveThread(input), - scheduler, - concurrency, - }), - settle: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:settle", - execute: (input: SettleThreadInput) => settleThread(input), - scheduler, - concurrency, - }), - unsettle: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:unsettle", - execute: (input: UnsettleThreadInput) => unsettleThread(input), - scheduler, - concurrency, - }), - snooze: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:snooze", - execute: (input: SnoozeThreadInput) => snoozeThread(input), - scheduler, - concurrency, - }), - unsnooze: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:unsnooze", - execute: (input: UnsnoozeThreadInput) => unsnoozeThread(input), - scheduler, - concurrency, - }), - pin: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:pin", - execute: (input: PinThreadInput) => pinThread(input), - scheduler, - concurrency, - }), - unpin: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:unpin", - execute: (input: UnpinThreadInput) => unpinThread(input), - scheduler, - concurrency, - }), - reorderPin: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:reorder-pin", - execute: (input: ReorderPinnedThreadInput) => reorderPinnedThread(input), - scheduler, - concurrency, - }), - updateMetadata: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:update-metadata", - execute: (input: UpdateThreadMetadataInput) => updateThreadMetadata(input), - scheduler, - concurrency, - }), - setRuntimeMode: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:set-runtime-mode", - execute: (input: SetThreadRuntimeModeInput) => setThreadRuntimeMode(input), - scheduler, - concurrency, - }), - setInteractionMode: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:set-interaction-mode", - execute: (input: SetThreadInteractionModeInput) => setThreadInteractionMode(input), - scheduler, - concurrency, - }), - startTurn: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:start-turn", - execute: (input: StartThreadTurnInput) => startThreadTurn(input), - scheduler, - concurrency, - }), - interruptTurn: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:interrupt-turn", - execute: (input: InterruptThreadTurnInput) => interruptThreadTurn(input), - scheduler, - concurrency, - }), - respondToApproval: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:respond-to-approval", - execute: (input: RespondToThreadApprovalInput) => respondToThreadApproval(input), + const command = ( + label: string, + execute: (input: Input) => ReturnType, + ) => + createEnvironmentCommand(runtime, { + label: `environment-data:commands:thread:${label}`, + execute, scheduler, concurrency, - }), - respondToUserInput: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:respond-to-user-input", - execute: (input: RespondToThreadUserInputInput) => respondToThreadUserInput(input), - scheduler, - concurrency, - }), - revertCheckpoint: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:revert-checkpoint", - execute: (input: RevertThreadCheckpointInput) => revertThreadCheckpoint(input), - scheduler, - concurrency, - }), - stopSession: createEnvironmentCommand(runtime, { - label: "environment-data:commands:thread:stop-session", - execute: (input: StopThreadSessionInput) => stopThreadSession(input), - scheduler, - concurrency, - }), + }); + return { + create: command("create", createThread), + delete: command("delete", deleteThread), + archive: command("archive", archiveThread), + unarchive: command("unarchive", unarchiveThread), + settle: command("settle", settleThread), + unsettle: command("unsettle", unsettleThread), + snooze: command("snooze", snoozeThread), + unsnooze: command("unsnooze", unsnoozeThread), + pin: command("pin", pinThread), + unpin: command("unpin", unpinThread), + reorderPin: command("reorder-pin", reorderPinnedThread), + updateMetadata: command("update-metadata", updateThreadMetadata), + setRuntimeMode: command("set-runtime-mode", setThreadRuntimeMode), + setInteractionMode: command("set-interaction-mode", setThreadInteractionMode), + startTurn: command("start-turn", startThreadTurn), + interruptTurn: command("interrupt-turn", interruptThreadTurn), + respondToApproval: command("respond-to-approval", respondToThreadApproval), + respondToUserInput: command("respond-to-user-input", respondToThreadUserInput), + revertCheckpoint: command("revert-checkpoint", revertThreadCheckpoint), + stopSession: command("stop-session", stopThreadSession), uploadFeedback: createEnvironmentRpcCommand(runtime, { label: "environment-data:commands:thread:upload-feedback", tag: WS_METHODS.providerUploadFeedback, diff --git a/packages/client-runtime/src/state/threadReducer.test.ts b/packages/client-runtime/src/state/threadReducer.test.ts index 401980997663..635b449fcb3a 100644 --- a/packages/client-runtime/src/state/threadReducer.test.ts +++ b/packages/client-runtime/src/state/threadReducer.test.ts @@ -14,12 +14,32 @@ import type { OrchestrationThread } from "@t3tools/contracts"; import { applyThreadDetailEvent } from "./threadReducer.ts"; +function updatedThread(result: ReturnType) { + if (result.kind !== "updated") { + throw new Error(`Expected an updated thread, received ${result.kind}`); + } + return result.thread; +} + +const makeActivity = (id: string, sequence: number | null, summary: string) => ({ + id: EventId.make(id), + tone: "tool" as const, + kind: "command", + summary, + payload: {}, + turnId: TurnId.make("turn-1"), + ...(sequence === null ? {} : { sequence }), + createdAt: "2026-04-01T11:00:00.000Z", +}); + const baseEventFields = { eventId: EventId.make("event-1"), commandId: null, causationEventId: null, correlationId: null, metadata: {}, + aggregateKind: "thread", + aggregateId: ThreadId.make("thread-1"), } as const; const baseThread: OrchestrationThread = { @@ -94,14 +114,12 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.id).toBe("thread-2"); - expect(result.thread.title).toBe("New Thread"); - expect(result.thread.branch).toBe("main"); - expect(result.thread.messages).toEqual([]); - expect(result.thread.session).toBeNull(); - } + const thread = updatedThread(result); + expect(thread.id).toBe("thread-2"); + expect(thread.title).toBe("New Thread"); + expect(thread.branch).toBe("main"); + expect(thread.messages).toEqual([]); + expect(thread.session).toBeNull(); }); }); @@ -111,8 +129,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 2, occurredAt: "2026-04-01T02:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.deleted", payload: { threadId: ThreadId.make("thread-1"), @@ -136,8 +152,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 3, occurredAt: "2026-04-01T03:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.archived", payload: { threadId: ThreadId.make("thread-1"), @@ -146,11 +160,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.archivedAt).toBe("2026-04-01T03:00:00.000Z"); - expect(result.thread.titleRegeneration).toBeNull(); - } + const thread = updatedThread(result); + expect(thread.archivedAt).toBe("2026-04-01T03:00:00.000Z"); + expect(thread.titleRegeneration).toBeNull(); }); it("clears archivedAt", () => { @@ -159,8 +171,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 4, occurredAt: "2026-04-01T04:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.unarchived", payload: { threadId: ThreadId.make("thread-1"), @@ -168,10 +178,8 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.archivedAt).toBeNull(); - } + const thread = updatedThread(result); + expect(thread.archivedAt).toBeNull(); }); }); @@ -182,8 +190,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 5, occurredAt: settledAt, - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.settled", payload: { threadId: ThreadId.make("thread-1"), @@ -192,11 +198,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.settledOverride).toBe("settled"); - expect(result.thread.settledAt).toBe(settledAt); - } + const thread = updatedThread(result); + expect(thread.settledOverride).toBe("settled"); + expect(thread.settledAt).toBe(settledAt); }); it.each([ @@ -213,8 +217,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 6, occurredAt: updatedAt, - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.unsettled", payload: { threadId: ThreadId.make("thread-1"), @@ -223,11 +225,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.settledOverride).toBe(settledOverride); - expect(result.thread.settledAt).toBeNull(); - } + const thread = updatedThread(result); + expect(thread.settledOverride).toBe(settledOverride); + expect(thread.settledAt).toBeNull(); }); }); @@ -238,8 +238,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 5, occurredAt: pinnedAt, - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.pinned", payload: { threadId: ThreadId.make("thread-1"), @@ -248,10 +246,8 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.pinnedAt).toBe(pinnedAt); - } + const thread = updatedThread(result); + expect(thread.pinnedAt).toBe(pinnedAt); }); it("clears pinnedAt", () => { @@ -264,8 +260,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 6, occurredAt: updatedAt, - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.unpinned", payload: { threadId: ThreadId.make("thread-1"), @@ -273,10 +267,8 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.pinnedAt).toBeNull(); - } + const thread = updatedThread(result); + expect(thread.pinnedAt).toBeNull(); }); }); @@ -286,8 +278,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), @@ -297,13 +287,11 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.title).toBe("Updated Title"); - expect(result.thread.branch).toBe("feature/demo"); - // Model selection should be unchanged since it wasn't in the payload - expect(result.thread.modelSelection).toEqual(baseThread.modelSelection); - } + const thread = updatedThread(result); + expect(thread.title).toBe("Updated Title"); + expect(thread.branch).toBe("feature/demo"); + // Model selection should be unchanged since it wasn't in the payload + expect(thread.modelSelection).toEqual(baseThread.modelSelection); }); it("sets and clears a linked pull request", () => { @@ -317,8 +305,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), @@ -335,8 +321,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 6, occurredAt: "2026-04-01T06:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), @@ -358,8 +342,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 6, occurredAt: "2026-04-01T06:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.message-sent", payload: { threadId: ThreadId.make("thread-1"), @@ -373,11 +355,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.messages).toHaveLength(1); - expect(result.thread.messages[0]?.text).toBe("Hello, world!"); - } + const thread = updatedThread(result); + expect(thread.messages).toHaveLength(1); + expect(thread.messages[0]?.text).toBe("Hello, world!"); }); it("appends text for streaming messages", () => { @@ -400,8 +380,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 7, occurredAt: "2026-04-01T06:01:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.message-sent", payload: { threadId: ThreadId.make("thread-1"), @@ -415,11 +393,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.messages).toHaveLength(1); - expect(result.thread.messages[0]?.text).toBe("Hello, world!"); - } + const thread = updatedThread(result); + expect(thread.messages).toHaveLength(1); + expect(thread.messages[0]?.text).toBe("Hello, world!"); }); it("updates latestTurn for assistant messages with a turn", () => { @@ -427,8 +403,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 8, occurredAt: "2026-04-01T07:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.message-sent", payload: { threadId: ThreadId.make("thread-1"), @@ -442,12 +416,10 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.latestTurn?.turnId).toBe("turn-1"); - expect(result.thread.latestTurn?.state).toBe("completed"); - expect(result.thread.latestTurn?.assistantMessageId).toBe("msg-3"); - } + const thread = updatedThread(result); + expect(thread.latestTurn?.turnId).toBe("turn-1"); + expect(thread.latestTurn?.state).toBe("completed"); + expect(thread.latestTurn?.assistantMessageId).toBe("msg-3"); }); it("keeps latestTurn running for interim assistant messages while the session runs the turn", () => { @@ -476,8 +448,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 8, occurredAt: "2026-04-01T07:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.message-sent", payload: { threadId: ThreadId.make("thread-1"), @@ -491,11 +461,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.latestTurn?.state).toBe("running"); - expect(result.thread.latestTurn?.completedAt).toBeNull(); - } + const thread = updatedThread(result); + expect(thread.latestTurn?.state).toBe("running"); + expect(thread.latestTurn?.completedAt).toBeNull(); }); it("keeps latestTurn and checkpoints references across a streaming delta", () => { @@ -546,8 +514,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 9, occurredAt: "2026-04-01T07:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.message-sent", payload: { threadId: ThreadId.make("thread-1"), @@ -561,13 +527,11 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.messages).not.toBe(streamingThread.messages); - expect(result.thread.messages[0]?.text).toBe("Hello, world"); - expect(result.thread.latestTurn).toBe(streamingThread.latestTurn); - expect(result.thread.checkpoints).toBe(streamingThread.checkpoints); - } + const thread = updatedThread(result); + expect(thread.messages).not.toBe(streamingThread.messages); + expect(thread.messages[0]?.text).toBe("Hello, world"); + expect(thread.latestTurn).toBe(streamingThread.latestTurn); + expect(thread.checkpoints).toBe(streamingThread.checkpoints); }); it("replaces latestTurn and checkpoints when the first assistant message binds the turn", () => { @@ -607,8 +571,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 9, occurredAt: "2026-04-01T07:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.message-sent", payload: { threadId: ThreadId.make("thread-1"), @@ -622,13 +584,11 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.latestTurn).not.toBe(unboundThread.latestTurn); - expect(result.thread.latestTurn?.assistantMessageId).toBe("msg-2"); - expect(result.thread.checkpoints).not.toBe(unboundThread.checkpoints); - expect(result.thread.checkpoints[0]?.assistantMessageId).toBe("msg-2"); - } + const thread = updatedThread(result); + expect(thread.latestTurn).not.toBe(unboundThread.latestTurn); + expect(thread.latestTurn?.assistantMessageId).toBe("msg-2"); + expect(thread.checkpoints).not.toBe(unboundThread.checkpoints); + expect(thread.checkpoints[0]?.assistantMessageId).toBe("msg-2"); }); }); @@ -650,8 +610,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 9, occurredAt: "2026-04-01T08:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.session-set", payload: { threadId: ThreadId.make("thread-1"), @@ -667,11 +625,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.latestTurn?.state).toBe("completed"); - expect(result.thread.latestTurn?.completedAt).toBe("2026-04-01T08:00:00.000Z"); - } + const thread = updatedThread(result); + expect(thread.latestTurn?.state).toBe("completed"); + expect(thread.latestTurn?.completedAt).toBe("2026-04-01T08:00:00.000Z"); }); it("updates session and latestTurn for a running session", () => { @@ -679,8 +635,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 9, occurredAt: "2026-04-01T08:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.session-set", payload: { threadId: ThreadId.make("thread-1"), @@ -696,12 +650,10 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.session?.status).toBe("running"); - expect(result.thread.latestTurn?.turnId).toBe("turn-1"); - expect(result.thread.latestTurn?.state).toBe("running"); - } + const thread = updatedThread(result); + expect(thread.session?.status).toBe("running"); + expect(thread.latestTurn?.turnId).toBe("turn-1"); + expect(thread.latestTurn?.state).toBe("running"); }); }); @@ -724,8 +676,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 10, occurredAt: "2026-04-01T09:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.session-stop-requested", payload: { threadId: ThreadId.make("thread-1"), @@ -733,11 +683,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.session?.status).toBe("stopped"); - expect(result.thread.session?.activeTurnId).toBeNull(); - } + const thread = updatedThread(result); + expect(thread.session?.status).toBe("stopped"); + expect(thread.session?.activeTurnId).toBeNull(); }); it("returns unchanged when no session exists", () => { @@ -745,8 +693,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 10, occurredAt: "2026-04-01T09:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.session-stop-requested", payload: { threadId: ThreadId.make("thread-1"), @@ -763,8 +709,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 11, occurredAt: "2026-04-01T10:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.proposed-plan-upserted", payload: { threadId: ThreadId.make("thread-1"), @@ -780,11 +724,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.proposedPlans).toHaveLength(1); - expect(result.thread.proposedPlans[0]?.id).toBe("plan-1"); - } + const thread = updatedThread(result); + expect(thread.proposedPlans).toHaveLength(1); + expect(thread.proposedPlans[0]?.id).toBe("plan-1"); }); }); @@ -794,8 +736,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 12, occurredAt: "2026-04-01T11:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), @@ -811,11 +751,9 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.activities).toHaveLength(1); - expect(result.thread.activities[0]?.kind).toBe("file-edit"); - } + const thread = updatedThread(result); + expect(thread.activities).toHaveLength(1); + expect(thread.activities[0]?.kind).toBe("file-edit"); }); it("preserves the complete activity history when live events arrive", () => { @@ -835,8 +773,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 130, occurredAt: "2026-04-01T11:01:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), @@ -854,114 +790,78 @@ describe("applyThreadDetailEvent", () => { }, ); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.activities).toHaveLength(130); - expect(result.thread.activities[0]?.id).toBe("activity-0"); - } + const thread = updatedThread(result); + expect(thread.activities).toHaveLength(130); + expect(thread.activities[0]?.id).toBe("activity-0"); }); it("re-sorts when an activity arrives out of order", () => { - const makeActivity = (id: string, sequence: number) => ({ - id: EventId.make(id), - tone: "tool" as const, - kind: "command", - summary: `Ran command ${sequence}`, - payload: {}, - turnId: TurnId.make("turn-1"), - sequence, - createdAt: "2026-04-01T11:00:00.000Z", - }); const result = applyThreadDetailEvent( { ...baseThread, - activities: [makeActivity("activity-a", 1), makeActivity("activity-c", 3)], + activities: [ + makeActivity("activity-a", 1, "Ran command 1"), + makeActivity("activity-c", 3, "Ran command 3"), + ], }, { ...baseEventFields, sequence: 131, occurredAt: "2026-04-01T11:01:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), - activity: makeActivity("activity-b", 2), + activity: makeActivity("activity-b", 2, "Ran command 2"), }, }, ); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.activities.map((activity) => activity.id)).toEqual([ - "activity-a", - "activity-b", - "activity-c", - ]); - } + const thread = updatedThread(result); + expect(thread.activities.map((activity) => activity.id)).toEqual([ + "activity-a", + "activity-b", + "activity-c", + ]); }); it("repairs snapshot ordering before fast-path appends engage", () => { - const makeActivity = (id: string, sequence: number | null) => ({ - id: EventId.make(id), - tone: "tool" as const, - kind: "command", - summary: `Ran ${id}`, - payload: {}, - turnId: TurnId.make("turn-1"), - ...(sequence === null ? {} : { sequence }), - createdAt: "2026-04-01T11:00:00.000Z", - }); // Snapshot loads deliver null-sequence rows first (DB order), which // activityOrder sorts last; an in-order live append must not freeze // that prefix. const result = applyThreadDetailEvent( { ...baseThread, - activities: [makeActivity("activity-null", null), makeActivity("activity-a", 1)], + activities: [ + makeActivity("activity-null", null, "Ran activity-null"), + makeActivity("activity-a", 1, "Ran activity-a"), + ], }, { ...baseEventFields, sequence: 135, occurredAt: "2026-04-01T11:01:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), - activity: makeActivity("activity-b", 2), + activity: makeActivity("activity-b", 2, "Ran activity-b"), }, }, ); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.activities.map((activity) => activity.id)).toEqual([ - "activity-a", - "activity-b", - "activity-null", - ]); - } + const thread = updatedThread(result); + expect(thread.activities.map((activity) => activity.id)).toEqual([ + "activity-a", + "activity-b", + "activity-null", + ]); }); it("dedupes a re-delivery arriving right after an in-order append", () => { - const makeActivity = (id: string, sequence: number, summary: string) => ({ - id: EventId.make(id), - tone: "tool" as const, - kind: "command", - summary, - payload: {}, - turnId: TurnId.make("turn-1"), - sequence, - createdAt: "2026-04-01T11:00:00.000Z", - }); const makeEvent = (sequence: number, activity: ReturnType) => ({ ...baseEventFields, sequence, occurredAt: "2026-04-01T11:01:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), activity }, }) as const; @@ -997,16 +897,6 @@ describe("applyThreadDetailEvent", () => { }); it("replaces a re-delivered activity instead of duplicating it", () => { - const makeActivity = (id: string, sequence: number, summary: string) => ({ - id: EventId.make(id), - tone: "tool" as const, - kind: "command", - summary, - payload: {}, - turnId: TurnId.make("turn-1"), - sequence, - createdAt: "2026-04-01T11:00:00.000Z", - }); const result = applyThreadDetailEvent( { ...baseThread, @@ -1019,8 +909,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 132, occurredAt: "2026-04-01T11:01:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), @@ -1029,14 +917,12 @@ describe("applyThreadDetailEvent", () => { }, ); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.activities.map((activity) => activity.id)).toEqual([ - "activity-a", - "activity-b", - ]); - expect(result.thread.activities[1]?.summary).toBe("second (redelivered)"); - } + const thread = updatedThread(result); + expect(thread.activities.map((activity) => activity.id)).toEqual([ + "activity-a", + "activity-b", + ]); + expect(thread.activities[1]?.summary).toBe("second (redelivered)"); }); it("replaces earlier resolvable context-window updates for the same turn", () => { @@ -1066,8 +952,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 20, occurredAt: "2026-04-01T11:02:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), @@ -1076,13 +960,11 @@ describe("applyThreadDetailEvent", () => { }, ); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - const ids = result.thread.activities.map((activity) => activity.id); - // Same-turn resolvable rows collapse to the newest; the other turn's - // row and the malformed row are untouched. - expect(ids).toEqual(["activity-other-turn", "activity-cw-malformed", "activity-cw-3"]); - } + const thread = updatedThread(result); + const ids = thread.activities.map((activity) => activity.id); + // Same-turn resolvable rows collapse to the newest; the other turn's + // row and the malformed row are untouched. + expect(ids).toEqual(["activity-other-turn", "activity-cw-malformed", "activity-cw-3"]); }); it("does not collapse context-window history for a malformed update", () => { @@ -1103,8 +985,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 21, occurredAt: "2026-04-01T11:03:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.activity-appended", payload: { threadId: ThreadId.make("thread-1"), @@ -1118,13 +998,11 @@ describe("applyThreadDetailEvent", () => { }, ); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - // The resolvable row must survive so consumers can still derive a - // usage value by walking backwards past the malformed row. - const ids = result.thread.activities.map((activity) => activity.id); - expect(ids).toEqual(["activity-cw-resolvable", "activity-cw-broken"]); - } + const thread = updatedThread(result); + // The resolvable row must survive so consumers can still derive a + // usage value by walking backwards past the malformed row. + const ids = thread.activities.map((activity) => activity.id); + expect(ids).toEqual(["activity-cw-resolvable", "activity-cw-broken"]); }); }); @@ -1134,8 +1012,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 13, occurredAt: "2026-04-01T12:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.turn-diff-completed", payload: { threadId: ThreadId.make("thread-1"), @@ -1149,12 +1025,10 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - expect(result.thread.checkpoints).toHaveLength(1); - expect(result.thread.latestTurn?.turnId).toBe("turn-1"); - expect(result.thread.latestTurn?.state).toBe("completed"); - } + const thread = updatedThread(result); + expect(thread.checkpoints).toHaveLength(1); + expect(thread.latestTurn?.turnId).toBe("turn-1"); + expect(thread.latestTurn?.state).toBe("completed"); }); }); @@ -1217,8 +1091,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 14, occurredAt: "2026-04-01T04:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.reverted", payload: { threadId: ThreadId.make("thread-1"), @@ -1226,15 +1098,13 @@ describe("applyThreadDetailEvent", () => { }, }); - expect(result.kind).toBe("updated"); - if (result.kind === "updated") { - // turn-2 checkpoint is filtered out (turnCount 2 > revert target 1) - expect(result.thread.checkpoints).toHaveLength(1); - expect(result.thread.checkpoints[0]?.turnId).toBe("turn-1"); - // msg-3 (turn-2) is filtered, msg-1 (no turn) and msg-2 (turn-1) remain - expect(result.thread.messages).toHaveLength(2); - expect(result.thread.latestTurn?.turnId).toBe("turn-1"); - } + const thread = updatedThread(result); + // turn-2 checkpoint is filtered out (turnCount 2 > revert target 1) + expect(thread.checkpoints).toHaveLength(1); + expect(thread.checkpoints[0]?.turnId).toBe("turn-1"); + // msg-3 (turn-2) is filtered, msg-1 (no turn) and msg-2 (turn-1) remain + expect(thread.messages).toHaveLength(2); + expect(thread.latestTurn?.turnId).toBe("turn-1"); }); }); @@ -1244,8 +1114,6 @@ describe("applyThreadDetailEvent", () => { ...baseEventFields, sequence: 15, occurredAt: "2026-04-01T13:00:00.000Z", - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), type: "thread.approval-response-requested", payload: { threadId: ThreadId.make("thread-1"), diff --git a/packages/client-runtime/src/work-log/activity-parsing.ts b/packages/client-runtime/src/work-log/activity-parsing.ts new file mode 100644 index 000000000000..79b7b4af5e03 --- /dev/null +++ b/packages/client-runtime/src/work-log/activity-parsing.ts @@ -0,0 +1,405 @@ +import { + isToolLifecycleItemType, + type ToolLifecycleItemType, + type OrchestrationThreadActivity, + type UserInputQuestion, +} from "@t3tools/contracts"; + +export function parseUserInputQuestions( + payload: Record | null, +): ReadonlyArray | null { + const questions = payload?.questions; + if (!Array.isArray(questions)) { + return null; + } + const parsed = questions + .map((entry) => { + if (!entry || typeof entry !== "object") return null; + const question = entry as Record; + if ( + typeof question.id !== "string" || + typeof question.header !== "string" || + typeof question.question !== "string" || + !Array.isArray(question.options) + ) { + return null; + } + const options = question.options + .map((option) => { + if (!option || typeof option !== "object") return null; + const optionRecord = option as Record; + if ( + typeof optionRecord.label !== "string" || + typeof optionRecord.description !== "string" + ) { + return null; + } + return { + label: optionRecord.label, + description: optionRecord.description, + ...(typeof optionRecord.value === "string" ? { value: optionRecord.value } : {}), + }; + }) + .filter((option): option is UserInputQuestion["options"][number] => option !== null); + if (options.length === 0 && question.allowCustomAnswer === false) { + return null; + } + return { + id: question.id, + header: question.header, + question: question.question, + options, + multiSelect: question.multiSelect === true, + ...(typeof question.allowCustomAnswer === "boolean" + ? { allowCustomAnswer: question.allowCustomAnswer } + : {}), + }; + }) + .filter((question): question is UserInputQuestion => question !== null); + return parsed.length > 0 ? parsed : null; +} + +/** Adapters forward unknown wire-only SDK messages (background_tasks_changed, + * commands_changed, ...) as runtime warnings. The suffix comes from + * describeUnknownSdkMessage in the Claude adapter; a row with no displayable + * text carries nothing a user can act on, so it does not render. */ +export function isNoContentRuntimeWarning(activity: OrchestrationThreadActivity): boolean { + return ( + activity.kind === "runtime.warning" && + activity.summary.endsWith("(no displayable text content)") + ); +} + +export function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { + if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { + return false; + } + + const payload = + activity.payload && typeof activity.payload === "object" + ? (activity.payload as Record) + : null; + return typeof payload?.detail === "string" && payload.detail.startsWith("ExitPlanMode:"); +} + +export function mergeChangedFiles( + previous: ReadonlyArray | undefined, + next: ReadonlyArray | undefined, +): string[] { + const merged = [...(previous ?? []), ...(next ?? [])]; + if (merged.length === 0) { + return []; + } + return [...new Set(merged)]; +} + +export function asRecord(value: unknown): Record | null { + return value && typeof value === "object" ? (value as Record) : null; +} + +export function asTrimmedString(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function trimMatchingOuterQuotes(value: string): string { + const trimmed = value.trim(); + if ( + (trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"')) + ) { + const unquoted = trimmed.slice(1, -1).trim(); + return unquoted.length > 0 ? unquoted : trimmed; + } + return trimmed; +} + +function executableBasename(value: string): string | null { + const trimmed = trimMatchingOuterQuotes(value); + if (trimmed.length === 0) { + return null; + } + const normalized = trimmed.replace(/\\/g, "/"); + const segments = normalized.split("/"); + const last = segments.at(-1)?.trim() ?? ""; + return last.length > 0 ? last.toLowerCase() : null; +} + +function splitExecutableAndRest(value: string): { executable: string; rest: string } | null { + const trimmed = value.trim(); + if (trimmed.length === 0) { + return null; + } + + if (trimmed.startsWith('"') || trimmed.startsWith("'")) { + const quote = trimmed.charAt(0); + const closeIndex = trimmed.indexOf(quote, 1); + if (closeIndex <= 0) { + return null; + } + return { + executable: trimmed.slice(0, closeIndex + 1), + rest: trimmed.slice(closeIndex + 1).trim(), + }; + } + + const firstWhitespace = trimmed.search(/\s/); + if (firstWhitespace < 0) { + return { + executable: trimmed, + rest: "", + }; + } + + return { + executable: trimmed.slice(0, firstWhitespace), + rest: trimmed.slice(firstWhitespace).trim(), + }; +} + +const SHELL_WRAPPER_SPECS = [ + { + executables: ["pwsh", "pwsh.exe", "powershell", "powershell.exe"], + wrapperFlagPattern: /(?:^|\s)-command\s+/i, + }, + { + executables: ["cmd", "cmd.exe"], + wrapperFlagPattern: /(?:^|\s)\/c\s+/i, + }, + { + executables: ["bash", "sh", "zsh"], + wrapperFlagPattern: /(?:^|\s)-(?:l)?c\s+/i, + }, +] as const; + +function findShellWrapperSpec(shell: string) { + return SHELL_WRAPPER_SPECS.find((spec) => + (spec.executables as ReadonlyArray).includes(shell), + ); +} + +function unwrapCommandRemainder(value: string, wrapperFlagPattern: RegExp): string | null { + const match = wrapperFlagPattern.exec(value); + if (!match) { + return null; + } + + const command = value.slice(match.index + match[0].length).trim(); + if (command.length === 0) { + return null; + } + + const openingQuote = command[0]; + if ((openingQuote === "'" || openingQuote === '"') && !command.endsWith(openingQuote)) { + return null; + } + + const unwrapped = trimMatchingOuterQuotes(command); + return unwrapped.length > 0 ? unwrapped : null; +} + +function unwrapKnownShellCommandWrapper(value: string): string { + const split = splitExecutableAndRest(value); + if (!split || split.rest.length === 0) { + return value; + } + + const shell = executableBasename(split.executable); + if (!shell) { + return value; + } + + const spec = findShellWrapperSpec(shell); + if (!spec) { + return value; + } + + return unwrapCommandRemainder(split.rest, spec.wrapperFlagPattern) ?? value; +} + +function formatCommandArrayPart(value: string): string { + return /[\s"'`]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; +} + +function formatCommandValue(value: unknown): string | null { + const direct = asTrimmedString(value); + if (direct) { + return direct; + } + if (!Array.isArray(value)) { + return null; + } + const parts: Array = []; + for (const entry of value) { + const part = asTrimmedString(entry); + if (part !== null) { + parts.push(part); + } + } + if (parts.length === 0) { + return null; + } + return parts.map((part) => formatCommandArrayPart(part)).join(" "); +} + +function normalizeCommandValue(value: unknown): string | null { + const formatted = formatCommandValue(value); + return formatted ? unwrapKnownShellCommandWrapper(formatted) : null; +} + +function toRawToolCommand(value: unknown, normalizedCommand: string | null): string | null { + const formatted = formatCommandValue(value); + if (!formatted || normalizedCommand === null) { + return null; + } + return formatted === normalizedCommand ? null : formatted; +} + +export function extractToolCommand(payload: Record | null): { + command: string | null; + rawCommand: string | null; +} { + const data = asRecord(payload?.data); + const item = asRecord(data?.item); + const itemResult = asRecord(item?.result); + const itemInput = asRecord(item?.input); + const itemType = asTrimmedString(payload?.itemType); + const detail = asTrimmedString(payload?.detail); + const candidates: unknown[] = [ + item?.command, + itemInput?.command, + itemResult?.command, + data?.command, + itemType === "command_execution" && detail ? stripTrailingExitCode(detail).output : null, + ]; + + for (const candidate of candidates) { + const command = normalizeCommandValue(candidate); + if (!command) { + continue; + } + return { + command, + rawCommand: toRawToolCommand(candidate, command), + }; + } + + return { + command: null, + rawCommand: null, + }; +} + +export function extractToolTitle(payload: Record | null): string | null { + return asTrimmedString(payload?.title); +} + +export function stripTrailingExitCode(value: string): { + output: string | null; + exitCode?: number | undefined; +} { + const trimmed = value.trim(); + const match = /^(?[\s\S]*?)(?:\s*\d+)>)\s*$/i.exec( + trimmed, + ); + if (!match?.groups) { + return { + output: trimmed.length > 0 ? trimmed : null, + }; + } + const exitCode = Number.parseInt(match.groups.code ?? "", 10); + const normalizedOutput = match.groups.output?.trim() ?? ""; + return { + output: normalizedOutput.length > 0 ? normalizedOutput : null, + ...(Number.isInteger(exitCode) ? { exitCode } : {}), + }; +} + +export function extractWorkLogItemType( + payload: Record | null, +): ToolLifecycleItemType | undefined { + if (typeof payload?.itemType === "string" && isToolLifecycleItemType(payload.itemType)) { + return payload.itemType; + } + return undefined; +} + +function pushChangedFile(target: string[], seen: Set, value: unknown) { + const normalized = asTrimmedString(value); + if (!normalized || seen.has(normalized)) { + return; + } + seen.add(normalized); + target.push(normalized); +} + +function collectChangedFiles(value: unknown, target: string[], seen: Set, depth: number) { + if (depth > 4 || target.length >= 12) { + return; + } + if (Array.isArray(value)) { + for (const entry of value) { + collectChangedFiles(entry, target, seen, depth + 1); + if (target.length >= 12) { + return; + } + } + return; + } + + const record = asRecord(value); + if (!record) { + return; + } + + pushChangedFile(target, seen, record.path); + pushChangedFile(target, seen, record.filePath); + pushChangedFile(target, seen, record.relativePath); + pushChangedFile(target, seen, record.filename); + pushChangedFile(target, seen, record.newPath); + pushChangedFile(target, seen, record.oldPath); + + for (const nestedKey of [ + "item", + "result", + "input", + "data", + "changes", + "files", + "edits", + "patch", + "patches", + "operations", + ]) { + if (!(nestedKey in record)) { + continue; + } + collectChangedFiles(record[nestedKey], target, seen, depth + 1); + if (target.length >= 12) { + return; + } + } +} + +export function extractChangedFiles(payload: Record | null): string[] { + const changedFiles: string[] = []; + const seen = new Set(); + collectChangedFiles(asRecord(payload?.data), changedFiles, seen, 0); + return changedFiles; +} + +export function compareActivityLifecycleRank(kind: string): number { + if (kind.endsWith(".started") || kind === "tool.started") { + return 0; + } + if (kind.endsWith(".progress") || kind.endsWith(".updated")) { + return 1; + } + if (kind.endsWith(".completed") || kind.endsWith(".resolved")) { + return 2; + } + return 1; +} diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index a14a4ed0a08a..277cb402b532 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -1,3 +1,4 @@ +import type { Mutable } from "effect/Types"; import type { VcsCreateRefInput, VcsCreateRefResult, @@ -156,20 +157,12 @@ export const ContextMenuItemSchema: Schema.Codec = Sc ), }); -export type DesktopUpdateStatus = - | "disabled" - | "idle" - | "checking" - | "up-to-date" - | "available" - | "downloading" - | "downloaded" - | "error"; - -export type DesktopRuntimeArch = "arm64" | "x64" | "other"; -export type DesktopTheme = "light" | "dark" | "system"; -export type DesktopUpdateChannel = "latest" | "nightly"; -export type DesktopAppStageLabel = "Alpha" | "Dev" | "Nightly"; +export type DesktopUpdateStatus = typeof DesktopUpdateStatusSchema.Type; + +export type DesktopRuntimeArch = typeof DesktopRuntimeArchSchema.Type; +export type DesktopTheme = typeof DesktopThemeSchema.Type; +export type DesktopUpdateChannel = typeof DesktopUpdateChannelSchema.Type; +export type DesktopAppStageLabel = typeof DesktopAppStageLabelSchema.Type; export const DesktopUpdateStatusSchema = Schema.Literals([ "disabled", @@ -186,11 +179,7 @@ export const DesktopThemeSchema = Schema.Literals(["light", "dark", "system"]); export const DesktopUpdateChannelSchema = Schema.Literals(["latest", "nightly"]); export const DesktopAppStageLabelSchema = Schema.Literals(["Alpha", "Dev", "Nightly"]); -export interface DesktopAppBranding { - baseName: string; - stageLabel: DesktopAppStageLabel; - displayName: string; -} +export type DesktopAppBranding = Mutable; export const DesktopAppBrandingSchema = Schema.Struct({ baseName: Schema.String, @@ -314,7 +303,7 @@ export const DesktopSshEnvironmentTargetSchema = Schema.Struct({ }); export type DesktopSshEnvironmentTarget = typeof DesktopSshEnvironmentTargetSchema.Type; -export type DesktopSshHostSource = "ssh-config" | "known-hosts"; +export type DesktopSshHostSource = typeof DesktopSshHostSourceSchema.Type; export const DesktopSshHostSourceSchema = Schema.Literals(["ssh-config", "known-hosts"]); export interface DesktopDiscoveredSshHost extends DesktopSshEnvironmentTarget { @@ -411,20 +400,14 @@ export const PersistedSavedEnvironmentRecordSchema = Schema.Struct({ }); export type PersistedSavedEnvironmentRecord = typeof PersistedSavedEnvironmentRecordSchema.Type; -export type DesktopServerExposureMode = "local-only" | "network-accessible"; +export type DesktopServerExposureMode = typeof DesktopServerExposureModeSchema.Type; export const DesktopServerExposureModeSchema = Schema.Literals([ "local-only", "network-accessible", ]); -export interface DesktopServerExposureState { - mode: DesktopServerExposureMode; - endpointUrl: string | null; - advertisedHost: string | null; - tailscaleServeEnabled: boolean; - tailscaleServePort: number; -} +export type DesktopServerExposureState = Mutable; export const DesktopServerExposureStateSchema = Schema.Struct({ mode: DesktopServerExposureModeSchema, @@ -454,11 +437,7 @@ export const PickFolderOptionsSchema = Schema.Struct({ * empty text so the renderer can reject them by size without the main * process ever holding their contents. */ -export interface PickedThemeFile { - name: string; - size: number; - text: string; -} +export type PickedThemeFile = Mutable; export const PickedThemeFileSchema = Schema.Struct({ name: Schema.String, @@ -466,11 +445,7 @@ export const PickedThemeFileSchema = Schema.Struct({ text: Schema.String, }); -export interface DesktopWslDistro { - name: string; - isDefault: boolean; - version: 1 | 2; -} +export type DesktopWslDistro = Mutable; export const DesktopWslDistroSchema = Schema.Struct({ name: Schema.String, @@ -528,21 +503,16 @@ export type DesktopPreviewNavStatus = * Emulated `prefers-color-scheme` for the guest page. "system" clears the * override so the page follows the OS appearance. */ -export type DesktopPreviewColorScheme = "system" | "light" | "dark"; +export type DesktopPreviewColorScheme = typeof DesktopPreviewColorSchemeSchema.Type; -export const DesktopPreviewColorSchemeSchema: Schema.Codec = - Schema.Literals(["system", "light", "dark"]); +export const DesktopPreviewColorSchemeSchema = Schema.Literals(["system", "light", "dark"]); export const FAVICON_DATA_URL_MAX_LENGTH = 8192; export const FAVICON_CAPTURED_AT_MAX = 8_640_000_000_000_000; -export interface DesktopPreviewFavicon { - dataUrl: string; - pageUrl: string; - capturedAt: number; -} +export type DesktopPreviewFavicon = Mutable; -export const DesktopPreviewFaviconSchema: Schema.Codec = Schema.Struct({ +export const DesktopPreviewFaviconSchema = Schema.Struct({ dataUrl: Schema.String.check( Schema.isMaxLength(FAVICON_DATA_URL_MAX_LENGTH), Schema.isPattern(/^data:image\/png;base64,[a-z0-9+/]+={0,2}$/i), @@ -654,46 +624,29 @@ export const DesktopPreviewWebviewConfigSchema: Schema.Codec = - Schema.Struct({ - colorScheme: Schema.Literals(["light", "dark"]), - radius: Schema.String, - background: Schema.String, - foreground: Schema.String, - popover: Schema.String, - popoverForeground: Schema.String, - primary: Schema.String, - primaryForeground: Schema.String, - muted: Schema.String, - mutedForeground: Schema.String, - accent: Schema.String, - accentForeground: Schema.String, - border: Schema.String, - input: Schema.String, - ring: Schema.String, - fontSans: Schema.String, - fontMono: Schema.String, - }); +export type DesktopPreviewAnnotationTheme = Mutable< + typeof DesktopPreviewAnnotationThemeSchema.Type +>; + +export const DesktopPreviewAnnotationThemeSchema = Schema.Struct({ + colorScheme: Schema.Literals(["light", "dark"]), + radius: Schema.String, + background: Schema.String, + foreground: Schema.String, + popover: Schema.String, + popoverForeground: Schema.String, + primary: Schema.String, + primaryForeground: Schema.String, + muted: Schema.String, + mutedForeground: Schema.String, + accent: Schema.String, + accentForeground: Schema.String, + border: Schema.String, + input: Schema.String, + ring: Schema.String, + fontSans: Schema.String, + fontMono: Schema.String, +}); export interface DesktopPreviewRecordingFrame { tabId: string; @@ -703,57 +656,40 @@ export interface DesktopPreviewRecordingFrame { receivedAt: string; } -export interface DesktopPreviewRecordingArtifact { - id: string; - tabId: string; - path: string; - mimeType: string; - sizeBytes: number; - createdAt: string; -} +export type DesktopPreviewRecordingArtifact = Mutable< + typeof DesktopPreviewRecordingArtifactSchema.Type +>; -export const DesktopPreviewRecordingArtifactSchema: Schema.Codec = - Schema.Struct({ - id: Schema.String, - tabId: DesktopPreviewTabIdSchema, - path: Schema.String, - mimeType: Schema.String, - sizeBytes: Schema.Int, - createdAt: Schema.String, - }); +export const DesktopPreviewRecordingArtifactSchema = Schema.Struct({ + id: Schema.String, + tabId: DesktopPreviewTabIdSchema, + path: Schema.String, + mimeType: Schema.String, + sizeBytes: Schema.Int, + createdAt: Schema.String, +}); -export interface DesktopPreviewScreenshotArtifact { - id: string; - tabId: string; - path: string; - mimeType: "image/png"; - sizeBytes: number; - createdAt: string; -} +export type DesktopPreviewScreenshotArtifact = Mutable< + typeof DesktopPreviewScreenshotArtifactSchema.Type +>; -export const DesktopPreviewScreenshotArtifactSchema: Schema.Codec = - Schema.Struct({ - id: Schema.String, - tabId: DesktopPreviewTabIdSchema, - path: Schema.String, - mimeType: Schema.Literal("image/png"), - sizeBytes: Schema.Int, - createdAt: Schema.String, - }); +export const DesktopPreviewScreenshotArtifactSchema = Schema.Struct({ + id: Schema.String, + tabId: DesktopPreviewTabIdSchema, + path: Schema.String, + mimeType: Schema.Literal("image/png"), + sizeBytes: Schema.Int, + createdAt: Schema.String, +}); /** * Single stack frame captured by react-grab's `getElementContext`. We surface * the source file/line so coding agents can jump straight to the JSX that * produced the picked DOM node. */ -export interface PickedElementStackFrame { - functionName: string | null; - fileName: string | null; - lineNumber: number | null; - columnNumber: number | null; -} +export type PickedElementStackFrame = Mutable; -export const PickedElementStackFrameSchema: Schema.Codec = Schema.Struct({ +export const PickedElementStackFrameSchema = Schema.Struct({ functionName: Schema.NullOr(Schema.String), fileName: Schema.NullOr(Schema.String), lineNumber: Schema.NullOr(Schema.Number), @@ -802,26 +738,18 @@ export const PickedElementPayloadSchema: Schema.Codec = Sc pickedAt: Schema.String, }); -export interface PreviewAnnotationRect { - x: number; - y: number; - width: number; - height: number; -} +export type PreviewAnnotationRect = Mutable; -export const PreviewAnnotationRectSchema: Schema.Codec = Schema.Struct({ +export const PreviewAnnotationRectSchema = Schema.Struct({ x: Schema.Number, y: Schema.Number, width: Schema.Number, height: Schema.Number, }); -export interface PreviewAnnotationPoint { - x: number; - y: number; -} +export type PreviewAnnotationPoint = Mutable; -export const PreviewAnnotationPointSchema: Schema.Codec = Schema.Struct({ +export const PreviewAnnotationPointSchema = Schema.Struct({ x: Schema.Number, y: Schema.Number, }); @@ -867,22 +795,15 @@ export const PreviewAnnotationStrokeTargetSchema: Schema.Codec; -export const PreviewAnnotationStyleChangeSchema: Schema.Codec = - Schema.Struct({ - targetId: Schema.String, - selector: Schema.NullOr(Schema.String), - property: Schema.String, - previousValue: Schema.String, - value: Schema.String, - }); +export const PreviewAnnotationStyleChangeSchema = Schema.Struct({ + targetId: Schema.String, + selector: Schema.NullOr(Schema.String), + property: Schema.String, + previousValue: Schema.String, + value: Schema.String, +}); export interface PreviewAnnotationScreenshot { dataUrl: string; @@ -932,9 +853,8 @@ export const PreviewAnnotationPayloadSchema: Schema.Codec = - Schema.Literals(["attach", "send"]); +export type PreviewAnnotationSubmission = typeof PreviewAnnotationSubmissionSchema.Type; +export const PreviewAnnotationSubmissionSchema = Schema.Literals(["attach", "send"]); export interface PreviewAnnotationSubmissionResult { annotation: PreviewAnnotationPayload; diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts index 940778fba1ea..7e122a477fc9 100644 --- a/packages/shared/src/model.ts +++ b/packages/shared/src/model.ts @@ -298,11 +298,6 @@ export function readCustomModelEntries(value: unknown): CustomModelDefinition[] return entries; } -/** Slugs of a `customModels` setting, in stored order. */ -export function readCustomModelSlugs(value: unknown): string[] { - return readCustomModelEntries(value).map((entry) => entry.slug); -} - /** * Write a definition back to the compact stored shape: a bare slug when it * carries nothing custom, otherwise an entry with only the set fields. diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index affb83eb7506..e4acd3255ec8 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -128,22 +128,17 @@ function mockProcess(exitCode: number, stdout = "") { }); } -function iconResizeSpawnerLayer( - commands: Array<{ readonly command: string; readonly args: ReadonlyArray }>, - exitCodes: ReadonlyArray, +function mockSpawnerLayer( + commands: Array, + exitCodes: ReadonlyArray = [], ) { let commandIndex = 0; return Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { - const childProcess = command as unknown as { - readonly command: string; - readonly args: ReadonlyArray; - }; - commands.push({ - command: childProcess.command, - args: childProcess.args, - }); + if (!ChildProcess.isStandardCommand(command)) + return Effect.die(new Error("Unexpected command pipeline")); + commands.push(command); return Effect.succeed(mockProcess(exitCodes[commandIndex++] ?? 0)); }), ); @@ -326,24 +321,16 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { it.effect("omits update feeds for pull request preview builds", () => Effect.gen(function* () { - const preview = yield* createBuildConfig( - "mac", - "dmg", - "0.0.33-pr.8182.1", - false, - false, - undefined, - undefined, - ); - const release = yield* createBuildConfig( - "mac", - "dmg", - "0.0.33", - false, - false, - undefined, - undefined, - ); + const preview = yield* createBuildConfig({ + platform: "mac", + target: "dmg", + version: "0.0.33-pr.8182.1", + }); + const release = yield* createBuildConfig({ + platform: "mac", + target: "dmg", + version: "0.0.33", + }); assert.notProperty(preview, "publish"); assert.deepStrictEqual(release.publish, [ @@ -576,44 +563,23 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { it.effect("applies platform-specific packaging to the build config", () => Effect.gen(function* () { - const mac = yield* createBuildConfig( - "mac", - "dmg", - "1.2.3", - false, - false, - undefined, - undefined, - ); - const linux = yield* createBuildConfig( - "linux", - "AppImage", - "1.2.3", - false, - false, - undefined, - undefined, - ); - const win = yield* createBuildConfig( - "win", - "nsis", - "1.2.3", - false, - false, - undefined, - undefined, - true, - ); - const winWithoutWslPrebuild = yield* createBuildConfig( - "win", - "nsis", - "1.2.3", - false, - false, - undefined, - undefined, - false, - ); + const mac = yield* createBuildConfig({ platform: "mac", target: "dmg", version: "1.2.3" }); + const linux = yield* createBuildConfig({ + platform: "linux", + target: "AppImage", + version: "1.2.3", + }); + const win = yield* createBuildConfig({ + platform: "win", + target: "nsis", + version: "1.2.3", + wslRuntimeBundled: true, + }); + const winWithoutWslPrebuild = yield* createBuildConfig({ + platform: "win", + target: "nsis", + version: "1.2.3", + }); // All platforms keep app.asar fully packed; Windows ships the server // tree as the hand-packed server.asar sidecar in extraResources instead @@ -837,8 +803,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { it.effect("reports every missing Linux desktop build prerequisite with an install command", () => Effect.scoped( Effect.gen(function* () { - const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = - []; + const commands: Array = []; const spawner = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { @@ -846,7 +811,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { readonly command: string; readonly args: ReadonlyArray; }; - commands.push(childProcess); + commands.push(command as ChildProcess.StandardCommand); const fails = childProcess.command === "cargo" || childProcess.command === "rustc" || @@ -959,7 +924,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-windows-preflight-" }); const pythonPath = path.join(tempDir, "python.exe"); yield* fs.writeFileString(pythonPath, "python"); - const commands: string[] = []; + const commands: Array = []; const spawner = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { @@ -1093,49 +1058,28 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ).pipe(Effect.provideService(HostProcessPlatform, "linux")), ); - it.effect("rejects a Windows package missing its expected WSL runtime", () => - Effect.scoped( - Effect.gen(function* () { - const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true }); - const error = yield* validateWindowsPackagedPayload({ - stageDistDir: fixture.stageDistDir, - appExecutableName: fixture.appExecutableName, - targetArch: "x64", - expectWslRuntime: true, - }).pipe(Effect.flip); - - assert.instanceOf(error, WindowsPackagedPayloadValidationError); - assert.equal(error.reason, "wsl-runtime-missing"); - }), - ), - ); - - it.effect("rejects forbidden native members in the emitted WSL archive", () => - Effect.scoped( - Effect.gen(function* () { - const fixture = yield* makeWindowsPayloadFixture({ - copyUnpackedNatives: true, - wslRuntime: "forbidden", - }); - const error = yield* validateWindowsPackagedPayload({ - stageDistDir: fixture.stageDistDir, - appExecutableName: fixture.appExecutableName, - targetArch: "x64", - expectWslRuntime: true, - }).pipe(Effect.flip); - - assert.instanceOf(error, WindowsPackagedPayloadValidationError); - assert.equal(error.reason, "wsl-runtime-invalid"); - }), - ), - ); - - it.effect("rejects an emitted WSL archive whose sidecar digest does not match", () => + it.effect.each([ + { + title: "missing its expected WSL runtime", + wslRuntime: undefined, + reason: "wsl-runtime-missing", + }, + { + title: "with forbidden WSL native members", + wslRuntime: "forbidden", + reason: "wsl-runtime-invalid", + }, + { + title: "with a mismatched WSL sidecar digest", + wslRuntime: "bad-digest", + reason: "wsl-runtime-invalid", + }, + ] as const)("rejects a Windows package $title", ({ wslRuntime, reason }) => Effect.scoped( Effect.gen(function* () { const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true, - wslRuntime: "bad-digest", + ...(wslRuntime === undefined ? {} : { wslRuntime }), }); const error = yield* validateWindowsPackagedPayload({ stageDistDir: fixture.stageDistDir, @@ -1143,29 +1087,15 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { targetArch: "x64", expectWslRuntime: true, }).pipe(Effect.flip); - assert.instanceOf(error, WindowsPackagedPayloadValidationError); - assert.equal(error.reason, "wsl-runtime-invalid"); + assert.equal(error.reason, reason); }), ), ); it.effect("probes fff through the packaged Windows primary instead of helper executables", () => { - const commands: Array<{ - readonly command: string; - readonly args: ReadonlyArray; - readonly options: { - readonly cwd?: string; - readonly env?: Readonly>; - }; - }> = []; - const spawnerLayer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make((command) => { - commands.push(command as unknown as (typeof commands)[number]); - return Effect.succeed(mockProcess(0)); - }), - ); + const commands: Array = []; + const spawnerLayer = mockSpawnerLayer(commands); return Effect.scoped( Effect.gen(function* () { @@ -1214,14 +1144,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); it.effect("builds the Linux browser secret helper for a concrete architecture", () => { - const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = []; - const spawnerLayer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make((command) => { - commands.push(command as unknown as (typeof commands)[number]); - return Effect.succeed(mockProcess(0)); - }), - ); + const commands: Array = []; + const spawnerLayer = mockSpawnerLayer(commands); return Effect.gen(function* () { // `universal` is a mac-only arch the option type still admits. The helper @@ -1257,14 +1181,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); it.effect("refuses a Linux build on a host that cannot build the browser secret helper", () => { - const commands: Array<{ readonly command: string }> = []; - const spawnerLayer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make((command) => { - commands.push(command as unknown as (typeof commands)[number]); - return Effect.succeed(mockProcess(0)); - }), - ); + const commands: Array = []; + const spawnerLayer = mockSpawnerLayer(commands); return Effect.gen(function* () { // The helper links against the host's libsecret and its build script is @@ -1293,19 +1211,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); it.effect("skips the primary native probe for cross-architecture Windows payloads", () => { - const commands: Array<{ - readonly command: string; - readonly options: { - readonly env?: Readonly>; - }; - }> = []; - const spawnerLayer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make((command) => { - commands.push(command as unknown as (typeof commands)[number]); - return Effect.succeed(mockProcess(0)); - }), - ); + const commands: Array = []; + const spawnerLayer = mockSpawnerLayer(commands); return Effect.scoped( Effect.gen(function* () { @@ -1469,11 +1376,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ); it.effect("preserves both Linux icon resize failures with structural context", () => { - const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = []; + const commands: Array = []; return Effect.gen(function* () { const error = yield* stageLinuxIconSize("source.png", "target.png", 512, false).pipe( - Effect.provide(iconResizeSpawnerLayer(commands, [1, 2])), + Effect.provide(mockSpawnerLayer(commands, [1, 2])), Effect.flip, ); @@ -1520,11 +1427,10 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { yield* fs.makeDirectory(dmgDir, { recursive: true }); const sourcePath = path.join(dmgDir, "dmg-background-nightly.svg"); yield* fs.writeFileString(sourcePath, ''); - const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = - []; + const commands: Array = []; yield* stageDesktopDmgBackground(stageResourcesDir, "nightly", false).pipe( - Effect.provide(iconResizeSpawnerLayer(commands, [0, 0])), + Effect.provide(mockSpawnerLayer(commands, [0, 0])), ); assert.deepStrictEqual( @@ -1696,9 +1602,15 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { it.effect("adds passkey entitlements and both renderer protocols to signed macOS builds", () => Effect.gen(function* () { - const config = yield* createBuildConfig("mac", "dmg", "1.2.3", true, false, undefined, { - entitlementsPath: "/tmp/entitlements.mac.plist", - provisioningProfilePath: "/tmp/t3code.provisionprofile", + const config = yield* createBuildConfig({ + platform: "mac", + target: "dmg", + version: "1.2.3", + signed: true, + macPasskeySigning: { + entitlementsPath: "/tmp/entitlements.mac.plist", + provisioningProfilePath: "/tmp/t3code.provisionprofile", + }, }); const mac = config.mac as Record; @@ -1714,15 +1626,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { it.effect("uses the nightly DMG background for nightly macOS builds", () => Effect.gen(function* () { - const config = yield* createBuildConfig( - "mac", - "dmg", - "1.2.3-nightly.20260815.1", - false, - false, - undefined, - undefined, - ); + const config = yield* createBuildConfig({ + platform: "mac", + target: "dmg", + version: "1.2.3-nightly.20260815.1", + }); assert.equal( (config.dmg as Record).background, @@ -1733,15 +1641,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { it.effect("keeps executable resource editing enabled for unsigned Windows builds", () => Effect.gen(function* () { - const config = yield* createBuildConfig( - "win", - "nsis", - "1.2.3", - false, - false, - undefined, - undefined, - ); + const config = yield* createBuildConfig({ + platform: "win", + target: "nsis", + version: "1.2.3", + }); const win = config.win as Record; assert.equal(win.icon, "icon.ico"); @@ -1834,11 +1738,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { // C:\... path, and handing that to tar is what made Git's GNU tar try to // reach a host named "C". it.effect("spawns tar with an archive target relative to the staged source tree", () => { - const commands: Array<{ - readonly command: string; - readonly args: ReadonlyArray; - readonly options: { readonly cwd?: string }; - }> = []; + const commands: Array = []; return Effect.scoped( Effect.gen(function* () { diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 32b090f7a83b..0c65ff4fcfb7 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -2562,25 +2562,33 @@ export function resolveDesktopProductName(version: string): string { : (desktopPackageJson.productName ?? "T3 Code"); } -export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( - platform: typeof BuildPlatform.Type, - target: string, - version: string, - signed: boolean, - mockUpdates: boolean, - mockUpdateServerPort: number | undefined, - macPasskeySigning: +export const createBuildConfig = Effect.fn("createBuildConfig")(function* ({ + platform, + target, + version, + signed = false, + mockUpdates = false, + mockUpdateServerPort, + macPasskeySigning, + wslRuntimeBundled = false, + arch, +}: { + readonly platform: typeof BuildPlatform.Type; + readonly target: string; + readonly version: string; + readonly signed?: boolean; + readonly mockUpdates?: boolean; + readonly mockUpdateServerPort?: number | undefined; + readonly macPasskeySigning?: | { readonly entitlementsPath: string; readonly provisioningProfilePath: string; } - | undefined, - // Windows only, and false when no Linux node-pty prebuild was bundled: the - // sidecar staging skips the archive in that case, and listing a resource - // whose source file was never written fails the electron-builder step. - wslRuntimeBundled = false, - arch?: typeof BuildArch.Type, -) { + | undefined; + // Only include WSL resources when sidecar staging actually wrote the archive. + readonly wslRuntimeBundled?: boolean; + readonly arch?: typeof BuildArch.Type; +}) { const buildConfig: Record = { appId: DESKTOP_APP_ID, productName: resolveDesktopProductName(version), @@ -3666,22 +3674,26 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( description: "T3 Code desktop build", author: "T3 Tools", main: "apps/desktop/dist-electron/main.cjs", - build: yield* createBuildConfig( - options.platform, - options.target, - appVersion, - options.signed, - options.mockUpdates, - options.mockUpdateServerPort, - macPasskeySigning && macEntitlementsPath - ? { - entitlementsPath: macEntitlementsPath, - provisioningProfilePath: macPasskeySigning.provisioningProfilePath, - } - : undefined, - bundlesWslRuntime({ arch: options.arch, prebuildPath: options.wslPrebuild }), - options.arch, - ), + build: yield* createBuildConfig({ + platform: options.platform, + target: options.target, + version: appVersion, + signed: options.signed, + mockUpdates: options.mockUpdates, + mockUpdateServerPort: options.mockUpdateServerPort, + macPasskeySigning: + macPasskeySigning && macEntitlementsPath + ? { + entitlementsPath: macEntitlementsPath, + provisioningProfilePath: macPasskeySigning.provisioningProfilePath, + } + : undefined, + wslRuntimeBundled: bundlesWslRuntime({ + arch: options.arch, + prebuildPath: options.wslPrebuild, + }), + arch: options.arch, + }), dependencies: stageDependencies, devDependencies: { electron: electronVersion, diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index 9b4f44475d95..10ea54c20cb1 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -59,6 +59,20 @@ function mockProcess(exit: number | PlatformError.PlatformError) { }); } +const defaultDevEnvInput = { + mode: "dev", + baseEnv: {}, + serverOffset: 0, + webOffset: 0, + t3Home: undefined, + browser: undefined, + autoBootstrapProjectFromCwd: undefined, + logWebSocketEvents: undefined, + host: undefined, + port: undefined, + devUrl: undefined, +} satisfies Parameters[0]; + const devServerInput = { mode: "dev:server", t3Home: "/tmp/t3code-dev-runner", @@ -140,19 +154,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { describe("createDevRunnerEnv", () => { it.effect("leaves the shared home implicit and disables browser auto-open", () => Effect.gen(function* () { - const env = yield* createDevRunnerEnv({ - mode: "dev", - baseEnv: {}, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, - }); + const env = yield* createDevRunnerEnv({ ...defaultDevEnvInput }); assert.equal(env.T3CODE_HOME, undefined); assert.equal(env.T3CODE_NO_BROWSER, "1"); @@ -161,19 +163,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("allows browser auto-open to be explicitly enabled", () => Effect.gen(function* () { - const env = yield* createDevRunnerEnv({ - mode: "dev", - baseEnv: {}, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: true, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, - }); + const env = yield* createDevRunnerEnv({ ...defaultDevEnvInput, browser: true }); assert.equal(env.T3CODE_NO_BROWSER, "0"); }), @@ -182,17 +172,9 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("requires the browser flag even when the environment enables auto-open", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ - mode: "dev", + ...defaultDevEnvInput, baseEnv: { T3CODE_NO_BROWSER: "0" }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, browser: false, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.T3CODE_NO_BROWSER, "1"); @@ -203,10 +185,8 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { Effect.gen(function* () { const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ + ...defaultDevEnvInput, mode: "dev:server", - baseEnv: {}, - serverOffset: 0, - webOffset: 0, t3Home: "/tmp/custom-t3", browser: false, autoBootstrapProjectFromCwd: false, @@ -231,20 +211,11 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("strips inherited service-launcher context", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ - mode: "dev", + ...defaultDevEnvInput, baseEnv: { T3_SERVICE_LAUNCHER_CONTEXT: '{"childVersion":"9.9.9"}', T3_BOOT_SERVICE_UNIT: "t3code.service", }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.T3_SERVICE_LAUNCHER_CONTEXT, undefined); @@ -255,19 +226,10 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("does not force websocket logging on in dev mode when unset", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ - mode: "dev", + ...defaultDevEnvInput, baseEnv: { T3CODE_LOG_WS_EVENTS: "keep-me-out", }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.T3CODE_MODE, "web"); @@ -278,19 +240,11 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("forwards explicit websocket logging false without coercing it away", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ - mode: "dev", + ...defaultDevEnvInput, baseEnv: { T3CODE_LOG_WS_EVENTS: "1", }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: false, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.T3CODE_LOG_WS_EVENTS, "0"); @@ -300,19 +254,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("uses custom t3Home when provided", () => Effect.gen(function* () { const path = yield* Path.Path; - const env = yield* createDevRunnerEnv({ - mode: "dev", - baseEnv: {}, - serverOffset: 0, - webOffset: 0, - t3Home: "/tmp/my-t3", - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, - }); + const env = yield* createDevRunnerEnv({ ...defaultDevEnvInput, t3Home: "/tmp/my-t3" }); assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3")); }), @@ -322,6 +264,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { Effect.gen(function* () { const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ + ...defaultDevEnvInput, mode: "dev:desktop", baseEnv: { T3CODE_PORT: "13773", @@ -331,15 +274,10 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { VITE_DEV_SERVER_URL: "http://127.0.0.1:8526", VITE_WS_URL: "ws://localhost:13773", }, - serverOffset: 0, - webOffset: 0, t3Home: "/tmp/my-t3", browser: true, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, host: "127.0.0.1", port: 4222, - devUrl: undefined, }); assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3")); @@ -357,19 +295,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("defaults dev server mode to the higher backend port range", () => Effect.gen(function* () { - const env = yield* createDevRunnerEnv({ - mode: "dev", - baseEnv: {}, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, - }); + const env = yield* createDevRunnerEnv({ ...defaultDevEnvInput }); assert.equal(env.T3CODE_PORT, "13773"); assert.equal(env.PORT, "5733"); @@ -383,20 +309,12 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect(`leaves the client backend URLs unset in ${mode} mode`, () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ + ...defaultDevEnvInput, mode, baseEnv: { VITE_HTTP_URL: "http://localhost:1234", VITE_WS_URL: "ws://localhost:1234", }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.VITE_HTTP_URL, undefined); @@ -415,17 +333,9 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("clears the single-origin marker in dev:desktop mode", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ + ...defaultDevEnvInput, mode: "dev:desktop", baseEnv: { T3CODE_SINGLE_ORIGIN_DEV: "1" }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.T3CODE_SINGLE_ORIGIN_DEV, undefined); @@ -436,17 +346,9 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("clears the single-origin marker in dev:server mode", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ + ...defaultDevEnvInput, mode: "dev:server", baseEnv: { T3CODE_SINGLE_ORIGIN_DEV: "1" }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.T3CODE_SINGLE_ORIGIN_DEV, undefined); @@ -461,17 +363,9 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect(`drops an inherited HOST in ${mode} mode`, () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ + ...defaultDevEnvInput, mode, baseEnv: { HOST: "0.0.0.0" }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.HOST, undefined); @@ -484,17 +378,9 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("drops an inherited HOST even when --host is given", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ - mode: "dev", + ...defaultDevEnvInput, baseEnv: { HOST: "0.0.0.0" }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, host: "0.0.0.0", - port: undefined, - devUrl: undefined, }); assert.equal(env.HOST, undefined); @@ -506,17 +392,9 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("still pins HOST for dev:desktop", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ + ...defaultDevEnvInput, mode: "dev:desktop", baseEnv: { HOST: "0.0.0.0" }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.HOST, "127.0.0.1"); @@ -525,19 +403,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("keeps explicit backend URLs for the desktop renderer", () => Effect.gen(function* () { - const env = yield* createDevRunnerEnv({ - mode: "dev:desktop", - baseEnv: {}, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, - }); + const env = yield* createDevRunnerEnv({ ...defaultDevEnvInput, mode: "dev:desktop" }); assert.equal(env.VITE_HTTP_URL, "http://127.0.0.1:13773"); assert.equal(env.VITE_WS_URL, "ws://127.0.0.1:13773"); @@ -882,17 +748,8 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { it.effect("ignores an ambient T3CODE_HOME when no home is resolved", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ - mode: "dev", + ...defaultDevEnvInput, baseEnv: { T3CODE_HOME: "/home/user/.t3" }, - serverOffset: 0, - webOffset: 0, - t3Home: undefined, - browser: undefined, - autoBootstrapProjectFromCwd: undefined, - logWebSocketEvents: undefined, - host: undefined, - port: undefined, - devUrl: undefined, }); assert.equal(env.T3CODE_HOME, undefined); diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index d426cc7829b1..2cb12cbee9c3 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -340,62 +340,36 @@ export function createDevRunnerEnv({ delete output.T3CODE_HOME; } - // A dev-runner server is never launcher-managed. When the shell that runs - // this script was itself spawned by the machine's managed t3 service (an - // agent working inside T3 Code), these leak through and the child server - // fails startup with "The service launcher started a different t3 version" - // (serviceLauncherClient.ts resolveStartup). + // Managed-service context must not leak into a separately launched dev server. delete output.T3_SERVICE_LAUNCHER_CONTEXT; delete output.T3_BOOT_SERVICE_UNIT; + delete output.T3CODE_DESKTOP_WS_URL; + output.T3CODE_PORT = String(serverPort); - if (!isDesktopMode) { - output.T3CODE_PORT = String(serverPort); - // HOST is Vite's own bind address, and the desktop branch below is the - // only place we set it. An inherited one (an exported HOST, a container, - // a `HOST=0.0.0.0 npm start` habit) would otherwise reach Vite and pin - // its HMR socket to that address — see the `explicitHost` gate in - // apps/web/vite.config.ts. Over a shared origin that is invisible: the - // page loads and only HMR quietly dials the wrong machine. - delete output.HOST; - if (mode === "dev" || mode === "dev:web") { - // Browser dev is single-origin: everything (including /ws) is proxied - // through Vite, so the client must resolve its backend from - // window.location.origin rather than a baked-in localhost URL. See - // resolveConfiguredPrimaryTarget in apps/web/src/environments/primary/target.ts - // — it only defers to the origin when both of these are absent. Baking - // localhost here is what breaks any non-localhost origin (tailnet, LAN, - // phone): the remote browser dials its own machine. - delete output.VITE_HTTP_URL; - delete output.VITE_WS_URL; - // Deleting is not enough on its own: vite.config.ts calls loadRepoEnv, - // which merges `.env`/`.env.local` *under* this env, so a developer - // with either URL in their `.env` would get it back and silently lose - // single-origin mode. This states the intent positively so Vite can - // ignore those values rather than infer from their absence. - output.T3CODE_SINGLE_ORIGIN_DEV = "1"; - } else { - output.VITE_HTTP_URL = `http://localhost:${serverPort}`; - output.VITE_WS_URL = `ws://localhost:${serverPort}`; - delete output.T3CODE_SINGLE_ORIGIN_DEV; - } - } else { - output.T3CODE_PORT = String(serverPort); - output.VITE_HTTP_URL = `http://${DESKTOP_DEV_LOOPBACK_HOST}:${serverPort}`; - output.VITE_WS_URL = `ws://${DESKTOP_DEV_LOOPBACK_HOST}:${serverPort}`; - // Desktop pins the renderer to loopback on purpose; an ambient marker - // must not make Vite drop those URLs. - delete output.T3CODE_SINGLE_ORIGIN_DEV; + if (isDesktopMode) { + output.HOST = DESKTOP_DEV_LOOPBACK_HOST; delete output.T3CODE_MODE; delete output.T3CODE_NO_BROWSER; delete output.T3CODE_HOST; + } else { + // Inherited HOST would pin Vite's HMR connection to the wrong interface. + delete output.HOST; + output.T3CODE_MODE = "web"; + output.T3CODE_NO_BROWSER = browser === true ? "0" : "1"; + if (host !== undefined) output.T3CODE_HOST = host; } - if (!isDesktopMode && host !== undefined) { - output.T3CODE_HOST = host; - } - - if (!isDesktopMode) { - output.T3CODE_NO_BROWSER = browser === true ? "0" : "1"; + if (mode === "dev" || mode === "dev:web") { + // Remote browsers use Vite's proxy. The marker also prevents .env files + // from restoring localhost URLs when vite.config.ts loads the environment. + delete output.VITE_HTTP_URL; + delete output.VITE_WS_URL; + output.T3CODE_SINGLE_ORIGIN_DEV = "1"; + } else { + const backendHost = isDesktopMode ? DESKTOP_DEV_LOOPBACK_HOST : "localhost"; + output.VITE_HTTP_URL = `http://${backendHost}:${serverPort}`; + output.VITE_WS_URL = `ws://${backendHost}:${serverPort}`; + delete output.T3CODE_SINGLE_ORIGIN_DEV; } if (autoBootstrapProjectFromCwd !== undefined) { @@ -410,21 +384,6 @@ export function createDevRunnerEnv({ delete output.T3CODE_LOG_WS_EVENTS; } - if (mode === "dev") { - output.T3CODE_MODE = "web"; - delete output.T3CODE_DESKTOP_WS_URL; - } - - if (mode === "dev:server" || mode === "dev:web") { - output.T3CODE_MODE = "web"; - delete output.T3CODE_DESKTOP_WS_URL; - } - - if (isDesktopMode) { - output.HOST = DESKTOP_DEV_LOOPBACK_HOST; - delete output.T3CODE_DESKTOP_WS_URL; - } - return output; }); }