diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index 71bcf5f7aef1c4b5753d2882e325180b9fdfd797..ce0c7d013a9916048373b2beaac0401e3b8eb7c5 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -57,7 +57,6 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) => }), setAppUserModelId: () => Effect.void, getAppMetrics: Effect.succeed([]), - isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), setDesktopName: () => Effect.void, setDockIcon: (iconPath) => diff --git a/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts b/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts index aa28ff8d86eb554760ded328bb25b555b61316d4..fdc69841343cbaef094a6b87ded3107e1527fa83 100644 --- a/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts +++ b/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts @@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest"; import { ConnectionCatalogDocument } from "@t3tools/client-runtime/platform"; import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Layer from "effect/Layer"; @@ -22,6 +23,11 @@ const textEncoder = new TextEncoder(); const decodeConnectionCatalog = Schema.decodeEffect( Schema.fromJsonString(ConnectionCatalogDocument), ); +const encodeLegacySavedEnvironments = Schema.encodeEffect( + Schema.fromJsonString( + Schema.Struct({ version: Schema.Literal(1), records: Schema.Array(Schema.Unknown) }), + ), +); function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref | null = null) { return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, { isEncryptionAvailable: Effect.succeed(available), @@ -125,7 +131,8 @@ describe("DesktopConnectionCatalogStore", () => { withStore( Effect.gen(function* () { const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore; - const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; + const environment = yield* DesktopEnvironment.DesktopEnvironment; + const fileSystem = yield* FileSystem.FileSystem; const records: readonly PersistedSavedEnvironmentRecord[] = [ { environmentId: EnvironmentId.make("relay-environment"), @@ -159,11 +166,21 @@ describe("DesktopConnectionCatalogStore", () => { lastConnectedAt: null, }, ]; - yield* savedEnvironments.setRegistry(records); - assert.isTrue( - yield* savedEnvironments.setSecret({ - environmentId: EnvironmentId.make("bearer-environment"), - secret: "legacy-token", + yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); + yield* fileSystem.writeFileString( + environment.savedEnvironmentRegistryPath, + yield* encodeLegacySavedEnvironments({ + version: 1, + records: records.map((record) => + record.environmentId === "bearer-environment" + ? { + ...record, + encryptedBearerToken: Encoding.encodeBase64( + textEncoder.encode("encrypted:legacy-token"), + ), + } + : record, + ), }), ); @@ -218,7 +235,10 @@ describe("DesktopConnectionCatalogStore", () => { assert.equal(catalog.credentials[0].credential.token, "legacy-token"); } - yield* savedEnvironments.setRegistry([]); + yield* fileSystem.writeFileString( + environment.savedEnvironmentRegistryPath, + '{"version":1,"records":[]}', + ); assert.deepEqual(yield* store.get, migrated); }), ), diff --git a/apps/desktop/src/app/DesktopLifecycle.test.ts b/apps/desktop/src/app/DesktopLifecycle.test.ts index 515be6726c94c3ebe09449fa9b4557780e6f6e85..33c74f5a8b9b8a533f9b48459fbba013e5810801 100644 --- a/apps/desktop/src/app/DesktopLifecycle.test.ts +++ b/apps/desktop/src/app/DesktopLifecycle.test.ts @@ -43,7 +43,6 @@ function makeElectronAppLayer( setAboutPanelOptions: () => Effect.void, setAppUserModelId: () => Effect.void, getAppMetrics: Effect.succeed([]), - isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), setDesktopName: () => Effect.void, setDockIcon: () => Effect.void, diff --git a/apps/desktop/src/backend/DesktopBackendManager.test.ts b/apps/desktop/src/backend/DesktopBackendManager.test.ts index 5a1ef70ad1c2ab473ab1471d56a3c828e5cbc960..df2001f1ac204836548792a06abe5e61ab7bf59e 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.test.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.test.ts @@ -165,9 +165,7 @@ function makeTestInstance(input: MakeInstanceInput) { latest: Effect.succeed(Option.none()), changes: Stream.empty, encoded: input.desktopTelemetryStream ?? Stream.empty, - handleControl: () => Effect.void, - handleControlForSource: (_sourceId, message) => - (input.desktopTelemetryPublisher?.handleControl ?? (() => Effect.void))(message), + handleControlForSource: () => Effect.void, removeControlSource: () => Effect.void, publishUpdateReport: () => Effect.void, updateRequests: Stream.empty, @@ -596,7 +594,7 @@ describe("DesktopBackendManager", () => { const instance = yield* makeTestInstance({ spawnerLayer, desktopTelemetryPublisher: { - handleControl: (message) => + handleControlForSource: (_sourceId, message) => message.type === "setDiagnosticsDemand" ? Deferred.succeed(handled, message.enabled).pipe(Effect.asVoid) : Effect.void, diff --git a/apps/desktop/src/backend/DesktopBackendPool.test.ts b/apps/desktop/src/backend/DesktopBackendPool.test.ts index b9a96d72a77aea438504f34265ee19218d865262..7859223161b734547469b1e6d03a1f8c51e5a3a4 100644 --- a/apps/desktop/src/backend/DesktopBackendPool.test.ts +++ b/apps/desktop/src/backend/DesktopBackendPool.test.ts @@ -70,7 +70,6 @@ function makePoolLayer( latest: Effect.succeed(Option.none()), changes: Stream.empty, encoded: Stream.empty, - handleControl: () => Effect.void, handleControlForSource: () => Effect.void, removeControlSource: () => Effect.void, publishUpdateReport: () => Effect.void, diff --git a/apps/desktop/src/electron/ElectronApp.test.ts b/apps/desktop/src/electron/ElectronApp.test.ts index 4189ea793e2d945ad949ef9e8b9aab6213d5ccf9..84f37b6f5f53753cc2583b04a8dfed651b123630 100644 --- a/apps/desktop/src/electron/ElectronApp.test.ts +++ b/apps/desktop/src/electron/ElectronApp.test.ts @@ -10,7 +10,6 @@ const { getAppPathMock, getSystemLocaleMock, getVersionMock, - isDefaultProtocolClientMock, onMock, quitMock, relaunchMock, @@ -32,7 +31,6 @@ const { getAppPathMock: vi.fn(() => "/app"), getSystemLocaleMock: vi.fn(() => "en-GB"), getVersionMock: vi.fn(() => "1.2.3"), - isDefaultProtocolClientMock: vi.fn(() => false), onMock: vi.fn(), quitMock: vi.fn(), relaunchMock: vi.fn(), @@ -64,7 +62,6 @@ vi.mock("electron", () => ({ getAppPath: getAppPathMock, getSystemLocale: getSystemLocaleMock, getVersion: getVersionMock, - isDefaultProtocolClient: isDefaultProtocolClientMock, isPackaged: true, name: "T3 Code", on: onMock, diff --git a/apps/desktop/src/electron/ElectronApp.ts b/apps/desktop/src/electron/ElectronApp.ts index b54da535add0727608c2119a819ce42698860ae0..f75ea51552e744247e813315eb17155c92b36dac 100644 --- a/apps/desktop/src/electron/ElectronApp.ts +++ b/apps/desktop/src/electron/ElectronApp.ts @@ -64,7 +64,6 @@ export class ElectronApp extends Context.Service< ) => Effect.Effect; readonly setAppUserModelId: (id: string) => Effect.Effect; readonly getAppMetrics: Effect.Effect>; - readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect; readonly setAsDefaultProtocolClient: ( protocol: string, path?: string, @@ -166,8 +165,6 @@ export const make = ElectronApp.of({ Electron.app.setAppUserModelId(id); }), getAppMetrics: Effect.sync(() => Electron.app.getAppMetrics()), - isDefaultProtocolClient: (protocol) => - Effect.sync(() => Electron.app.isDefaultProtocolClient(protocol)), setAsDefaultProtocolClient: (protocol, path, args) => Effect.sync(() => { if (path === undefined) { diff --git a/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts b/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts index 348f6cb3843e2bf9d83f9d950bc9632b01fbc91d..dad53815f3a65c7b7d02327fd8c2b4e17525ca2d 100644 --- a/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts +++ b/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts @@ -2,6 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Layer from "effect/Layer"; @@ -39,16 +40,29 @@ const SavedEnvironmentRegistryDocumentProbe = Schema.Struct({ const SavedEnvironmentRegistryDocumentProbeJson = Schema.fromJsonString( SavedEnvironmentRegistryDocumentProbe, ); -const decodeSavedEnvironmentRegistryDocumentProbe = Schema.decodeEffect( - SavedEnvironmentRegistryDocumentProbeJson, -); const encodeSavedEnvironmentRegistryDocumentProbe = Schema.encodeEffect( SavedEnvironmentRegistryDocumentProbeJson, ); + +const seedSavedEnvironmentRegistry = Effect.fn(function* (encryptedBearerToken?: string) { + const environment = yield* DesktopEnvironment.DesktopEnvironment; + const fileSystem = yield* FileSystem.FileSystem; + yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); + const encoded = yield* encodeSavedEnvironmentRegistryDocumentProbe({ + version: 1, + records: [ + { + ...savedRegistryRecord, + ...(encryptedBearerToken === undefined ? {} : { encryptedBearerToken }), + }, + ], + }); + yield* fileSystem.writeFileString(environment.savedEnvironmentRegistryPath, `${encoded}\n`); +}); + function makeSafeStorageLayer(input: { readonly available: boolean; readonly availabilityError?: unknown; - readonly encryptError?: unknown; readonly decryptError?: unknown; }) { return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, { @@ -60,14 +74,7 @@ function makeSafeStorageLayer(input: { cause: input.availabilityError, }), ), - encryptString: (value) => - input.encryptError === undefined - ? Effect.succeed(textEncoder.encode(`enc:${value}`)) - : Effect.fail( - new ElectronSafeStorage.ElectronSafeStorageEncryptError({ - cause: input.encryptError, - }), - ), + encryptString: (value) => Effect.succeed(textEncoder.encode(`enc:${value}`)), decryptString: (value) => { if (input.decryptError !== undefined) { return Effect.fail( @@ -96,7 +103,6 @@ function makeLayer( options?: { readonly availableSecretStorage?: boolean; readonly availabilityError?: unknown; - readonly encryptError?: unknown; readonly decryptError?: unknown; }, fileSystemLayer: Layer.Layer = NodeServices.layer, @@ -120,7 +126,6 @@ function makeLayer( const safeStorageLayer = makeSafeStorageLayer({ available: options?.availableSecretStorage ?? true, availabilityError: options?.availabilityError, - encryptError: options?.encryptError, decryptError: options?.decryptError, }); const dependencies = Layer.mergeAll( @@ -138,7 +143,6 @@ const withSavedEnvironments = ( options?: { readonly availableSecretStorage?: boolean; readonly availabilityError?: unknown; - readonly encryptError?: unknown; readonly decryptError?: unknown; }, ) => @@ -151,20 +155,13 @@ const withSavedEnvironments = ( }).pipe(Effect.provide(NodeServices.layer), Effect.scoped); describe("DesktopSavedEnvironments", () => { - it.effect("persists and reloads saved environment metadata", () => + it.effect("reads saved environment metadata", () => withSavedEnvironments( Effect.gen(function* () { - const environment = yield* DesktopEnvironment.DesktopEnvironment; - const fileSystem = yield* FileSystem.FileSystem; const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* savedEnvironments.setRegistry([savedRegistryRecord]); + yield* seedSavedEnvironmentRegistry(); assert.deepEqual(yield* savedEnvironments.getRegistry, [savedRegistryRecord]); - const persisted = yield* decodeSavedEnvironmentRegistryDocumentProbe( - yield* fileSystem.readFileString(environment.savedEnvironmentRegistryPath), - ); - assert.equal(persisted.version, 1); - assert.lengthOf(persisted.records, 1); }), ), ); @@ -205,17 +202,12 @@ describe("DesktopSavedEnvironments", () => { ), ); - it.effect("persists encrypted saved environment secrets when encryption is available", () => + it.effect("reads encrypted saved environment secrets when encryption is available", () => withSavedEnvironments( Effect.gen(function* () { const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* savedEnvironments.setRegistry([savedRegistryRecord]); - - assert.isTrue( - yield* savedEnvironments.setSecret({ - environmentId: savedRegistryRecord.environmentId, - secret: "bearer-token", - }), + yield* seedSavedEnvironmentRegistry( + Encoding.encodeBase64(textEncoder.encode("enc:bearer-token")), ); assert.deepEqual( @@ -230,14 +222,8 @@ describe("DesktopSavedEnvironments", () => { withSavedEnvironments( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; - const fileSystem = yield* FileSystem.FileSystem; const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); - const encoded = yield* encodeSavedEnvironmentRegistryDocumentProbe({ - version: 1, - records: [{ ...savedRegistryRecord, encryptedBearerToken: "%%%" }], - }); - yield* fileSystem.writeFileString(environment.savedEnvironmentRegistryPath, `${encoded}\n`); + yield* seedSavedEnvironmentRegistry("%%%"); const error = yield* savedEnvironments .getSecret(savedRegistryRecord.environmentId) @@ -256,17 +242,17 @@ describe("DesktopSavedEnvironments", () => { ), ); - it.effect("returns false when writing secrets while encryption is unavailable", () => + it.effect("returns no secret while encryption is unavailable", () => withSavedEnvironments( Effect.gen(function* () { const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* savedEnvironments.setRegistry([savedRegistryRecord]); + yield* seedSavedEnvironmentRegistry( + Encoding.encodeBase64(textEncoder.encode("enc:bearer-token")), + ); - assert.isFalse( - yield* savedEnvironments.setSecret({ - environmentId: savedRegistryRecord.environmentId, - secret: "next-token", - }), + assert.deepEqual( + yield* savedEnvironments.getSecret(savedRegistryRecord.environmentId), + Option.none(), ); }), { availableSecretStorage: false }, @@ -279,13 +265,12 @@ describe("DesktopSavedEnvironments", () => { Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* savedEnvironments.setRegistry([savedRegistryRecord]); + yield* seedSavedEnvironmentRegistry( + Encoding.encodeBase64(textEncoder.encode("enc:bearer-token")), + ); const error = yield* savedEnvironments - .setSecret({ - environmentId: savedRegistryRecord.environmentId, - secret: "next-token", - }) + .getSecret(savedRegistryRecord.environmentId) .pipe(Effect.flip); assert.instanceOf( @@ -309,45 +294,6 @@ describe("DesktopSavedEnvironments", () => { ); }); - it.effect("removes saved environment secrets", () => - withSavedEnvironments( - Effect.gen(function* () { - const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* savedEnvironments.setRegistry([savedRegistryRecord]); - yield* savedEnvironments.setSecret({ - environmentId: savedRegistryRecord.environmentId, - secret: "bearer-token", - }); - - yield* savedEnvironments.removeSecret(savedRegistryRecord.environmentId); - - assert.isTrue( - Option.isNone(yield* savedEnvironments.getSecret(savedRegistryRecord.environmentId)), - ); - }), - ), - ); - - it.effect("removes saved environment metadata and its embedded secret atomically", () => - withSavedEnvironments( - Effect.gen(function* () { - const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* savedEnvironments.setRegistry([savedRegistryRecord]); - yield* savedEnvironments.setSecret({ - environmentId: savedRegistryRecord.environmentId, - secret: "bearer-token", - }); - - yield* savedEnvironments.removeEnvironment(savedRegistryRecord.environmentId); - - assert.deepEqual(yield* savedEnvironments.getRegistry, []); - assert.isTrue( - Option.isNone(yield* savedEnvironments.getSecret(savedRegistryRecord.environmentId)), - ); - }), - ), - ); - it.effect("treats empty saved environment documents as empty", () => withSavedEnvironments( Effect.gen(function* () { @@ -388,13 +334,6 @@ describe("DesktopSavedEnvironments", () => { secretError, DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError, ); - const mutationError = yield* savedEnvironments - .setRegistry([savedRegistryRecord]) - .pipe(Effect.flip); - assert.instanceOf( - mutationError, - DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError, - ); }), ), ); @@ -431,77 +370,4 @@ describe("DesktopSavedEnvironments", () => { assert.notEqual(error.message, permissionError.message); }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); - - it.effect("reports the failed saved environment write operation and path", () => - Effect.gen(function* () { - const baseFileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({ - prefix: "t3-desktop-saved-environments-test-", - }); - const permissionError = PlatformError.systemError({ - _tag: "PermissionDenied", - module: "FileSystem", - method: "makeDirectory", - pathOrDescriptor: path.join(baseDir, "userdata"), - }); - const fileSystemLayer = Layer.succeed( - FileSystem.FileSystem, - FileSystem.makeNoop({ - readFileString: baseFileSystem.readFileString, - makeDirectory: () => Effect.fail(permissionError), - }), - ); - const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments.pipe( - Effect.provide(makeLayer(baseDir, undefined, fileSystemLayer)), - ); - - const error = yield* savedEnvironments.setRegistry([savedRegistryRecord]).pipe(Effect.flip); - assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsWriteError); - assert.equal(error.operation, "create-directory"); - assert.equal(error.path, path.join(baseDir, "userdata")); - assert.strictEqual(error.cause, permissionError); - assert.equal( - error.message, - `Desktop saved-environment write failed during create-directory at ${path.join(baseDir, "userdata")}.`, - ); - assert.notEqual(error.message, permissionError.message); - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), - ); - - it.effect("returns false when writing a secret without metadata", () => - withSavedEnvironments( - Effect.gen(function* () { - const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - - assert.isFalse( - yield* savedEnvironments.setSecret({ - environmentId: savedRegistryRecord.environmentId, - secret: "bearer-token", - }), - ); - }), - ), - ); - - it.effect("preserves encrypted secrets when metadata is rewritten", () => - withSavedEnvironments( - Effect.gen(function* () { - const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; - yield* savedEnvironments.setRegistry([savedRegistryRecord]); - yield* savedEnvironments.setSecret({ - environmentId: savedRegistryRecord.environmentId, - secret: "bearer-token", - }); - - yield* savedEnvironments.setRegistry([savedRegistryRecord]); - - assert.deepEqual(yield* savedEnvironments.getRegistry, [savedRegistryRecord]); - assert.deepEqual( - yield* savedEnvironments.getSecret(savedRegistryRecord.environmentId), - Option.some("bearer-token"), - ); - }), - ), - ); }); diff --git a/apps/desktop/src/settings/DesktopSavedEnvironments.ts b/apps/desktop/src/settings/DesktopSavedEnvironments.ts index 79959bc3c733e21b6baf101da4ec069820840368..f54b464a7dbdf99a64d94abeab25819d8c9dbeef 100644 --- a/apps/desktop/src/settings/DesktopSavedEnvironments.ts +++ b/apps/desktop/src/settings/DesktopSavedEnvironments.ts @@ -1,13 +1,11 @@ import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import * as Context from "effect/Context"; -import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; @@ -65,37 +63,12 @@ const SavedEnvironmentRegistryDocumentJson = fromLenientJson( const decodeSavedEnvironmentRegistryDocumentJson = Schema.decodeEffect( SavedEnvironmentRegistryDocumentJson, ); -const encodeSavedEnvironmentRegistryDocumentJson = Schema.encodeEffect( - SavedEnvironmentRegistryDocumentJson, -); - -const DesktopSavedEnvironmentsWriteOperation = Schema.Literals([ - "create-temporary-file-name", - "encode-registry", - "create-directory", - "write-temporary-file", - "replace-registry-file", -]); const DesktopSavedEnvironmentSecretProtectionOperation = Schema.Literals([ "check-encryption-availability", - "encrypt-secret", "decrypt-secret", ]); -export class DesktopSavedEnvironmentsWriteError extends Schema.TaggedError()( - "DesktopSavedEnvironmentsWriteError", - { - operation: DesktopSavedEnvironmentsWriteOperation, - path: Schema.String, - cause: Schema.Defect(), - }, -) { - override get message(): string { - return `Desktop saved-environment write failed during ${this.operation} at ${this.path}.`; - } -} - export class DesktopSavedEnvironmentsReadError extends Schema.TaggedError()( "DesktopSavedEnvironmentsReadError", { @@ -152,19 +125,12 @@ export type DesktopSavedEnvironmentsReadRegistryError = | DesktopSavedEnvironmentsReadError | DesktopSavedEnvironmentsDocumentDecodeError; -export type DesktopSavedEnvironmentsMutationError = - | DesktopSavedEnvironmentsReadRegistryError - | DesktopSavedEnvironmentsWriteError; - export type DesktopSavedEnvironmentsGetSecretError = | DesktopSavedEnvironmentsReadRegistryError | DesktopSavedEnvironmentSecretDecodeError | DesktopSavedEnvironmentSecretProtectionError; -export type DesktopSavedEnvironmentsSetSecretError = - | DesktopSavedEnvironmentsMutationError - | DesktopSavedEnvironmentSecretProtectionError; - +/** Reads the previous registry for connection-catalog migration. */ export class DesktopSavedEnvironments extends Context.Service< DesktopSavedEnvironments, { @@ -172,22 +138,10 @@ export class DesktopSavedEnvironments extends Context.Service< readonly PersistedSavedEnvironmentRecord[], DesktopSavedEnvironmentsReadRegistryError >; - readonly setRegistry: ( - records: readonly PersistedSavedEnvironmentRecord[], - ) => Effect.Effect; - readonly removeEnvironment: ( - environmentId: string, - ) => Effect.Effect; + readonly getSecret: ( environmentId: string, ) => Effect.Effect, DesktopSavedEnvironmentsGetSecretError>; - readonly setSecret: (input: { - readonly environmentId: string; - readonly secret: string; - }) => Effect.Effect; - readonly removeSecret: ( - environmentId: string, - ) => Effect.Effect; } >()("@t3tools/desktop/settings/DesktopSavedEnvironments") {} @@ -209,28 +163,6 @@ function toPersistedSavedEnvironmentRecord( }; } -function toSavedEnvironmentStorageRecord( - record: PersistedSavedEnvironmentRecord | PersistedSavedEnvironmentStorageRecord, - encryptedBearerToken: Option.Option, -): PersistedSavedEnvironmentStorageRecord { - const nextRecord = { - environmentId: record.environmentId, - label: record.label, - httpBaseUrl: record.httpBaseUrl, - wsBaseUrl: record.wsBaseUrl, - createdAt: record.createdAt, - lastConnectedAt: record.lastConnectedAt, - }; - const metadata = { - ...(record.desktopSsh ? { desktopSsh: record.desktopSsh } : {}), - ...(record.relayManaged ? { relayManaged: record.relayManaged } : {}), - }; - return Option.match(encryptedBearerToken, { - onNone: () => ({ ...nextRecord, ...metadata }), - onSome: (value) => ({ ...nextRecord, ...metadata, encryptedBearerToken: value }), - }); -} - function normalizeSavedEnvironmentRegistryDocument( document: SavedEnvironmentRegistryStorageDocument, ): SavedEnvironmentRegistryDocument { @@ -272,80 +204,6 @@ function readRegistryDocument( ); } -const writeRegistryDocument = Effect.fn("desktop.savedEnvironments.writeRegistryDocument")( - function* (input: { - readonly fileSystem: FileSystem.FileSystem; - readonly path: Path.Path; - readonly registryPath: string; - readonly document: SavedEnvironmentRegistryDocument; - readonly suffix: string; - }): Effect.fn.Return { - const directory = input.path.dirname(input.registryPath); - const tempPath = `${input.registryPath}.${process.pid}.${input.suffix}.tmp`; - const encoded = yield* encodeSavedEnvironmentRegistryDocumentJson(input.document).pipe( - Effect.mapError( - (cause) => - new DesktopSavedEnvironmentsWriteError({ - operation: "encode-registry", - path: input.registryPath, - cause, - }), - ), - ); - yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe( - Effect.mapError( - (cause) => - new DesktopSavedEnvironmentsWriteError({ - operation: "create-directory", - path: directory, - cause, - }), - ), - ); - yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe( - Effect.mapError( - (cause) => - new DesktopSavedEnvironmentsWriteError({ - operation: "write-temporary-file", - path: tempPath, - cause, - }), - ), - ); - yield* input.fileSystem.rename(tempPath, input.registryPath).pipe( - Effect.mapError( - (cause) => - new DesktopSavedEnvironmentsWriteError({ - operation: "replace-registry-file", - path: input.registryPath, - cause, - }), - ), - ); - }, -); - -function preserveExistingSecrets( - currentDocument: SavedEnvironmentRegistryDocument, - records: readonly PersistedSavedEnvironmentRecord[], -): SavedEnvironmentRegistryDocument { - const encryptedBearerTokenById = new Map( - currentDocument.records.flatMap((record) => - record.encryptedBearerToken - ? [[record.environmentId, record.encryptedBearerToken] as const] - : [], - ), - ); - - return { - version: currentDocument.version, - records: records.map((record) => { - const encryptedBearerToken = encryptedBearerTokenById.get(record.environmentId); - return toSavedEnvironmentStorageRecord(record, Option.fromNullishOr(encryptedBearerToken)); - }), - }; -} - function decodeSecretBytes( environmentId: string, registryPath: string, @@ -368,31 +226,8 @@ function decodeSecretBytes( export const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; + const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage; - const crypto = yield* Crypto.Crypto; - - const writeDocument = (document: SavedEnvironmentRegistryDocument) => - crypto.randomUUIDv4.pipe( - Effect.map((uuid) => uuid.replace(/-/g, "")), - Effect.mapError( - (cause) => - new DesktopSavedEnvironmentsWriteError({ - operation: "create-temporary-file-name", - path: environment.savedEnvironmentRegistryPath, - cause, - }), - ), - Effect.flatMap((suffix) => - writeRegistryDocument({ - fileSystem, - path, - registryPath: environment.savedEnvironmentRegistryPath, - document, - suffix, - }), - ), - ); return DesktopSavedEnvironments.of({ getRegistry: readRegistryDocument(fileSystem, environment.savedEnvironmentRegistryPath).pipe( @@ -401,30 +236,7 @@ export const make = Effect.gen(function* () { ), Effect.withSpan("desktop.savedEnvironments.getRegistry"), ), - setRegistry: Effect.fn("desktop.savedEnvironments.setRegistry")(function* (records) { - const currentDocument = yield* readRegistryDocument( - fileSystem, - environment.savedEnvironmentRegistryPath, - ); - yield* writeDocument(preserveExistingSecrets(currentDocument, records)); - }), - removeEnvironment: Effect.fn("desktop.savedEnvironments.removeEnvironment")( - function* (environmentId) { - yield* Effect.annotateCurrentSpan({ environmentId }); - const document = yield* readRegistryDocument( - fileSystem, - environment.savedEnvironmentRegistryPath, - ); - if (!document.records.some((record) => record.environmentId === environmentId)) { - return; - } - yield* writeDocument({ - version: document.version, - records: document.records.filter((record) => record.environmentId !== environmentId), - }); - }, - ), getSecret: Effect.fn("desktop.savedEnvironments.getSecret")(function* (environmentId) { yield* Effect.annotateCurrentSpan({ environmentId }); const document = yield* readRegistryDocument( @@ -472,85 +284,6 @@ export const make = Effect.gen(function* () { ), ); }), - setSecret: Effect.fn("desktop.savedEnvironments.setSecret")(function* (input) { - const { environmentId, secret } = input; - yield* Effect.annotateCurrentSpan({ environmentId }); - const document = yield* readRegistryDocument( - fileSystem, - environment.savedEnvironmentRegistryPath, - ); - - const encryptionAvailable = yield* safeStorage.isEncryptionAvailable.pipe( - Effect.mapError( - (cause) => - new DesktopSavedEnvironmentSecretProtectionError({ - operation: "check-encryption-availability", - environmentId, - registryPath: environment.savedEnvironmentRegistryPath, - cause, - }), - ), - ); - if (!encryptionAvailable) { - return false; - } - - const encryptedBearerToken = Encoding.encodeBase64( - yield* safeStorage.encryptString(secret).pipe( - Effect.mapError( - (cause) => - new DesktopSavedEnvironmentSecretProtectionError({ - operation: "encrypt-secret", - environmentId, - registryPath: environment.savedEnvironmentRegistryPath, - cause, - }), - ), - ), - ); - let found = false; - const nextDocument: SavedEnvironmentRegistryDocument = { - version: document.version, - records: document.records.map((record) => { - if (record.environmentId !== environmentId) { - return record; - } - - found = true; - return toSavedEnvironmentStorageRecord(record, Option.some(encryptedBearerToken)); - }), - }; - - if (found) { - yield* writeDocument(nextDocument); - } - return found; - }), - removeSecret: Effect.fn("desktop.savedEnvironments.removeSecret")(function* (environmentId) { - yield* Effect.annotateCurrentSpan({ environmentId }); - const document = yield* readRegistryDocument( - fileSystem, - environment.savedEnvironmentRegistryPath, - ); - if ( - !document.records.some( - (record) => - record.environmentId === environmentId && record.encryptedBearerToken !== undefined, - ) - ) { - return; - } - - yield* writeDocument({ - version: document.version, - records: document.records.map((record) => { - if (record.environmentId !== environmentId) { - return record; - } - return toPersistedSavedEnvironmentRecord(record); - }), - }); - }), }); }); diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 5a76402b1d34e599beecd170882772ac9a1cb29f..6a369a4598a27bbe6d8085de18f9bdc2ee9d1ef4 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -397,16 +397,6 @@ describe("DesktopShellEnvironment", () => { }), ); - it("resolves dbus runtime dir candidates with existence checks", () => { - const busPath = DesktopShellEnvironment.resolveDefaultLinuxDbusSessionBusAddress({ - env: { XDG_RUNTIME_DIR: "/tmp/stale-runtime" }, - uid: 1000, - exists: (path) => path === "/run/user/1000/bus", - }); - - assert.equal(busPath, "unix:path=/run/user/1000/bus"); - }); - it.effect("logs command failures with safe probe context and the exact cause", () => { const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index 5c6b67fd58c015f72f55f884dce7f5d06759bfc3..704b061db36357657edf8a5551d4a7d2174ad916 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -122,30 +122,6 @@ const linuxRuntimeDirCandidates = ( return candidates.filter((candidate) => candidate.length > 0); }; -function resolveDefaultLinuxDbusSessionBusPath(input: { - readonly env: NodeJS.ProcessEnv; - readonly uid: number | undefined; - readonly exists?: (path: string) => boolean; -}): string | null { - for (const runtimeDir of linuxRuntimeDirCandidates(input.env, input.uid)) { - const busPath = `${runtimeDir}/bus`; - if (input.exists === undefined || input.exists(busPath)) { - return busPath; - } - } - - return null; -} - -export function resolveDefaultLinuxDbusSessionBusAddress(input: { - readonly env: NodeJS.ProcessEnv; - readonly exists: (path: string) => boolean; - readonly uid: number | undefined; -}): string | null { - const busPath = resolveDefaultLinuxDbusSessionBusPath(input); - return busPath !== null && input.exists(busPath) ? `unix:path=${busPath}` : null; -} - const pathComparisonKey = (entry: string, platform: NodeJS.Platform) => { const normalized = entry.trim().replace(/^"+|"+$/g, ""); return platform === "win32" ? normalized.toLowerCase() : normalized; diff --git a/apps/desktop/src/ssh/DesktopSshEnvironment.ts b/apps/desktop/src/ssh/DesktopSshEnvironment.ts index b0094f6867bc00c1599bb3c5ae55b0bef3e1e45b..b5e724084a21ad6c31bef45eebea4be7894c0ab8 100644 --- a/apps/desktop/src/ssh/DesktopSshEnvironment.ts +++ b/apps/desktop/src/ssh/DesktopSshEnvironment.ts @@ -45,10 +45,6 @@ export type DesktopSshEnvironmentOperationError = export type DesktopSshEnvironmentDiscoverError = SshHostDiscoveryError; -export type DesktopSshEnvironmentError = - | DesktopSshEnvironmentDiscoverError - | DesktopSshEnvironmentOperationError; - export class DesktopSshEnvironment extends Context.Service< DesktopSshEnvironment, { diff --git a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts index 120199da6486b4cb3bec724270c6c0e6e7c88e1d..5c0bf0998f40d29889761bec18b2139c9dcb9050 100644 --- a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts +++ b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts @@ -164,10 +164,6 @@ export type DesktopSshPasswordPromptResolveError = | DesktopSshPromptInvalidRequestIdError | DesktopSshPromptExpiredError; -export type DesktopSshPasswordPromptError = - | DesktopSshPasswordPromptRequestError - | DesktopSshPasswordPromptResolveError; - export const DesktopSshPasswordPromptCancellation = Schema.Union([ DesktopSshPromptCancelledError, DesktopSshPromptWindowClosedError, diff --git a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts index d705252ef588e20376d97b85981d545ebc68bc96..cef90b29874bd0a2046f4f26fae093607eb094c6 100644 --- a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts +++ b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts @@ -39,7 +39,6 @@ function makeElectronAppLayer( onMetricsRead(); return metrics; }), - isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), setDesktopName: () => Effect.void, setDockIcon: () => Effect.void, @@ -172,7 +171,7 @@ describe("DesktopTelemetryPublisher", () => { const nextSnapshotFiber = yield* Stream.runHead(publisher.changes).pipe(Effect.forkChild); yield* Effect.yieldNow; - yield* publisher.handleControl({ + yield* publisher.handleControlForSource("primary-backend", { version: 1, type: "setDiagnosticsDemand", enabled: true, @@ -188,7 +187,7 @@ describe("DesktopTelemetryPublisher", () => { type: "setDiagnosticsDemand", enabled: true, }); - yield* publisher.handleControl({ + yield* publisher.handleControlForSource("primary-backend", { version: 1, type: "setDiagnosticsDemand", enabled: false, @@ -308,7 +307,7 @@ describe("DesktopTelemetryPublisher", () => { Effect.forkChild, ); yield* Effect.yieldNow; - yield* publisher.handleControl({ + yield* publisher.handleControlForSource("primary-backend", { version: 1, type: "setHostPowerIntervals", activeIntervalMs: 7_000, @@ -369,7 +368,7 @@ describe("DesktopTelemetryPublisher", () => { Effect.forkChild, ); yield* Effect.yieldNow; - yield* publisher.handleControl({ + yield* publisher.handleControlForSource("primary-backend", { version: 1, type: "setDiagnosticsDemand", enabled: true, diff --git a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts index 8fd478c821e7bfaf993aab612ffed32896e26b13..18ea0a8380a327e376d0ac1d2fbe3eac93c448d7 100644 --- a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts +++ b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts @@ -63,7 +63,6 @@ export class DesktopTelemetryPublisher extends Context.Service< readonly latest: Effect.Effect>; readonly changes: Stream.Stream; readonly encoded: Stream.Stream; - readonly handleControl: (message: DesktopTelemetryControlMessage) => Effect.Effect; readonly handleControlForSource: ( sourceId: string, message: DesktopTelemetryControlMessage, @@ -365,8 +364,6 @@ export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () { : Queue.offer(sampleTriggers, undefined).pipe(Effect.asVoid), ), ); - const handleControl: DesktopTelemetryPublisher["Service"]["handleControl"] = (message) => - handleControlForSource("legacy", message); const snapshots = Stream.unwrap( Effect.gen(function* () { @@ -415,7 +412,6 @@ export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () { latest: Ref.get(latest), changes: Stream.fromPubSub(changes), encoded, - handleControl, handleControlForSource, removeControlSource, publishUpdateReport, diff --git a/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts b/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts index 0f4cb970da2c1a0fe0f29df786ea7f75431e65d5..1198429a17007614bb593fe8d42cc9a85a16c0f2 100644 --- a/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts @@ -57,7 +57,6 @@ function runRemoteUpdatesTest( latest: Effect.succeedNone, changes: Stream.empty, encoded: Stream.empty, - handleControl: () => Effect.void, handleControlForSource: () => Effect.void, removeControlSource: () => Effect.void, publishUpdateReport: (report) => diff --git a/apps/desktop/src/window/DesktopApplicationMenu.test.ts b/apps/desktop/src/window/DesktopApplicationMenu.test.ts index 4b06f5ee510e58b8a53b3ae8c9c1e70f956a809c..5f5cbaa1d7fc454cfa06b80bb477c1581ec356b5 100644 --- a/apps/desktop/src/window/DesktopApplicationMenu.test.ts +++ b/apps/desktop/src/window/DesktopApplicationMenu.test.ts @@ -41,7 +41,6 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, { setAboutPanelOptions: () => Effect.void, setAppUserModelId: () => Effect.void, getAppMetrics: Effect.succeed([]), - isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), setDesktopName: () => Effect.void, setDockIcon: () => Effect.void, diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index 9b36d60f94ec842d79b1f925d0b947f0eb54dd39..74111988f15007e7dade5921837102e3cb95863e 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -2001,19 +2001,6 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { ) } - private func fileStatusText(_ changeType: String?) -> String { - switch changeType { - case "new": - return "A" - case "deleted": - return "D" - case "renamed": - return "R" - default: - return "" - } - } - private func fileStatusColor(_ changeType: String?) -> UIColor { switch changeType { case "new": @@ -2027,13 +2014,6 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { } } - private func drawStatusPill(_ text: String, rect: CGRect, color: UIColor, font: UIFont) { - let path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height / 2) - color.withAlphaComponent(0.12).setFill() - path.fill() - drawCenteredText(text, rect: rect, color: color, font: font) - } - private func drawFileIcon(rect: CGRect, changeType: String?) { let color = fileStatusColor(changeType) let outerPath = UIBezierPath(roundedRect: rect, cornerRadius: 6) diff --git a/apps/mobile/src/features/cloud/linkEnvironment.test.ts b/apps/mobile/src/features/cloud/linkEnvironment.test.ts index feadf6c818933e2b54bd39b2e0568f769c57124c..8fa346c5be4599856e59b105c7eaf0812d8de7e1 100644 --- a/apps/mobile/src/features/cloud/linkEnvironment.test.ts +++ b/apps/mobile/src/features/cloud/linkEnvironment.test.ts @@ -4,20 +4,13 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { EnvironmentId } from "@t3tools/contracts"; import { RelayMobileClientId } from "@t3tools/contracts/relay"; -import { DPOP_UNKNOWN_HINT, ManagedRelay } from "@t3tools/client-runtime/relay"; +import { ManagedRelay } from "@t3tools/client-runtime/relay"; import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc"; import { HttpClient } from "effect/unstable/http"; -import { MobilePreferencesStore } from "../../persistence/mobile-preferences"; + import { MobileStorage } from "../../persistence/mobile-storage"; -import { - linkEnvironmentToCloud, - linkEnvironmentToCloudWithPreference, - connectCloudEnvironment, - listCloudEnvironments, - listCloudEnvironmentsWithStatus, - refreshCloudEnvironmentConnection, -} from "./linkEnvironment"; +import { linkEnvironmentToCloudWithPreference } from "./linkEnvironment"; vi.mock("expo-constants", () => ({ default: { @@ -56,8 +49,6 @@ vi.mock("expo-secure-store", () => ({ setItemAsync: vi.fn(), })); -const loadPreferences = vi.fn(() => Effect.succeed({})); - const savedConnection = { environmentId: EnvironmentId.make("env-1"), environmentLabel: "Desktop", @@ -68,8 +59,6 @@ const savedConnection = { bearerToken: "local-bearer", }; -const stableClerkToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyXzEyMyJ9.test"; - const createProofMock = vi.fn( (input: { readonly method: string; readonly url: string; readonly accessToken?: string }) => Effect.succeed(`dpop:${input.method}:${input.url}`), @@ -86,14 +75,6 @@ function cloudClientLayer() { const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init)); return Layer.mergeAll( httpClientLayer, - Layer.succeed( - MobilePreferencesStore, - MobilePreferencesStore.of({ - load: loadPreferences(), - savePatch: (patch) => Effect.succeed(patch), - update: () => Effect.succeed({}), - }), - ), Layer.succeed( MobileStorage, MobileStorage.of({ @@ -123,7 +104,6 @@ const withCloudServices = ( | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner - | MobilePreferencesStore | MobileStorage >, ) => effect.pipe(Effect.provide(cloudClientLayer())); @@ -163,439 +143,12 @@ function requestBodyText(body: BodyInit | null | undefined): string { return body instanceof Uint8Array ? new TextDecoder().decode(body) : String(body ?? ""); } -function validDpopAccessTokenResponse(scope = "environment:status environment:connect") { - return { - access_token: "relay-dpop-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 300, - scope, - }; -} - -function listedEnvironment(environmentId: string) { - return { - environmentId: EnvironmentId.make(environmentId), - label: "Desktop", - endpoint: { - httpBaseUrl: `https://${environmentId}.example.test/`, - wsBaseUrl: `wss://${environmentId}.example.test/ws`, - providerKind: "cloudflare_tunnel" as const, - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }; -} - describe("mobile cloud link environment client", () => { beforeEach(() => { vi.restoreAllMocks(); createProofMock.mockClear(); - loadPreferences.mockClear(); }); - it.effect("decodes relay environment list responses before returning records", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn(() => - Promise.resolve( - Response.json({ - environments: [ - { - environmentId: "env-1", - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - ], - }), - ), - ), - ); - - const records = yield* withCloudServices( - listCloudEnvironments({ clerkToken: "clerk-token" }), - ); - expect(records).toEqual([ - { - environmentId: "env-1", - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - ]); - }), - ); - - it.effect("rejects malformed relay environment list responses", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn(() => - Promise.resolve( - Response.json({ - environments: [ - { - environmentId: "env-1", - label: "Desktop", - endpoint: { - httpBaseUrl: "", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - ], - }), - ), - ), - ); - - const error = yield* withCloudServices( - listCloudEnvironments({ clerkToken: "clerk-token" }), - ).pipe(Effect.flip); - expect(error).toMatchObject({ - _tag: "CloudEnvironmentLinkError", - message: "https://relay.example.test/v1/environments failed", - }); - }), - ); - - it.effect("loads signed status for each advertised cloud environment", () => - Effect.gen(function* () { - const fetchMock = vi.fn((url: string | URL, _init?: RequestInit) => { - if (String(url) === "https://relay.example.test/v1/environments") { - return Promise.resolve( - Response.json({ - environments: [ - { - environmentId: "env-1", - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - ], - }), - ); - } - if (String(url) === "https://relay.example.test/v1/client/dpop-token") { - return Promise.resolve(Response.json(validDpopAccessTokenResponse())); - } - expect(String(url)).toBe("https://relay.example.test/v1/environments/env-1/status"); - 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 }, - }, - }), - ); - }); - vi.stubGlobal("fetch", fetchMock); - - const records = yield* withCloudServices( - listCloudEnvironmentsWithStatus({ clerkToken: "clerk-token" }), - ); - expect(records).toMatchObject([ - { - environment: { - environmentId: "env-1", - label: "Desktop", - }, - status: { - environmentId: "env-1", - status: "online", - checkedAt: "2026-05-25T00:01:00.000Z", - }, - statusError: null, - }, - ]); - expect(String(fetchMock.mock.calls[2]?.[0])).toBe( - "https://relay.example.test/v1/environments/env-1/status", - ); - expect(fetchMock.mock.calls[2]?.[1]?.method).toBe("POST"); - const statusHeaders = new Headers(fetchMock.mock.calls[2]?.[1]?.headers); - expect(statusHeaders.get("authorization")).toBe("DPoP relay-dpop-token"); - expect(statusHeaders.get("dpop")).toBe( - "dpop:POST:https://relay.example.test/v1/environments/env-1/status", - ); - expect(createProofMock).toHaveBeenCalledWith({ - method: "POST", - url: "https://relay.example.test/v1/environments/env-1/status", - accessToken: "relay-dpop-token", - }); - }), - ); - - it.effect("reuses one valid DPoP access token while probing multiple environment statuses", () => - Effect.gen(function* () { - const fetchMock = vi.fn((url: string | URL, _init?: RequestInit) => { - if (String(url).endsWith("/v1/environments")) { - return Promise.resolve( - Response.json({ - environments: [listedEnvironment("env-1"), listedEnvironment("env-2")], - }), - ); - } - if (String(url).endsWith("/v1/client/dpop-token")) { - return Promise.resolve(Response.json(validDpopAccessTokenResponse())); - } - const environmentId = String(url).includes("/env-1/") ? "env-1" : "env-2"; - return Promise.resolve( - Response.json({ - environmentId, - endpoint: listedEnvironment(environmentId).endpoint, - status: "online", - checkedAt: "2026-05-25T00:01:00.000Z", - descriptor: { - environmentId, - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }, - }), - ); - }); - vi.stubGlobal("fetch", fetchMock); - - yield* withCloudServices(listCloudEnvironmentsWithStatus({ clerkToken: stableClerkToken })); - - expect( - fetchMock.mock.calls.filter(([url]) => String(url).endsWith("/v1/client/dpop-token")), - ).toHaveLength(1); - }), - ); - - it.effect("reuses the status-and-connect token when connecting from the cloud list", () => - Effect.gen(function* () { - const fetchMock = vi.fn((url: string | URL, _init?: RequestInit) => { - if (String(url).endsWith("/v1/environments")) { - return Promise.resolve( - Response.json({ - environments: [listedEnvironment("env-1")], - }), - ); - } - if (String(url).endsWith("/v1/client/dpop-token")) { - return Promise.resolve(Response.json(validDpopAccessTokenResponse())); - } - if (String(url).endsWith("/status")) { - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: listedEnvironment("env-1").endpoint, - 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 }, - }, - }), - ); - } - if (String(url).endsWith("/connect")) { - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: listedEnvironment("env-1").endpoint, - credential: "one-time-cloud-credential", - expiresAt: "2026-05-25T00:05:00.000Z", - }), - ); - } - if (String(url).endsWith("/.well-known/t3/environment")) { - return Promise.resolve( - Response.json({ - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }), - ); - } - return Promise.resolve( - Response.json({ - access_token: "environment-dpop-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 3600, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }), - ); - }); - vi.stubGlobal("fetch", fetchMock); - - yield* withCloudServices( - Effect.gen(function* () { - const records = yield* listCloudEnvironmentsWithStatus({ - clerkToken: stableClerkToken, - }); - yield* connectCloudEnvironment({ - clerkToken: stableClerkToken, - environment: records[0]!.environment, - }); - }), - ); - - expect( - fetchMock.mock.calls.filter(([url]) => String(url).endsWith("/v1/client/dpop-token")), - ).toHaveLength(1); - const exchangeRequest = fetchMock.mock.calls.find(([url]) => - String(url).endsWith("/v1/client/dpop-token"), - )?.[1]; - expect(new URLSearchParams(requestBodyText(exchangeRequest?.body)).get("scope")).toBe( - "environment:status environment:connect", - ); - const environmentTokenRequest = fetchMock.mock.calls.find(([url]) => - String(url).endsWith("/oauth/token"), - )?.[1]; - const environmentTokenBody = new URLSearchParams( - requestBodyText(environmentTokenRequest?.body), - ); - expect(environmentTokenBody.get("client_label")).toBe("T3 Code Mobile"); - expect(environmentTokenBody.get("client_device_type")).toBe("mobile"); - expect(environmentTokenBody.get("client_os")).toBe("iOS"); - }), - ); - - it.effect("keeps advertised environments visible when status probing fails", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => { - if (String(url) === "https://relay.example.test/v1/environments") { - return Promise.resolve( - Response.json({ - environments: [ - { - environmentId: "env-1", - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - ], - }), - ); - } - if (String(url) === "https://relay.example.test/v1/client/dpop-token") { - return Promise.resolve(Response.json(validDpopAccessTokenResponse())); - } - return Promise.resolve(Response.json({ error: "offline" }, { status: 503 })); - }), - ); - - const records = yield* withCloudServices( - listCloudEnvironmentsWithStatus({ clerkToken: "clerk-token" }), - ); - expect(records).toMatchObject([ - { - environment: { - environmentId: "env-1", - label: "Desktop", - }, - status: null, - statusError: "https://relay.example.test/v1/environments/env-1/status failed", - }, - ]); - }), - ); - - it.effect("rejects status responses for a different advertised environment", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => { - if (String(url) === "https://relay.example.test/v1/environments") { - return Promise.resolve( - Response.json({ - environments: [ - { - environmentId: "env-1", - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - ], - }), - ); - } - if (String(url) === "https://relay.example.test/v1/client/dpop-token") { - return Promise.resolve(Response.json(validDpopAccessTokenResponse())); - } - return Promise.resolve( - Response.json({ - environmentId: "env-other", - 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-other", - label: "Other Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }, - }), - ); - }), - ); - - const records = yield* withCloudServices( - listCloudEnvironmentsWithStatus({ clerkToken: "clerk-token" }), - ); - expect(records).toMatchObject([ - { - environment: { - environmentId: "env-1", - label: "Desktop", - }, - status: null, - statusError: "Relay returned status for a different environment.", - }, - ]); - }), - ); - it.effect( "rejects relay link credentials for a different environment before persisting relay config", () => @@ -612,9 +165,10 @@ describe("mobile cloud link environment client", () => { vi.stubGlobal("fetch", fetchMock); const error = yield* withCloudServices( - linkEnvironmentToCloud({ + linkEnvironmentToCloudWithPreference({ clerkToken: "clerk-token", connection: savedConnection, + liveActivitiesEnabled: true, }), ).pipe(Effect.flip); expect(error).toMatchObject({ @@ -644,9 +198,10 @@ describe("mobile cloud link environment client", () => { vi.stubGlobal("fetch", fetchMock); const error = yield* withCloudServices( - linkEnvironmentToCloud({ + linkEnvironmentToCloudWithPreference({ clerkToken: "clerk-token", connection: savedConnection, + liveActivitiesEnabled: true, }), ).pipe(Effect.flip); expect(error._tag).toBe("CloudEnvironmentLinkError"); @@ -681,9 +236,10 @@ describe("mobile cloud link environment client", () => { vi.stubGlobal("fetch", fetchMock); const error = yield* withCloudServices( - linkEnvironmentToCloud({ + linkEnvironmentToCloudWithPreference({ clerkToken: "clerk-token", connection: savedConnection, + liveActivitiesEnabled: true, }), ).pipe(Effect.flip); expect(error).toMatchObject({ @@ -718,9 +274,10 @@ describe("mobile cloud link environment client", () => { vi.stubGlobal("fetch", fetchMock); const error = yield* withCloudServices( - linkEnvironmentToCloud({ + linkEnvironmentToCloudWithPreference({ clerkToken: "clerk-token", connection: savedConnection, + liveActivitiesEnabled: true, }), ).pipe(Effect.flip); expect(error).toMatchObject({ @@ -733,7 +290,6 @@ describe("mobile cloud link environment client", () => { it.effect("preserves disabled Live Activity preferences when linking an environment", () => Effect.gen(function* () { - loadPreferences.mockReturnValueOnce(Effect.succeed({ liveActivitiesEnabled: false })); const bodies: Array = []; const fetchMock = vi.fn((url: string | URL, init?: RequestInit) => { if (init?.body) { @@ -756,9 +312,10 @@ describe("mobile cloud link environment client", () => { vi.stubGlobal("fetch", fetchMock); yield* withCloudServices( - linkEnvironmentToCloud({ + linkEnvironmentToCloudWithPreference({ clerkToken: "clerk-token", connection: savedConnection, + liveActivitiesEnabled: false, }), ); @@ -786,9 +343,8 @@ describe("mobile cloud link environment client", () => { }), ); - it.effect("uses an explicit Live Activity preference when persisted state is unavailable", () => + it.effect("enables Live Activities for both the link challenge and registration", () => Effect.gen(function* () { - loadPreferences.mockReturnValueOnce(Effect.die("persisted preferences must not be read")); const bodies: Array> = []; const fetchMock = vi.fn((url: string | URL, init?: RequestInit) => { if (init?.body) { @@ -824,429 +380,4 @@ describe("mobile cloud link environment client", () => { ]); }), ); - - it.effect( - "does not persist cloud connect bootstrap credentials in saved connection records", - () => - Effect.gen(function* () { - let connectRequestBody = ""; - const fetchMock = vi.fn((url: string | URL, init?: RequestInit) => { - if (String(url).endsWith("/v1/client/dpop-token")) { - return Promise.resolve( - Response.json(validDpopAccessTokenResponse("environment:connect")), - ); - } - if (String(url).endsWith("/.well-known/t3/environment")) { - return Promise.resolve( - Response.json({ - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }), - ); - } - if (String(url).endsWith("/oauth/token")) { - return Promise.resolve( - Response.json({ - access_token: "environment-dpop-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 3600, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }), - ); - } - connectRequestBody = requestBodyText(init?.body); - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - credential: "one-time-cloud-credential", - expiresAt: "2026-05-25T00:05:00.000Z", - }), - ); - }); - vi.stubGlobal("fetch", fetchMock); - - const connection = yield* withCloudServices( - connectCloudEnvironment({ - clerkToken: "clerk-token", - environment: { - environmentId: EnvironmentId.make("env-1"), - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - }), - ); - - expect(connection.pairingUrl).toBe("https://desktop.example.test/"); - expect(connection.pairingUrl).not.toContain("one-time-cloud-credential"); - expect(connection.bearerToken).toBeNull(); - expect(connection.authenticationMethod).toBe("dpop"); - expect(connection.dpopAccessToken).toBe("environment-dpop-token"); - expect(connection.relayManaged).toBe(true); - // @effect-diagnostics-next-line preferSchemaOverJson:off - expect(JSON.parse(connectRequestBody)).toMatchObject({ - deviceId: "device-1", - clientKeyThumbprint: "client-proof-key-thumbprint", - }); - expect(createProofMock).toHaveBeenCalledWith({ - method: "POST", - url: "https://relay.example.test/v1/environments/env-1/connect", - accessToken: "relay-dpop-token", - }); - expect(createProofMock).toHaveBeenCalledWith({ - method: "POST", - url: "https://desktop.example.test/oauth/token", - }); - }), - ); - - it.effect("refreshes a saved environment against a rotated managed endpoint", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => { - if (String(url).endsWith("/v1/client/dpop-token")) { - return Promise.resolve( - Response.json(validDpopAccessTokenResponse("environment:connect")), - ); - } - if (String(url).endsWith("/.well-known/t3/environment")) { - return Promise.resolve( - Response.json({ - environmentId: "env-1", - label: "Rotated Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }), - ); - } - if (String(url).endsWith("/oauth/token")) { - return Promise.resolve( - Response.json({ - access_token: "fresh-environment-dpop-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 3600, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }), - ); - } - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://rotated-desktop.example.test/", - wsBaseUrl: "wss://rotated-desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - credential: "rotated-one-time-cloud-credential", - expiresAt: "2026-05-25T00:05:00.000Z", - }), - ); - }), - ); - - const connection = yield* withCloudServices( - refreshCloudEnvironmentConnection({ - clerkToken: "clerk-token", - connection: { - environmentId: EnvironmentId.make("env-1"), - environmentLabel: "Desktop", - pairingUrl: "https://desktop.example.test/", - displayUrl: "https://desktop.example.test/", - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - bearerToken: null, - authenticationMethod: "dpop", - relayManaged: true, - }, - }), - ); - - expect(connection).toMatchObject({ - environmentId: "env-1", - environmentLabel: "Rotated Desktop", - displayUrl: "https://rotated-desktop.example.test/", - httpBaseUrl: "https://rotated-desktop.example.test/", - wsBaseUrl: "wss://rotated-desktop.example.test/ws", - dpopAccessToken: "fresh-environment-dpop-token", - }); - }), - ); - - it.effect("rejects relay connect responses for a different environment", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => - Promise.resolve( - String(url).endsWith("/v1/client/dpop-token") - ? Response.json(validDpopAccessTokenResponse("environment:connect")) - : Response.json({ - environmentId: "env-other", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - credential: "one-time-cloud-credential", - expiresAt: "2026-05-25T00:05:00.000Z", - }), - ), - ), - ); - - const error = yield* withCloudServices( - connectCloudEnvironment({ - clerkToken: "clerk-token", - environment: { - environmentId: EnvironmentId.make("env-1"), - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - }), - ).pipe(Effect.flip); - expect(error).toMatchObject({ - _tag: "CloudEnvironmentLinkError", - message: "Relay returned credentials for a different environment.", - }); - }), - ); - - it.effect("preserves relay DPoP auth failures while connecting environments", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => - Promise.resolve( - String(url).endsWith("/v1/client/dpop-token") - ? Response.json(validDpopAccessTokenResponse("environment:connect")) - : Response.json( - { - _tag: "RelayAuthInvalidError", - code: "auth_invalid", - reason: "invalid_dpop", - traceId: "trace-connect", - }, - { status: 401 }, - ), - ), - ), - ); - - const error = yield* withCloudServices( - connectCloudEnvironment({ - clerkToken: "clerk-token", - environment: { - environmentId: EnvironmentId.make("env-1"), - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - }), - ).pipe(Effect.flip); - expect(error).toMatchObject({ - _tag: "CloudEnvironmentLinkError", - message: `https://relay.example.test/v1/environments/env-1/connect failed: Relay rejected the DPoP proof. ${DPOP_UNKNOWN_HINT}`, - traceId: "trace-connect", - }); - }), - ); - - it.effect( - "presents clock skew as one possible cause when an older environment rejects DPoP", - () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => { - const value = String(url); - if (value.endsWith("/v1/client/dpop-token")) { - return Promise.resolve( - Response.json(validDpopAccessTokenResponse("environment:connect")), - ); - } - if (value.endsWith("/v1/environments/env-1/connect")) { - return Promise.resolve( - Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - credential: "one-time-cloud-credential", - expiresAt: "2026-05-25T00:05:00.000Z", - }), - ); - } - if (value.endsWith("/.well-known/t3/environment")) { - return Promise.resolve( - Response.json({ - environmentId: "env-1", - label: "Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }), - ); - } - return Promise.resolve( - Response.json( - { - _tag: "EnvironmentAuthInvalidError", - code: "auth_invalid", - reason: "invalid_credential", - traceId: "trace-environment", - }, - { status: 401 }, - ), - ); - }), - ); - - const error = yield* withCloudServices( - connectCloudEnvironment({ - clerkToken: "clerk-token", - environment: { - environmentId: EnvironmentId.make("env-1"), - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - }), - ).pipe(Effect.flip); - - expect(error).toMatchObject({ - _tag: "CloudEnvironmentLinkError", - message: `Could not exchange a managed endpoint DPoP access token. ${DPOP_UNKNOWN_HINT}`, - traceId: "trace-environment", - }); - }), - ); - - it.effect("rejects relay connect responses for a different endpoint", () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => - Promise.resolve( - String(url).endsWith("/v1/client/dpop-token") - ? Response.json(validDpopAccessTokenResponse("environment:connect")) - : Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://other-desktop.example.test/", - wsBaseUrl: "wss://other-desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - credential: "one-time-cloud-credential", - expiresAt: "2026-05-25T00:05:00.000Z", - }), - ), - ), - ); - - const error = yield* withCloudServices( - connectCloudEnvironment({ - clerkToken: "clerk-token", - environment: { - environmentId: EnvironmentId.make("env-1"), - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - }), - ).pipe(Effect.flip); - expect(error).toMatchObject({ - _tag: "CloudEnvironmentLinkError", - message: "Relay returned credentials for a different endpoint.", - }); - }), - ); - - it.effect( - "rejects managed endpoints whose descriptor does not match the selected environment", - () => - Effect.gen(function* () { - vi.stubGlobal( - "fetch", - vi.fn((url: string | URL) => - Promise.resolve( - String(url).endsWith("/v1/client/dpop-token") - ? Response.json(validDpopAccessTokenResponse("environment:connect")) - : String(url).endsWith("/.well-known/t3/environment") - ? Response.json({ - environmentId: "env-other", - label: "Other Desktop", - platform: { os: "darwin", arch: "arm64" }, - serverVersion: "0.0.0-test", - capabilities: { repositoryIdentity: true }, - }) - : Response.json({ - environmentId: "env-1", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - credential: "one-time-cloud-credential", - expiresAt: "2026-05-25T00:05:00.000Z", - }), - ), - ), - ); - - const error = yield* withCloudServices( - connectCloudEnvironment({ - clerkToken: "clerk-token", - environment: { - environmentId: EnvironmentId.make("env-1"), - label: "Desktop", - endpoint: { - httpBaseUrl: "https://desktop.example.test/", - wsBaseUrl: "wss://desktop.example.test/ws", - providerKind: "cloudflare_tunnel", - }, - linkedAt: "2026-05-25T00:00:00.000Z", - }, - }), - ).pipe(Effect.flip); - expect(error).toMatchObject({ - _tag: "CloudEnvironmentLinkError", - message: "Connected endpoint descriptor does not match the selected environment.", - }); - }), - ); }); diff --git a/apps/mobile/src/features/cloud/linkEnvironment.ts b/apps/mobile/src/features/cloud/linkEnvironment.ts index b8dc8f878e0ccbc2e002cc447ae90ec8b5c3e00b..73bd28c550edafe4f879d348efa48279fcddee8d 100644 --- a/apps/mobile/src/features/cloud/linkEnvironment.ts +++ b/apps/mobile/src/features/cloud/linkEnvironment.ts @@ -4,45 +4,24 @@ import * as Schema from "effect/Schema"; import { HttpClient } from "effect/unstable/http"; import { EnvironmentCloudEndpointUnavailableError, - EnvironmentAuthInvalidError, EnvironmentHttpBadRequestError, EnvironmentHttpConflictError, EnvironmentHttpForbiddenError, EnvironmentHttpInternalServerError, EnvironmentHttpUnauthorizedError, } from "@t3tools/contracts"; -import { stripPairingTokenFromUrl } from "@t3tools/shared/remote"; import { - type RelayEnvironmentConnectResponse as RelayEnvironmentConnectResponseType, type RelayEnvironmentLinkResponse as RelayEnvironmentLinkResponseType, - RelayEnvironmentConnectScope, - RelayEnvironmentStatusScope, - type RelayDpopAccessTokenScope, - type RelayClientEnvironmentRecord, - type RelayEnvironmentStatusResponse as RelayEnvironmentStatusResponseType, type RelayManagedEndpointProviderKind, } from "@t3tools/contracts/relay"; -import { exchangeRemoteDpopAccessToken } from "@t3tools/client-runtime/authorization"; -import { fetchRemoteEnvironmentDescriptor } from "@t3tools/client-runtime/environment"; import { findErrorTraceId } from "@t3tools/client-runtime/errors"; -import { - dpopFailureMessage, - ManagedRelay, - relayProtectedErrorMessage, -} from "@t3tools/client-runtime/relay"; +import { ManagedRelay, relayProtectedErrorMessage } from "@t3tools/client-runtime/relay"; import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc"; -import { authClientMetadata } from "../../lib/authClientMetadata"; import type { SavedRemoteConnection } from "../../lib/connection"; -import * as MobilePreferences from "../../persistence/mobile-preferences"; import * as MobileStorage from "../../persistence/mobile-storage"; import { resolveCloudPublicConfig } from "./publicConfig"; -const RELAY_STATUS_AND_CONNECT_SCOPES = [ - RelayEnvironmentStatusScope, - RelayEnvironmentConnectScope, -] satisfies ReadonlyArray; - function readRelayUrl(): string | null { return resolveCloudPublicConfig().relay.url; } @@ -53,12 +32,6 @@ export class CloudEnvironmentLinkError extends Data.TaggedError("CloudEnvironmen readonly traceId?: string; }> {} -export interface CloudEnvironmentRecordWithStatus { - readonly environment: RelayClientEnvironmentRecord; - readonly status: RelayEnvironmentStatusResponseType | null; - readonly statusError: string | null; -} - const isEnvironmentCloudApiError = Schema.is( Schema.Union([ EnvironmentHttpBadRequestError, @@ -69,24 +42,19 @@ const isEnvironmentCloudApiError = Schema.is( EnvironmentCloudEndpointUnavailableError, ]), ); -const isEnvironmentAuthInvalidError = Schema.is(EnvironmentAuthInvalidError); const MANAGED_ENDPOINT_PROVIDER_KIND = "cloudflare_tunnel" satisfies RelayManagedEndpointProviderKind; -function cloudEnvironmentLinkError(message: string, options?: { readonly dpop?: boolean }) { +function cloudEnvironmentLinkError(message: string) { return (cause: unknown) => { const environmentError = findEnvironmentCloudApiError(cause); const traceId = findErrorTraceId(cause); - const dpopAuthError = options?.dpop ? findEnvironmentAuthInvalidError(cause) : null; const detail = environmentError ? `${message.replace(/[.:]$/, "")}: ${environmentError.message}` : withDevCause(message, cause); return new CloudEnvironmentLinkError({ - message: - dpopAuthError?.reason === "invalid_credential" - ? dpopFailureMessage(detail, dpopAuthError.dpopFailureReason) - : detail, + message: detail, cause, ...(traceId === null ? {} : { traceId }), }); @@ -143,16 +111,6 @@ function findEnvironmentCloudApiError(cause: unknown): { readonly message: strin return "cause" in cause ? findEnvironmentCloudApiError(cause.cause) : null; } -function findEnvironmentAuthInvalidError(cause: unknown): EnvironmentAuthInvalidError | null { - if (isEnvironmentAuthInvalidError(cause)) { - return cause; - } - if (typeof cause !== "object" || cause === null) { - return null; - } - return "cause" in cause ? findEnvironmentAuthInvalidError(cause.cause) : null; -} - function requireRelayUrl(): Effect.Effect { const relayUrl = readRelayUrl(); return relayUrl @@ -186,54 +144,6 @@ function ensureLinkedEnvironmentMatches(input: { return Effect.void; } -function endpointMatches( - left: RelayClientEnvironmentRecord["endpoint"], - right: RelayClientEnvironmentRecord["endpoint"], -): boolean { - return ( - left.httpBaseUrl === right.httpBaseUrl && - left.wsBaseUrl === right.wsBaseUrl && - left.providerKind === right.providerKind - ); -} - -function ensureStatusMatchesEnvironment(input: { - readonly environment: RelayClientEnvironmentRecord; - readonly status: RelayEnvironmentStatusResponseType; -}): Effect.Effect { - if (input.status.environmentId !== input.environment.environmentId) { - return new CloudEnvironmentLinkError({ - message: "Relay returned status for a different environment.", - }); - } - if (!endpointMatches(input.status.endpoint, input.environment.endpoint)) { - return new CloudEnvironmentLinkError({ - message: "Relay returned status for a different endpoint.", - }); - } - if ( - input.status.descriptor && - input.status.descriptor.environmentId !== input.environment.environmentId - ) { - return new CloudEnvironmentLinkError({ - message: "Relay returned status descriptor for a different environment.", - }); - } - return Effect.void; -} - -function ensureConnectEndpointMatchesEnvironment(input: { - readonly environment: RelayClientEnvironmentRecord; - readonly connect: RelayEnvironmentConnectResponseType; -}): Effect.Effect { - if (!endpointMatches(input.connect.endpoint, input.environment.endpoint)) { - return new CloudEnvironmentLinkError({ - message: "Relay returned credentials for a different endpoint.", - }); - } - return Effect.void; -} - interface LinkEnvironmentToCloudInput { readonly connection: SavedRemoteConnection; readonly clerkToken: string; @@ -328,250 +238,3 @@ export function linkEnvironmentToCloudWithPreference( ); }); } - -export function linkEnvironmentToCloud( - input: LinkEnvironmentToCloudInput, -): Effect.Effect< - void, - CloudEnvironmentLinkError, - LinkEnvironmentToCloudRequirements | MobilePreferences.MobilePreferencesStore -> { - return MobilePreferences.MobilePreferencesStore.pipe( - Effect.flatMap((preferencesStore) => preferencesStore.load), - Effect.mapError(cloudEnvironmentLinkError("Could not load mobile notification preferences.")), - Effect.flatMap((preferences) => - linkEnvironmentToCloudWithPreference({ - ...input, - liveActivitiesEnabled: preferences.liveActivitiesEnabled !== false, - }), - ), - ); -} - -export function listCloudEnvironments(input: { - readonly clerkToken: string; -}): Effect.Effect< - ReadonlyArray, - CloudEnvironmentLinkError, - ManagedRelay.ManagedRelayClient -> { - return Effect.gen(function* () { - const relayUrl = yield* requireRelayUrl(); - const relayClient = yield* ManagedRelay.ManagedRelayClient; - - return yield* relayClient - .listEnvironments({ - clerkToken: input.clerkToken, - }) - .pipe(Effect.mapError(decodedRelayClientError(`${relayUrl}/v1/environments failed`))); - }); -} - -export function getCloudEnvironmentStatus(input: { - readonly clerkToken: string; - readonly environment: RelayClientEnvironmentRecord; - readonly relayScopes?: ReadonlyArray; -}): Effect.Effect< - RelayEnvironmentStatusResponseType, - CloudEnvironmentLinkError, - ManagedRelay.ManagedRelayClient -> { - return Effect.gen(function* () { - const relayUrl = yield* requireRelayUrl(); - const relayClient = yield* ManagedRelay.ManagedRelayClient; - const status = yield* relayClient - .getEnvironmentStatus({ - clerkToken: input.clerkToken, - scopes: input.relayScopes ?? [RelayEnvironmentStatusScope], - environmentId: input.environment.environmentId, - }) - .pipe( - Effect.mapError( - decodedRelayClientError( - `${relayUrl}/v1/environments/${encodeURIComponent(input.environment.environmentId)}/status failed`, - ), - ), - ); - yield* ensureStatusMatchesEnvironment({ environment: input.environment, status }); - return status; - }); -} - -export function loadCloudEnvironmentStatuses(input: { - readonly clerkToken: string; - readonly environments: ReadonlyArray; -}): Effect.Effect< - ReadonlyArray, - CloudEnvironmentLinkError, - ManagedRelay.ManagedRelayClient -> { - return Effect.forEach( - input.environments, - (environment) => - getCloudEnvironmentStatus({ - clerkToken: input.clerkToken, - environment, - relayScopes: RELAY_STATUS_AND_CONNECT_SCOPES, - }).pipe( - Effect.match({ - onFailure: (error) => ({ - environment, - status: null, - statusError: error.message, - }), - onSuccess: (status) => ({ - environment, - status, - statusError: null, - }), - }), - ), - { concurrency: "unbounded" }, - ); -} - -export function listCloudEnvironmentsWithStatus(input: { - readonly clerkToken: string; -}): Effect.Effect< - ReadonlyArray, - CloudEnvironmentLinkError, - ManagedRelay.ManagedRelayClient -> { - return Effect.gen(function* () { - const environments = yield* listCloudEnvironments(input); - return yield* loadCloudEnvironmentStatuses({ - clerkToken: input.clerkToken, - environments, - }); - }); -} - -const loadAgentAwarenessDeviceId = Effect.fn("mobile.cloud.loadAgentAwarenessDeviceId")( - function* () { - const storage = yield* MobileStorage.MobileStorage; - return yield* storage.loadOrCreateAgentAwarenessDeviceId.pipe( - Effect.mapError(cloudEnvironmentLinkError("Could not load the mobile device id.")), - ); - }, -); - -const connectRelayManagedEnvironment = Effect.fn("mobile.cloud.connectRelayManagedEnvironment")( - function* (input: { - readonly clerkToken: string; - readonly environmentId: RelayClientEnvironmentRecord["environmentId"]; - readonly expectedEnvironment?: RelayClientEnvironmentRecord; - }) { - yield* Effect.annotateCurrentSpan({ "environment.id": input.environmentId }); - const relayUrl = yield* requireRelayUrl(); - const relayClient = yield* ManagedRelay.ManagedRelayClient; - - const deviceId = yield* loadAgentAwarenessDeviceId(); - const connect = yield* relayClient - .connectEnvironment({ - clerkToken: input.clerkToken, - scopes: [RelayEnvironmentConnectScope], - environmentId: input.environmentId, - deviceId, - }) - .pipe( - Effect.mapError( - decodedRelayClientError( - `${relayUrl}/v1/environments/${encodeURIComponent(input.environmentId)}/connect failed`, - ), - ), - ); - if (connect.environmentId !== input.environmentId) { - return yield* new CloudEnvironmentLinkError({ - message: "Relay returned credentials for a different environment.", - }); - } - if (input.expectedEnvironment) { - yield* ensureConnectEndpointMatchesEnvironment({ - environment: input.expectedEnvironment, - connect, - }); - } - - const descriptor = yield* fetchRemoteEnvironmentDescriptor({ - httpBaseUrl: connect.endpoint.httpBaseUrl, - }).pipe( - Effect.mapError( - cloudEnvironmentLinkError("Could not fetch the connected environment descriptor."), - ), - ); - if (descriptor.environmentId !== connect.environmentId) { - return yield* new CloudEnvironmentLinkError({ - message: "Connected endpoint descriptor does not match the selected environment.", - }); - } - const signer = yield* ManagedRelay.ManagedRelayDpopSigner; - const bootstrapDpop = yield* signer - .createProof({ - method: "POST", - url: new URL("/oauth/token", connect.endpoint.httpBaseUrl).toString(), - }) - .pipe(Effect.mapError(cloudEnvironmentLinkError("Could not create bootstrap DPoP proof."))); - const bootstrap = yield* exchangeRemoteDpopAccessToken({ - httpBaseUrl: connect.endpoint.httpBaseUrl, - credential: connect.credential, - dpopProof: bootstrapDpop, - clientMetadata: authClientMetadata(), - }).pipe( - Effect.mapError( - cloudEnvironmentLinkError("Could not exchange a managed endpoint DPoP access token.", { - dpop: true, - }), - ), - ); - const pairingUrl = new URL(connect.endpoint.httpBaseUrl); - pairingUrl.hash = new URLSearchParams([["token", connect.credential]]).toString(); - - return { - environmentId: descriptor.environmentId, - environmentLabel: descriptor.label, - pairingUrl: stripPairingTokenFromUrl(pairingUrl).toString(), - displayUrl: connect.endpoint.httpBaseUrl, - httpBaseUrl: connect.endpoint.httpBaseUrl, - wsBaseUrl: connect.endpoint.wsBaseUrl, - bearerToken: null, - authenticationMethod: "dpop", - dpopAccessToken: bootstrap.access_token, - relayManaged: true, - } satisfies SavedRemoteConnection; - }, -); - -export function connectCloudEnvironment(input: { - readonly clerkToken: string; - readonly environment: RelayClientEnvironmentRecord; -}): Effect.Effect< - SavedRemoteConnection, - CloudEnvironmentLinkError, - | HttpClient.HttpClient - | ManagedRelay.ManagedRelayClient - | ManagedRelay.ManagedRelayDpopSigner - | MobileStorage.MobileStorage -> { - return connectRelayManagedEnvironment({ - clerkToken: input.clerkToken, - environmentId: input.environment.environmentId, - expectedEnvironment: input.environment, - }); -} - -export function refreshCloudEnvironmentConnection(input: { - readonly clerkToken: string; - readonly connection: SavedRemoteConnection; -}): Effect.Effect< - SavedRemoteConnection, - CloudEnvironmentLinkError, - | HttpClient.HttpClient - | ManagedRelay.ManagedRelayClient - | ManagedRelay.ManagedRelayDpopSigner - | MobileStorage.MobileStorage -> { - return connectRelayManagedEnvironment({ - clerkToken: input.clerkToken, - environmentId: input.connection.environmentId, - }); -} diff --git a/apps/mobile/src/features/review/reviewPerf.ts b/apps/mobile/src/features/review/reviewPerf.ts index 2ae1095b1c6a472534571bf2538f3cea950da083..2613ff0c3dbbc910ed98326015969e581099d8f2 100644 --- a/apps/mobile/src/features/review/reviewPerf.ts +++ b/apps/mobile/src/features/review/reviewPerf.ts @@ -42,33 +42,6 @@ export function measureReviewWork(name: string, callback: () => T): T { } } -export async function measureReviewAsyncWork( - name: string, - callback: () => Promise, -): Promise { - if (!isReviewPerfEnabled()) { - return callback(); - } - - const perf = getPerformance(); - const marker = `${REVIEW_PERF_PREFIX}.${name}.${reviewPerfSequence++}`; - const startMark = `${marker}.start`; - const endMark = `${marker}.end`; - const startedAt = perf?.now?.() ?? Date.now(); - - perf?.mark?.(startMark); - try { - return await callback(); - } finally { - const durationMs = (perf?.now?.() ?? Date.now()) - startedAt; - perf?.mark?.(endMark); - perf?.measure?.(`${REVIEW_PERF_PREFIX}.${name}`, startMark, endMark); - perf?.clearMarks?.(startMark); - perf?.clearMarks?.(endMark); - console.log(`[review-perf] ${name}`, { durationMs: Number(durationMs.toFixed(2)) }); - } -} - export function markReviewEvent(name: string, details?: Record): void { if (!isReviewPerfEnabled()) { return; diff --git a/apps/mobile/src/features/review/shikiReviewHighlighter.ts b/apps/mobile/src/features/review/shikiReviewHighlighter.ts index 52e0b83be34d632de270510d2b722c1be0b75d9f..3050f1f67ee8e0eb7fd39c437716d5af702fcb0d 100644 --- a/apps/mobile/src/features/review/shikiReviewHighlighter.ts +++ b/apps/mobile/src/features/review/shikiReviewHighlighter.ts @@ -15,14 +15,12 @@ import * as Schema from "effect/Schema"; import { resolveReviewHighlighterEngine, resolveReviewHighlighterEnginePreference, - type ReviewHighlighterEngine, } from "./reviewHighlighterEngine"; import { createIncrementalSnippet } from "./incrementalSnippet"; import type { ReviewRenderableLineRow } from "./reviewModel"; import { applyDiffRangesToTokens, computeWordAltDiffRanges } from "./reviewWordDiffs"; export type ReviewDiffTheme = "light" | "dark"; -export type { ReviewHighlighterEngine }; export class ReviewHighlighterEngineInitializationError extends Schema.TaggedError()( "ReviewHighlighterEngineInitializationError", @@ -181,7 +179,6 @@ const languageAliases: Record = { txt: "text", }; let highlighterPromise: Promise | null = null; -let activeHighlighterEnginePromise: Promise | null = null; type LoadedLanguageModule = { default: Parameters[0]; @@ -254,10 +251,7 @@ async function getHighlighter(): Promise { engine: nativeEngineModule.createNativeEngine(), }); logReviewHighlighterDiagnostic("using native engine"); - return { - highlighter, - engine: "native" as const, - }; + return highlighter; } } catch (error) { nativeInitializationError = new ReviewHighlighterEngineInitializationError({ @@ -308,55 +302,18 @@ async function getHighlighter(): Promise { logReviewHighlighterDiagnostic("using javascript engine", { resolvedEngine: engine, }); - return { - highlighter, - engine, - }; + return highlighter; })(); - highlighterPromise = configuredHighlighterPromise - .then((result) => result.highlighter) - .catch((error) => { - highlighterPromise = null; - activeHighlighterEnginePromise = null; - throw error; - }); - activeHighlighterEnginePromise = configuredHighlighterPromise - .then((result) => result.engine) - .catch((error) => { - activeHighlighterEnginePromise = null; - throw error; - }); + highlighterPromise = configuredHighlighterPromise.catch((error) => { + highlighterPromise = null; + throw error; + }); } return highlighterPromise; } -export async function getActiveReviewHighlighterEngine(): Promise { - await getHighlighter(); - return activeHighlighterEnginePromise ?? Promise.resolve("javascript"); -} - -export async function prepareReviewHighlighter(): Promise { - await getHighlighter(); -} - -export async function prepareReviewHighlighterLanguages( - languages: ReadonlyArray, -): Promise { - const highlighter = await getHighlighter(); - await Promise.all( - languages.map(async (language) => { - const candidate = resolveLanguageAlias(language); - if (candidate === "text" || !(candidate in languageImports)) { - return; - } - - await loadSingleLanguage(highlighter, candidate); - }), - ); -} - function resolveLanguageAlias(language: string): string { const normalized = language.toLowerCase(); return languageAliases[normalized] ?? normalized; diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts index ace2b0fc43bf5b85148970fa24a982d14ed7b271..0df54be2f7018f633e917a21de9d73c918bcbfa5 100644 --- a/apps/server/integration/OrchestrationEngineHarness.integration.ts +++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts @@ -28,11 +28,9 @@ import * as CheckpointStore from "../src/checkpointing/CheckpointStore.ts"; import { TextGeneration } from "../src/textGeneration/TextGeneration.ts"; import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts"; import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; -import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts"; import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts"; import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts"; import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts"; -import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts"; import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts"; import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts"; import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts"; @@ -189,7 +187,6 @@ export interface OrchestrationIntegrationHarness { readonly snapshotQuery: ProjectionSnapshotQuery["Service"]; readonly providerService: ProviderService["Service"]; readonly checkpointStore: CheckpointStore.CheckpointStore["Service"]; - readonly checkpointRepository: ProjectionCheckpointRepository["Service"]; readonly pendingApprovalRepository: ProjectionPendingApprovalRepository["Service"]; readonly waitForThread: ( threadId: string, @@ -313,7 +310,6 @@ export const makeOrchestrationIntegrationHarness = ( const runtimeServicesLayer = Layer.mergeAll( projectionSnapshotQueryLayer, orchestrationLayer.pipe(Layer.provide(projectionSnapshotQueryLayer)), - ProjectionCheckpointRepositoryLive, ProjectionPendingApprovalRepositoryLive, checkpointStoreLayer, providerLayer, @@ -463,10 +459,6 @@ export const makeOrchestrationIntegrationHarness = ( const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () => runtime.runPromise(Effect.service(CheckpointStore.CheckpointStore)), ).pipe(Effect.orDie); - const checkpointRepository = yield* tryRuntimePromise( - "load ProjectionCheckpointRepository service", - () => runtime.runPromise(Effect.service(ProjectionCheckpointRepository)), - ).pipe(Effect.orDie); const pendingApprovalRepository = yield* tryRuntimePromise( "load ProjectionPendingApprovalRepository service", () => runtime.runPromise(Effect.service(ProjectionPendingApprovalRepository)), @@ -612,7 +604,6 @@ export const makeOrchestrationIntegrationHarness = ( snapshotQuery, providerService, checkpointStore, - checkpointRepository, pendingApprovalRepository, waitForThread, waitForDomainEvent, diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts index abe00f16bb818d2f30a815fd0bc7aadcf5502578..68b566e2ee4abd4dff7f4607acd93f5e812c936a 100644 --- a/apps/server/integration/orchestrationEngine.integration.test.ts +++ b/apps/server/integration/orchestrationEngine.integration.test.ts @@ -20,7 +20,6 @@ import { import { assert, it } from "@effect/vitest"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; -import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import type { TestTurnResponse } from "./TestProviderAdapter.integration.ts"; @@ -247,14 +246,7 @@ it.live("runs a single turn end-to-end and persists checkpoint state in sqlite + ); assert.equal(thread.checkpoints[0]?.status, "ready"); assert.equal(thread.checkpoints[0]?.checkpointTurnCount, 1); - - const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ - threadId: THREAD_ID, - }); - assert.equal(checkpointRows.length, 1); - assert.equal(checkpointRows[0]?.checkpointTurnCount, 1); - assert.equal(checkpointRows[0]?.status, "ready"); - assert.deepEqual(checkpointRows[0]?.files, []); + assert.deepEqual(thread.checkpoints[0]?.files, []); const ref0 = checkpointRefForThreadTurn(THREAD_ID, 0); const ref1 = checkpointRefForThreadTurn(THREAD_ID, 1); @@ -498,11 +490,8 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => true, ); - const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ - threadId: THREAD_ID, - }); assert.deepEqual( - checkpointRows.map((row) => row.checkpointTurnCount), + secondTurnThread.checkpoints.map((row) => row.checkpointTurnCount), [1, 2], ); @@ -688,14 +677,7 @@ it.live("records failed turn runtime state and checkpoint status as error", () = assert.equal(thread.session?.status, "error"); assert.equal(thread.checkpoints[0]?.status, "error"); - const checkpointRow = yield* harness.checkpointRepository.getByThreadAndTurnCount({ - threadId: THREAD_ID, - checkpointTurnCount: 1, - }); - assert.equal(Option.isSome(checkpointRow), true); - if (Option.isSome(checkpointRow)) { - assert.equal(checkpointRow.value.status, "error"); - } + assert.equal(thread.checkpoints[0]?.checkpointTurnCount, 1); assert.equal( gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 1)), true, @@ -878,11 +860,6 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git false, ); assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]); - - const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ - threadId: THREAD_ID, - }); - assert.equal(checkpointRows.length, 1); }), ), ); diff --git a/apps/server/src/orchestration/Errors.ts b/apps/server/src/orchestration/Errors.ts index dccb8b4be0f7bc35e700a4495296dca8a873766b..5e6376072ce13671e33b4ddf9c7c4584a56d7d52 100644 --- a/apps/server/src/orchestration/Errors.ts +++ b/apps/server/src/orchestration/Errors.ts @@ -4,30 +4,6 @@ import * as Schema from "effect/Schema"; import type { ProjectionRepositoryError } from "../persistence/Errors.ts"; -export class OrchestrationCommandJsonParseError extends Schema.TaggedError()( - "OrchestrationCommandJsonParseError", - { - detail: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Invalid orchestration command JSON: ${this.detail}`; - } -} - -export class OrchestrationCommandDecodeError extends Schema.TaggedError()( - "OrchestrationCommandDecodeError", - { - issue: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Invalid orchestration command payload: ${this.issue}`; - } -} - export class OrchestrationCommandInvariantError extends Schema.TaggedError()( "OrchestrationCommandInvariantError", { @@ -100,31 +76,12 @@ export class OrchestrationProjectorDecodeError extends Schema.TaggedError()( - "OrchestrationListenerCallbackError", - { - listener: Schema.Literals(["read-model", "domain-event"]), - detail: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Orchestration ${this.listener} listener failed: ${this.detail}`; - } -} - export type OrchestrationDispatchError = | ProjectionRepositoryError | OrchestrationCommandRejection | OrchestrationCommandIdConflictError | OrchestrationCommandPreviouslyRejectedError - | OrchestrationProjectorDecodeError - | OrchestrationListenerCallbackError; - -export type OrchestrationEngineError = - | OrchestrationDispatchError - | OrchestrationCommandJsonParseError - | OrchestrationCommandDecodeError; + | OrchestrationProjectorDecodeError; export function toProjectorDecodeError(eventType: string) { return (error: Schema.SchemaError): OrchestrationProjectorDecodeError => diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index 03bf173292861ae7978f98e90c2e4a9914f23265..1e7058742e25c75a46d067778bfec5105b949b36 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -8,6 +8,7 @@ import { MessageId, NonNegativeInt, OrchestrationCheckpointFile, + OrchestrationCheckpointStatus, OrchestrationProposedPlanId, OrchestrationReadModel, OrchestrationThreadSearchSource, @@ -52,7 +53,6 @@ import { toPersistenceSqlError, type ProjectionRepositoryError, } from "../../persistence/Errors.ts"; -import { ProjectionCheckpoint } from "../../persistence/Services/ProjectionCheckpoints.ts"; import { ThreadBackgroundLivenessService } from "../ThreadBackgroundLiveness.ts"; import { ThreadPlanProgressService } from "../ThreadPlanProgress.ts"; import { ProjectionProject } from "../../persistence/Services/ProjectionProjects.ts"; @@ -151,11 +151,16 @@ const ProjectionThreadRuntimeContextDbRowSchema = Schema.Struct({ title: Schema.String, session: Schema.NullOr(ProjectionThreadSessionDbRowSchema), }); -const ProjectionCheckpointDbRowSchema = ProjectionCheckpoint.mapFields( - Struct.assign({ - files: Schema.fromJsonString(Schema.Array(OrchestrationCheckpointFile)), - }), -); +const ProjectionCheckpointDbRowSchema = Schema.Struct({ + threadId: ThreadId, + turnId: TurnId, + checkpointTurnCount: NonNegativeInt, + checkpointRef: CheckpointRef, + status: OrchestrationCheckpointStatus, + files: Schema.fromJsonString(Schema.Array(OrchestrationCheckpointFile)), + assistantMessageId: Schema.NullOr(MessageId), + completedAt: IsoDateTime, +}); const ProjectionLatestTurnDbRowSchema = Schema.Struct({ threadId: ProjectionThread.fields.threadId, turnId: TurnId, diff --git a/apps/server/src/persistence/Errors.test.ts b/apps/server/src/persistence/Errors.test.ts index dfa35020df33503d856f93387162a80e80a7d374..5b9d37b3a65518207fa41f65af9a23b1da2d62d0 100644 --- a/apps/server/src/persistence/Errors.test.ts +++ b/apps/server/src/persistence/Errors.test.ts @@ -42,7 +42,7 @@ it.effect("names a real SQLite condition without copying query data", () => assert.equal(error.cause, cause); assert.notInclude(error.message, payload); assert.notInclude(error.message, "private_column"); - }).pipe(Effect.provide(NodeSqliteClient.layerMemory())), + }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))), ); it("reads the condition through a wrapping driver error", () => { diff --git a/apps/server/src/persistence/Errors.ts b/apps/server/src/persistence/Errors.ts index 4625b368d8f2910e1a1b21b39dbb263d13f4f99c..1e3c9027879dc0ebe5ae4e596a99c2151049de43 100644 --- a/apps/server/src/persistence/Errors.ts +++ b/apps/server/src/persistence/Errors.ts @@ -124,42 +124,8 @@ export function toPersistenceDecodeError(operation: string) { export const isPersistenceError = (u: unknown) => isPersistenceSqlError(u) || isPersistenceDecodeError(u); -// =============================== -// Provider Session Repository Errors -// =============================== - -export class ProviderSessionRepositoryValidationError extends Schema.TaggedError()( - "ProviderSessionRepositoryValidationError", - { - operation: Schema.String, - issue: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider session repository validation failed in ${this.operation}: ${this.issue}`; - } -} - -export class ProviderSessionRepositoryPersistenceError extends Schema.TaggedError()( - "ProviderSessionRepositoryPersistenceError", - { - operation: Schema.String, - detail: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider session repository persistence error in ${this.operation}: ${this.detail}`; - } -} - export type OrchestrationEventStoreError = PersistenceSqlError | PersistenceDecodeError; -export type ProviderSessionRepositoryError = - | ProviderSessionRepositoryValidationError - | ProviderSessionRepositoryPersistenceError; - export type OrchestrationCommandReceiptRepositoryError = | PersistenceSqlError | PersistenceDecodeError; diff --git a/apps/server/src/persistence/Layers/ProjectionCheckpoints.ts b/apps/server/src/persistence/Layers/ProjectionCheckpoints.ts deleted file mode 100644 index cfa5658d0e9cdb57502116e51ea761b71df7dff9..0000000000000000000000000000000000000000 --- a/apps/server/src/persistence/Layers/ProjectionCheckpoints.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { OrchestrationCheckpointFile } from "@t3tools/contracts"; -import * as SqlClient from "effect/unstable/sql/SqlClient"; -import * as SqlSchema from "effect/unstable/sql/SqlSchema"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; -import * as Struct from "effect/Struct"; - -import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts"; -import { - DeleteByThreadIdInput, - GetByThreadAndTurnCountInput, - ListByThreadIdInput, - ProjectionCheckpoint, - ProjectionCheckpointRepository, - type ProjectionCheckpointRepositoryShape, -} from "../Services/ProjectionCheckpoints.ts"; - -const ProjectionCheckpointDbRowSchema = ProjectionCheckpoint.mapFields( - Struct.assign({ - files: Schema.fromJsonString(Schema.Array(OrchestrationCheckpointFile)), - }), -); - -function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { - return (cause: unknown) => - Schema.isSchemaError(cause) - ? toPersistenceDecodeError(decodeOperation)(cause) - : toPersistenceSqlError(sqlOperation)(cause); -} - -const makeProjectionCheckpointRepository = Effect.gen(function* () { - const sql = yield* SqlClient.SqlClient; - - const clearCheckpointConflict = SqlSchema.void({ - Request: GetByThreadAndTurnCountInput, - execute: ({ threadId, checkpointTurnCount }) => - sql` - UPDATE projection_turns - SET - checkpoint_turn_count = NULL, - checkpoint_ref = NULL, - checkpoint_status = NULL, - checkpoint_files_json = '[]' - WHERE thread_id = ${threadId} - AND checkpoint_turn_count = ${checkpointTurnCount} - `, - }); - - const upsertProjectionCheckpointRow = SqlSchema.void({ - Request: ProjectionCheckpointDbRowSchema, - execute: (row) => - sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES ( - ${row.threadId}, - ${row.turnId}, - NULL, - ${row.assistantMessageId}, - ${row.status === "error" ? "error" : "completed"}, - ${row.completedAt}, - ${row.completedAt}, - ${row.completedAt}, - ${row.checkpointTurnCount}, - ${row.checkpointRef}, - ${row.status}, - ${row.files} - ) - ON CONFLICT (thread_id, turn_id) - DO UPDATE SET - assistant_message_id = excluded.assistant_message_id, - state = excluded.state, - completed_at = excluded.completed_at, - checkpoint_turn_count = excluded.checkpoint_turn_count, - checkpoint_ref = excluded.checkpoint_ref, - checkpoint_status = excluded.checkpoint_status, - checkpoint_files_json = excluded.checkpoint_files_json - `, - }); - - const listProjectionCheckpointRows = SqlSchema.findAll({ - Request: ListByThreadIdInput, - 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" - FROM projection_turns - WHERE thread_id = ${threadId} - AND checkpoint_turn_count IS NOT NULL - ORDER BY checkpoint_turn_count ASC - `, - }); - - const getProjectionCheckpointRow = SqlSchema.findOneOption({ - Request: GetByThreadAndTurnCountInput, - Result: ProjectionCheckpointDbRowSchema, - execute: ({ threadId, checkpointTurnCount }) => - 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" - FROM projection_turns - WHERE thread_id = ${threadId} - AND checkpoint_turn_count = ${checkpointTurnCount} - `, - }); - - const deleteProjectionCheckpointRows = SqlSchema.void({ - Request: DeleteByThreadIdInput, - execute: ({ threadId }) => - sql` - UPDATE projection_turns - SET - checkpoint_turn_count = NULL, - checkpoint_ref = NULL, - checkpoint_status = NULL, - checkpoint_files_json = '[]' - WHERE thread_id = ${threadId} - AND checkpoint_turn_count IS NOT NULL - `, - }); - - const upsertCheckpointRow = (row: Schema.Schema.Type) => - sql.withTransaction( - clearCheckpointConflict({ - threadId: row.threadId, - checkpointTurnCount: row.checkpointTurnCount, - }).pipe(Effect.flatMap(() => upsertProjectionCheckpointRow(row))), - ); - - const upsert: ProjectionCheckpointRepositoryShape["upsert"] = (row) => - upsertCheckpointRow(row).pipe( - Effect.mapError( - toPersistenceSqlOrDecodeError( - "ProjectionCheckpointRepository.upsert:query", - "ProjectionCheckpointRepository.upsert:encodeRequest", - ), - ), - ); - - const listByThreadId: ProjectionCheckpointRepositoryShape["listByThreadId"] = (input) => - listProjectionCheckpointRows(input).pipe( - Effect.mapError( - toPersistenceSqlOrDecodeError( - "ProjectionCheckpointRepository.listByThreadId:query", - "ProjectionCheckpointRepository.listByThreadId:decodeRows", - ), - ), - Effect.map((rows) => rows as ReadonlyArray>), - ); - - const getByThreadAndTurnCount: ProjectionCheckpointRepositoryShape["getByThreadAndTurnCount"] = ( - input, - ) => - getProjectionCheckpointRow(input).pipe( - Effect.mapError( - toPersistenceSqlOrDecodeError( - "ProjectionCheckpointRepository.getByThreadAndTurnCount:query", - "ProjectionCheckpointRepository.getByThreadAndTurnCount:decodeRow", - ), - ), - Effect.flatMap((rowOption) => - Option.match(rowOption, { - onNone: () => Effect.succeed(Option.none()), - onSome: (row) => - Effect.succeed(Option.some(row as Schema.Schema.Type)), - }), - ), - ); - - const deleteByThreadId: ProjectionCheckpointRepositoryShape["deleteByThreadId"] = (input) => - deleteProjectionCheckpointRows(input).pipe( - Effect.mapError( - toPersistenceSqlError("ProjectionCheckpointRepository.deleteByThreadId:query"), - ), - ); - - return { - upsert, - listByThreadId, - getByThreadAndTurnCount, - deleteByThreadId, - } satisfies ProjectionCheckpointRepositoryShape; -}); - -export const ProjectionCheckpointRepositoryLive = Layer.effect( - ProjectionCheckpointRepository, - makeProjectionCheckpointRepository, -); diff --git a/apps/server/src/persistence/Layers/ProjectionPendingApprovals.ts b/apps/server/src/persistence/Layers/ProjectionPendingApprovals.ts index d5631cb0a62cd55c4136675401d31846ada2a3cd..883198402ee9a3f20ee3be3f8a6f31e26ea3afe7 100644 --- a/apps/server/src/persistence/Layers/ProjectionPendingApprovals.ts +++ b/apps/server/src/persistence/Layers/ProjectionPendingApprovals.ts @@ -7,7 +7,6 @@ import * as Schema from "effect/Schema"; import { toPersistenceSqlError } from "../Errors.ts"; import { GetProjectionPendingApprovalInput, - DeleteProjectionPendingApprovalInput, ListProjectionPendingApprovalsInput, ProjectionPendingApproval, ProjectionPendingApprovalRepository, @@ -97,15 +96,6 @@ const makeProjectionPendingApprovalRepository = Effect.gen(function* () { `, }); - const deleteProjectionPendingApprovalRow = SqlSchema.void({ - Request: DeleteProjectionPendingApprovalInput, - execute: ({ requestId }) => - sql` - DELETE FROM projection_pending_approvals - WHERE request_id = ${requestId} - `, - }); - const deleteProjectionPendingApprovalRowsByThread = SqlSchema.void({ Request: ListProjectionPendingApprovalsInput, execute: ({ threadId }) => @@ -143,15 +133,6 @@ const makeProjectionPendingApprovalRepository = Effect.gen(function* () { ), ); - const deleteByRequestId: ProjectionPendingApprovalRepositoryShape["deleteByRequestId"] = ( - input, - ) => - deleteProjectionPendingApprovalRow(input).pipe( - Effect.mapError( - toPersistenceSqlError("ProjectionPendingApprovalRepository.deleteByRequestId:query"), - ), - ); - const deleteByThreadId: ProjectionPendingApprovalRepositoryShape["deleteByThreadId"] = (input) => deleteProjectionPendingApprovalRowsByThread(input).pipe( Effect.mapError( @@ -164,7 +145,6 @@ const makeProjectionPendingApprovalRepository = Effect.gen(function* () { listByThreadId, countPendingByThreadId, getByRequestId, - deleteByRequestId, deleteByThreadId, } satisfies ProjectionPendingApprovalRepositoryShape; }); diff --git a/apps/server/src/persistence/Layers/ProjectionProjects.ts b/apps/server/src/persistence/Layers/ProjectionProjects.ts index f34f8129412422e213bab81993c763707bbacece..c4f07dc4912b897615b472fe4f9f45efe8153a0b 100644 --- a/apps/server/src/persistence/Layers/ProjectionProjects.ts +++ b/apps/server/src/persistence/Layers/ProjectionProjects.ts @@ -9,7 +9,6 @@ import * as Struct from "effect/Struct"; import { ModelSelection, ProjectIconOverride, ProjectScript } from "@t3tools/contracts"; import { toPersistenceSqlError } from "../Errors.ts"; import { - DeleteProjectionProjectInput, GetProjectionProjectInput, ProjectionProject, ProjectionProjectRepository, @@ -24,7 +23,6 @@ const ProjectionProjectDbRow = ProjectionProject.mapFields( scripts: Schema.fromJsonString(Schema.Array(ProjectScript)), }), ); -type ProjectionProjectDbRow = typeof ProjectionProjectDbRow.Type; const makeProjectionProjectRepository = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; @@ -100,38 +98,6 @@ const makeProjectionProjectRepository = Effect.gen(function* () { `, }); - const listProjectionProjectRows = SqlSchema.findAll({ - Request: Schema.Void, - Result: ProjectionProjectDbRow, - 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" - FROM projection_projects - ORDER BY created_at ASC, project_id ASC - `, - }); - - const deleteProjectionProjectRow = SqlSchema.void({ - Request: DeleteProjectionProjectInput, - execute: ({ projectId }) => - sql` - DELETE FROM projection_projects - WHERE project_id = ${projectId} - `, - }); - const upsert: ProjectionProjectRepositoryShape["upsert"] = (row) => upsertProjectionProjectRow(row).pipe( Effect.mapError(toPersistenceSqlError("ProjectionProjectRepository.upsert:query")), @@ -143,22 +109,9 @@ const makeProjectionProjectRepository = Effect.gen(function* () { Effect.mapError(toPersistenceSqlError("ProjectionProjectRepository.getById:query")), ); - const listAll: ProjectionProjectRepositoryShape["listAll"] = () => - listProjectionProjectRows().pipe( - Effect.map((rows) => rows.map((row) => ({ ...row, autoPull: row.autoPull === 1 }))), - Effect.mapError(toPersistenceSqlError("ProjectionProjectRepository.listAll:query")), - ); - - const deleteById: ProjectionProjectRepositoryShape["deleteById"] = (input) => - deleteProjectionProjectRow(input).pipe( - Effect.mapError(toPersistenceSqlError("ProjectionProjectRepository.deleteById:query")), - ); - return { upsert, getById, - listAll, - deleteById, } satisfies ProjectionProjectRepositoryShape; }); diff --git a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts index b7b2fdefba2f3e9b82595bae5cb4cfafde69e276..4be65bd5650c56382b96294ab3f4639b483c26a3 100644 --- a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts +++ b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts @@ -518,9 +518,6 @@ projectionRepositoriesLayer("Projection repositories", (it) => { assert.deepStrictEqual(Option.getOrNull(persisted)?.linkedPullRequest, linkedPullRequest); assert.deepStrictEqual(Option.getOrNull(persisted)?.branchPullRequest, branchPullRequest); - const listed = yield* threads.listByProjectId({ projectId: linkedPullRequest.projectId }); - assert.deepStrictEqual(listed[0]?.branchPullRequest, branchPullRequest); - const row = Option.getOrNull(persisted); if (row === null) return yield* Effect.die("Expected linked thread row to exist."); yield* threads.upsert({ ...row, linkedPullRequest: null }); diff --git a/apps/server/src/persistence/Layers/ProjectionState.ts b/apps/server/src/persistence/Layers/ProjectionState.ts index a6fb7f3be4dd13e9e1d3fa169e725ca2f1ef6d38..383ab7d9fee8411faa4db40f2c39f86dced9468d 100644 --- a/apps/server/src/persistence/Layers/ProjectionState.ts +++ b/apps/server/src/persistence/Layers/ProjectionState.ts @@ -1,4 +1,3 @@ -import { NonNegativeInt } from "@t3tools/contracts"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as SqlSchema from "effect/unstable/sql/SqlSchema"; import * as Effect from "effect/Effect"; @@ -14,10 +13,6 @@ import { ProjectionState, } from "../Services/ProjectionState.ts"; -const MinLastAppliedSequenceRowSchema = Schema.Struct({ - minLastAppliedSequence: Schema.NullOr(NonNegativeInt), -}); - const makeProjectionStateRepository = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; @@ -90,17 +85,6 @@ const makeProjectionStateRepository = Effect.gen(function* () { `, }); - const readMinLastAppliedSequence = SqlSchema.findOne({ - Request: Schema.Void, - Result: MinLastAppliedSequenceRowSchema, - execute: () => - sql` - SELECT - MIN(last_applied_sequence) AS "minLastAppliedSequence" - FROM projection_state - `, - }); - const upsert: ProjectionStateRepositoryShape["upsert"] = (row) => upsertProjectionStateRow(row).pipe( Effect.mapError(toPersistenceSqlError("ProjectionStateRepository.upsert:query")), @@ -121,20 +105,11 @@ const makeProjectionStateRepository = Effect.gen(function* () { Effect.mapError(toPersistenceSqlError("ProjectionStateRepository.listAll:query")), ); - const minLastAppliedSequence: ProjectionStateRepositoryShape["minLastAppliedSequence"] = () => - readMinLastAppliedSequence(undefined).pipe( - Effect.mapError( - toPersistenceSqlError("ProjectionStateRepository.minLastAppliedSequence:query"), - ), - Effect.map((row) => row.minLastAppliedSequence), - ); - return { upsert, upsertMany, getByProjector, listAll, - minLastAppliedSequence, } satisfies ProjectionStateRepositoryShape; }); diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts index 4feaf3a185b971d3dbbe293bf2fa75a9468699f9..af36578f286ef2daa8e6a18036f37a765f4752c1 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreads.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts @@ -7,9 +7,7 @@ import * as Struct from "effect/Struct"; import { toPersistenceSqlError } from "../Errors.ts"; import { - DeleteProjectionThreadInput, GetProjectionThreadInput, - ListProjectionThreadsByProjectInput, ProjectionThread, ProjectionThreadRepository, type ProjectionThreadRepositoryShape, @@ -24,7 +22,6 @@ const ProjectionThreadDbRow = ProjectionThread.mapFields( branchPullRequest: Schema.NullOr(Schema.fromJsonString(ThreadLinkedPullRequest)), }), ); -type ProjectionThreadDbRow = typeof ProjectionThreadDbRow.Type; const makeProjectionThreadRepository = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; @@ -172,57 +169,6 @@ const makeProjectionThreadRepository = Effect.gen(function* () { `, }); - const listProjectionThreadRows = SqlSchema.findAll({ - Request: ListProjectionThreadsByProjectInput, - Result: ProjectionThreadDbRow, - execute: ({ projectId }) => - sql` - SELECT - thread_id AS "threadId", - project_id AS "projectId", - title, - title_state_json AS "titleState", - model_selection_json AS "modelSelection", - runtime_mode AS "runtimeMode", - interaction_mode AS "interactionMode", - branch, - worktree_path AS "worktreePath", - linked_pull_request_json AS "linkedPullRequest", - branch_pull_request_json AS "branchPullRequest", - 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", - active_order_key AS "activeOrderKey", - 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" - FROM projection_threads - WHERE project_id = ${projectId} - ORDER BY created_at ASC, thread_id ASC - `, - }); - - const deleteProjectionThreadRow = SqlSchema.void({ - Request: DeleteProjectionThreadInput, - execute: ({ threadId }) => - sql` - DELETE FROM projection_threads - WHERE thread_id = ${threadId} - `, - }); - const upsert: ProjectionThreadRepositoryShape["upsert"] = (row) => upsertProjectionThreadRow(row).pipe( Effect.mapError(toPersistenceSqlError("ProjectionThreadRepository.upsert:query")), @@ -233,21 +179,9 @@ const makeProjectionThreadRepository = Effect.gen(function* () { Effect.mapError(toPersistenceSqlError("ProjectionThreadRepository.getById:query")), ); - const listByProjectId: ProjectionThreadRepositoryShape["listByProjectId"] = (input) => - listProjectionThreadRows(input).pipe( - Effect.mapError(toPersistenceSqlError("ProjectionThreadRepository.listByProjectId:query")), - ); - - const deleteById: ProjectionThreadRepositoryShape["deleteById"] = (input) => - deleteProjectionThreadRow(input).pipe( - Effect.mapError(toPersistenceSqlError("ProjectionThreadRepository.deleteById:query")), - ); - return { upsert, getById, - listByProjectId, - deleteById, } satisfies ProjectionThreadRepositoryShape; }); diff --git a/apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts b/apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts index fff5a738622d142cba5d45ab2238db39ed76d7fb..ca32b1bddd419383fb717900ad73053e3507fe73 100644 --- a/apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts +++ b/apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("016_CanonicalizeModelSelections", (it) => { it.effect( diff --git a/apps/server/src/persistence/Migrations/019_ProjectionSnapshotLookupIndexes.test.ts b/apps/server/src/persistence/Migrations/019_ProjectionSnapshotLookupIndexes.test.ts index 040a9fa47b92de279c111f0bba6f19cb3c68a6b7..248c74b43b34b67c169fba7fea2b35ab1a643b9c 100644 --- a/apps/server/src/persistence/Migrations/019_ProjectionSnapshotLookupIndexes.test.ts +++ b/apps/server/src/persistence/Migrations/019_ProjectionSnapshotLookupIndexes.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("019_ProjectionSnapshotLookupIndexes", (it) => { it.effect("creates indexes for targeted projection lookup filters", () => diff --git a/apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts b/apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts index 49585fb36f37430c2ad29163694856f4f2d390f2..73c579a13366982d83b1a2bc8d2d2359ddbdc0dc 100644 --- a/apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts +++ b/apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("024_BackfillProjectionThreadShellSummary", (it) => { it.effect("backfills thread shell summary fields and clears stale projected approvals", () => diff --git a/apps/server/src/persistence/Migrations/025_CleanupInvalidProjectionPendingApprovals.test.ts b/apps/server/src/persistence/Migrations/025_CleanupInvalidProjectionPendingApprovals.test.ts index efdf88bf6c3b2187984eaa48e4ffd7229eecbf15..7e65f4c04f72d291f419459b4df2e4f9c26b1162 100644 --- a/apps/server/src/persistence/Migrations/025_CleanupInvalidProjectionPendingApprovals.test.ts +++ b/apps/server/src/persistence/Migrations/025_CleanupInvalidProjectionPendingApprovals.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("025_CleanupInvalidProjectionPendingApprovals", (it) => { it.effect("removes pending-approval rows that do not come from approval requests", () => diff --git a/apps/server/src/persistence/Migrations/026_CanonicalizeModelSelectionOptions.test.ts b/apps/server/src/persistence/Migrations/026_CanonicalizeModelSelectionOptions.test.ts index 558183e216cc3321eddf165c76472f172be9ddf7..5dd053cfe77c0140c2a9422770b742fedb2e05e5 100644 --- a/apps/server/src/persistence/Migrations/026_CanonicalizeModelSelectionOptions.test.ts +++ b/apps/server/src/persistence/Migrations/026_CanonicalizeModelSelectionOptions.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("026_CanonicalizeModelSelectionOptions", (it) => { it.effect("converts legacy object-shape options into array-shape on projections and events", () => diff --git a/apps/server/src/persistence/Migrations/027_028_ProviderInstanceIdColumns.test.ts b/apps/server/src/persistence/Migrations/027_028_ProviderInstanceIdColumns.test.ts index b5e4f5cf3a22a9580f81145b8fabec3025c0ebd6..aa99f0c862a272b91e3cc5aabe8c36f66ee63e73 100644 --- a/apps/server/src/persistence/Migrations/027_028_ProviderInstanceIdColumns.test.ts +++ b/apps/server/src/persistence/Migrations/027_028_ProviderInstanceIdColumns.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("027_028_ProviderInstanceIdColumns", (it) => { it.effect("continues when provider_session_runtime was partially migrated", () => diff --git a/apps/server/src/persistence/Migrations/029_ProjectionThreadDetailOrderingIndexes.test.ts b/apps/server/src/persistence/Migrations/029_ProjectionThreadDetailOrderingIndexes.test.ts index 7078450c96ec8775c96b4a7cdbbb786c5e76dac7..cd246d16ee874e3c37305705a754e6be0699d72f 100644 --- a/apps/server/src/persistence/Migrations/029_ProjectionThreadDetailOrderingIndexes.test.ts +++ b/apps/server/src/persistence/Migrations/029_ProjectionThreadDetailOrderingIndexes.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("029_ProjectionThreadDetailOrderingIndexes", (it) => { it.effect("creates indexes matching thread detail ordering queries", () => diff --git a/apps/server/src/persistence/Migrations/031_AuthAuthorizationScopes.test.ts b/apps/server/src/persistence/Migrations/031_AuthAuthorizationScopes.test.ts index 63eba11aac7192aaec3bcc525a10cdfe42be82d4..60b0079d79f9ae4ff3551c0ff00bf2cf180f027a 100644 --- a/apps/server/src/persistence/Migrations/031_AuthAuthorizationScopes.test.ts +++ b/apps/server/src/persistence/Migrations/031_AuthAuthorizationScopes.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("031_AuthAuthorizationScopes", (it) => { it.effect("invalidates role-based auth records and installs scoped auth tables", () => diff --git a/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts b/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts index 0e7f54812c21bdeaf7ab34440d53698a58454154..cf0ecff3253a51d92a3a90442c77880849159011 100644 --- a/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts +++ b/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("035_ProjectionThreadTitleRegeneration", (it) => { it.effect("adds pending title regeneration columns", () => diff --git a/apps/server/src/persistence/Migrations/040_ProjectionProjectFaviconPath.test.ts b/apps/server/src/persistence/Migrations/040_ProjectionProjectFaviconPath.test.ts index 42728812500736fc80b564919caf92820f97d6ac..524831e891ab4b3a3c528e0c2837fbb73bd390dd 100644 --- a/apps/server/src/persistence/Migrations/040_ProjectionProjectFaviconPath.test.ts +++ b/apps/server/src/persistence/Migrations/040_ProjectionProjectFaviconPath.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("040_ProjectionProjectFaviconPath", (it) => { it.effect("adds the nullable favicon path to project projections", () => diff --git a/apps/server/src/persistence/Migrations/041_AuthSessionClientConnection.test.ts b/apps/server/src/persistence/Migrations/041_AuthSessionClientConnection.test.ts index b19f1ce7cf2000185d98113ae44e9ef0e04c9744..4f106de404a0c2a40927cb7d3dee3f99e1889116 100644 --- a/apps/server/src/persistence/Migrations/041_AuthSessionClientConnection.test.ts +++ b/apps/server/src/persistence/Migrations/041_AuthSessionClientConnection.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("041_AuthSessionClientConnection", (it) => { it.effect("adds nullable client surface and app version columns to auth sessions", () => diff --git a/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts b/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts index 02e7f2f6c59f5e1738773940d13316ad5ffa16bf..a4ed1653a962f1a3c1ae5251714f1e5cfc9b28e6 100644 --- a/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts +++ b/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("042_ProjectionThreadLinkedPullRequest", (it) => { it.effect("adds the linked pull request column", () => diff --git a/apps/server/src/persistence/Migrations/046_RepairAutomaticSettlementTimestamps.test.ts b/apps/server/src/persistence/Migrations/046_RepairAutomaticSettlementTimestamps.test.ts index f562fa655a118d90f0767b56336b51804dccee2e..de7c6f01f659561475c50e84363b795100458581 100644 --- a/apps/server/src/persistence/Migrations/046_RepairAutomaticSettlementTimestamps.test.ts +++ b/apps/server/src/persistence/Migrations/046_RepairAutomaticSettlementTimestamps.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); const MODEL_SELECTION = '{"instanceId":"codex","model":"gpt-5.6-sol"}'; diff --git a/apps/server/src/persistence/Migrations/047_ProjectionProjectIcon.test.ts b/apps/server/src/persistence/Migrations/047_ProjectionProjectIcon.test.ts index da3f7cd366d9ba87a9af018159af06ad4d559df1..c601bb0077fe044ec61b4061f195c3b836ce0651 100644 --- a/apps/server/src/persistence/Migrations/047_ProjectionProjectIcon.test.ts +++ b/apps/server/src/persistence/Migrations/047_ProjectionProjectIcon.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("047_ProjectionProjectIcon", (it) => { it.effect("adds the nullable project icon JSON to project projections", () => diff --git a/apps/server/src/persistence/Migrations/049_ProjectionThreadsActiveOrderKey.test.ts b/apps/server/src/persistence/Migrations/049_ProjectionThreadsActiveOrderKey.test.ts index 138d25754d7e4db9ca8fde2a6c8b20f9649c4483..938ab25d0f8adb4e5d4357784ce6115efd721ecc 100644 --- a/apps/server/src/persistence/Migrations/049_ProjectionThreadsActiveOrderKey.test.ts +++ b/apps/server/src/persistence/Migrations/049_ProjectionThreadsActiveOrderKey.test.ts @@ -6,13 +6,15 @@ import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import { runMigrations } from "../Migrations.ts"; import migrateActiveOrderKey from "./049_ProjectionThreadsActiveOrderKey.ts"; -it.layer(NodeSqliteClient.layerMemory())("049_ProjectionThreadsActiveOrderKey", (it) => { - it.effect("migrates old threads without changing their timestamps or assigning an order", () => - Effect.gen(function* () { - const sql = yield* SqlClient.SqlClient; - yield* runMigrations({ toMigrationInclusive: 48 }); - const now = "2026-01-01T00:00:00.000Z"; - yield* sql` +it.layer(NodeSqliteClient.layer({ filename: ":memory:" }))( + "049_ProjectionThreadsActiveOrderKey", + (it) => { + it.effect("migrates old threads without changing their timestamps or assigning an order", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* runMigrations({ toMigrationInclusive: 48 }); + const now = "2026-01-01T00:00:00.000Z"; + yield* sql` INSERT INTO projection_threads ( thread_id, project_id, title, model_selection_json, runtime_mode, created_at, updated_at @@ -21,24 +23,25 @@ it.layer(NodeSqliteClient.layerMemory())("049_ProjectionThreadsActiveOrderKey", '{"instanceId":"codex","model":"gpt-5.4"}', 'full-access', ${now}, ${now} ) `; - yield* runMigrations({ toMigrationInclusive: 49 }); - const migrated = yield* sql<{ readonly activeOrderKey: string | null }>` + yield* runMigrations({ toMigrationInclusive: 49 }); + const migrated = yield* sql<{ readonly activeOrderKey: string | null }>` SELECT active_order_key AS "activeOrderKey" FROM projection_threads WHERE thread_id = 'thread-1' `; - assert.deepEqual(migrated, [{ activeOrderKey: null }]); - // Recovery may run the same migration against a database that already - // has the column, including a placement written after the upgrade. - yield* sql`UPDATE projection_threads SET active_order_key = 'gm' WHERE thread_id = 'thread-1'`; - yield* migrateActiveOrderKey; - const rows = yield* sql<{ - readonly activeOrderKey: string | null; - readonly createdAt: string; - readonly updatedAt: string; - }>` + assert.deepEqual(migrated, [{ activeOrderKey: null }]); + // Recovery may run the same migration against a database that already + // has the column, including a placement written after the upgrade. + yield* sql`UPDATE projection_threads SET active_order_key = 'gm' WHERE thread_id = 'thread-1'`; + yield* migrateActiveOrderKey; + const rows = yield* sql<{ + readonly activeOrderKey: string | null; + readonly createdAt: string; + readonly updatedAt: string; + }>` SELECT active_order_key AS "activeOrderKey", created_at AS "createdAt", updated_at AS "updatedAt" FROM projection_threads WHERE thread_id = 'thread-1' `; - assert.deepEqual(rows, [{ activeOrderKey: "gm", createdAt: now, updatedAt: now }]); - }), - ); -}); + assert.deepEqual(rows, [{ activeOrderKey: "gm", createdAt: now, updatedAt: now }]); + }), + ); + }, +); diff --git a/apps/server/src/persistence/Migrations/050_ProjectionThreadPullRequests.test.ts b/apps/server/src/persistence/Migrations/050_ProjectionThreadPullRequests.test.ts index 61d9f0618e9c7d9219c3d620a3d2ddf673bc2953..7f3bc58d1cc152927ee85056c24be1d496dc01d0 100644 --- a/apps/server/src/persistence/Migrations/050_ProjectionThreadPullRequests.test.ts +++ b/apps/server/src/persistence/Migrations/050_ProjectionThreadPullRequests.test.ts @@ -9,7 +9,7 @@ import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); interface PullRequestRow { readonly threadId: string; @@ -156,26 +156,29 @@ layer("050_ProjectionThreadPullRequests", (it) => { ); }); -it.layer(Layer.fresh(NodeSqliteClient.layerMemory()))("050 Azure legacy links", (it) => { - it.effect("keeps legacy Azure repositories distinct across organizations", () => - Effect.gen(function* () { - const sql = yield* SqlClient.SqlClient; - yield* runMigrations({ toMigrationInclusive: 49 }); - for (const organization of ["org-a", "org-b"]) { - yield* sql` +it.layer(Layer.fresh(NodeSqliteClient.layer({ filename: ":memory:" })))( + "050 Azure legacy links", + (it) => { + it.effect("keeps legacy Azure repositories distinct across organizations", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* runMigrations({ toMigrationInclusive: 49 }); + for (const organization of ["org-a", "org-b"]) { + yield* sql` INSERT INTO projection_threads (thread_id, project_id, title, model_selection_json, linked_pull_request_json, created_at, updated_at) VALUES (${organization}, ${organization}, 'Azure', '{"instanceId":"codex","model":"gpt-5.4"}', ${encodeJson({ projectId: organization, repository: "web", number: 7, url: `https://dev.azure.com/${organization}/project/_git/web/pullrequest/7` })}, '2026-03-01T00:00:00.000Z', '2026-03-01T00:00:00.000Z') `; - } - yield* runMigrations({ toMigrationInclusive: 50 }); - const rows = - yield* sql`SELECT host, repository, number FROM projection_thread_pull_requests ORDER BY repository`; - assert.deepStrictEqual(rows, [ - { host: "dev.azure.com", repository: "org-a/project/_git/web", number: 7 }, - { host: "dev.azure.com", repository: "org-b/project/_git/web", number: 7 }, - ]); - }), - ); -}); + } + yield* runMigrations({ toMigrationInclusive: 50 }); + const rows = + yield* sql`SELECT host, repository, number FROM projection_thread_pull_requests ORDER BY repository`; + assert.deepStrictEqual(rows, [ + { host: "dev.azure.com", repository: "org-a/project/_git/web", number: 7 }, + { host: "dev.azure.com", repository: "org-b/project/_git/web", number: 7 }, + ]); + }), + ); + }, +); diff --git a/apps/server/src/persistence/Migrations/051_ProjectionThreadMessageContext.test.ts b/apps/server/src/persistence/Migrations/051_ProjectionThreadMessageContext.test.ts index c3cdfa1b9dcd40bf88949515625d870ba21dac5c..a0e59217cd30b487d53a02d79e2d3a37e4c09921 100644 --- a/apps/server/src/persistence/Migrations/051_ProjectionThreadMessageContext.test.ts +++ b/apps/server/src/persistence/Migrations/051_ProjectionThreadMessageContext.test.ts @@ -6,7 +6,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" }))); layer("051_ProjectionThreadMessageContext", (it) => { it.effect("accepts context added by an earlier development migration", () => diff --git a/apps/server/src/persistence/Services/ProjectionCheckpoints.ts b/apps/server/src/persistence/Services/ProjectionCheckpoints.ts deleted file mode 100644 index 7796ebec2a8801949c84edc5186a0309ba95551a..0000000000000000000000000000000000000000 --- a/apps/server/src/persistence/Services/ProjectionCheckpoints.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * ProjectionCheckpointRepository - Projection repository interface for checkpoints. - * - * Owns persistence operations for projected checkpoint summaries in thread - * timelines. - * - * @module ProjectionCheckpointRepository - */ -import { - CheckpointRef, - IsoDateTime, - MessageId, - NonNegativeInt, - OrchestrationCheckpointFile, - OrchestrationCheckpointStatus, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import * as Option from "effect/Option"; -import * as Context from "effect/Context"; -import * as Schema from "effect/Schema"; -import type * as Effect from "effect/Effect"; - -import type { ProjectionRepositoryError } from "../Errors.ts"; - -export const ProjectionCheckpoint = Schema.Struct({ - threadId: ThreadId, - turnId: TurnId, - checkpointTurnCount: NonNegativeInt, - checkpointRef: CheckpointRef, - status: OrchestrationCheckpointStatus, - files: Schema.Array(OrchestrationCheckpointFile), - assistantMessageId: Schema.NullOr(MessageId), - completedAt: IsoDateTime, -}); -export type ProjectionCheckpoint = typeof ProjectionCheckpoint.Type; - -export const ListByThreadIdInput = Schema.Struct({ - threadId: ThreadId, -}); -export type ListByThreadIdInput = typeof ListByThreadIdInput.Type; - -export const GetByThreadAndTurnCountInput = Schema.Struct({ - threadId: ThreadId, - checkpointTurnCount: NonNegativeInt, -}); -export type GetByThreadAndTurnCountInput = typeof GetByThreadAndTurnCountInput.Type; - -export const DeleteByThreadIdInput = Schema.Struct({ - threadId: ThreadId, -}); -export type DeleteByThreadIdInput = typeof DeleteByThreadIdInput.Type; - -/** - * ProjectionCheckpointRepositoryShape - Service API for projected checkpoints. - */ -export interface ProjectionCheckpointRepositoryShape { - /** - * Insert or replace a projected checkpoint row. - * - * Upserts by composite key `(threadId, checkpointTurnCount)`. - */ - readonly upsert: (row: ProjectionCheckpoint) => Effect.Effect; - - /** - * List projected checkpoints for a thread. - * - * Returned in ascending checkpoint turn-count order. - */ - readonly listByThreadId: ( - input: ListByThreadIdInput, - ) => Effect.Effect, ProjectionRepositoryError>; - - /** - * Read a projected checkpoint by thread and turn-count key. - */ - readonly getByThreadAndTurnCount: ( - input: GetByThreadAndTurnCountInput, - ) => Effect.Effect, ProjectionRepositoryError>; - - /** - * Delete projected checkpoint rows by thread. - */ - readonly deleteByThreadId: ( - input: DeleteByThreadIdInput, - ) => Effect.Effect; -} - -/** - * ProjectionCheckpointRepository - Service tag for checkpoint projection persistence. - */ -export class ProjectionCheckpointRepository extends Context.Service< - ProjectionCheckpointRepository, - ProjectionCheckpointRepositoryShape ->()("t3/persistence/Services/ProjectionCheckpoints/ProjectionCheckpointRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts b/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts index 43a829de63908107ae620ea097026de7f3ecc996..9b9e090af760fc6b245926ad29d73c27473b7355 100644 --- a/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts +++ b/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts @@ -42,11 +42,6 @@ export const GetProjectionPendingApprovalInput = Schema.Struct({ }); export type GetProjectionPendingApprovalInput = typeof GetProjectionPendingApprovalInput.Type; -export const DeleteProjectionPendingApprovalInput = Schema.Struct({ - requestId: ApprovalRequestId, -}); -export type DeleteProjectionPendingApprovalInput = typeof DeleteProjectionPendingApprovalInput.Type; - /** * ProjectionPendingApprovalRepositoryShape - Service API for pending approvals. */ @@ -81,13 +76,6 @@ export interface ProjectionPendingApprovalRepositoryShape { input: GetProjectionPendingApprovalInput, ) => Effect.Effect, ProjectionRepositoryError>; - /** - * Delete a pending approval row by request id. - */ - readonly deleteByRequestId: ( - input: DeleteProjectionPendingApprovalInput, - ) => Effect.Effect; - /** * Delete every pending approval row for a thread. */ diff --git a/apps/server/src/persistence/Services/ProjectionProjects.ts b/apps/server/src/persistence/Services/ProjectionProjects.ts index e5d1f6ba1f1d0e555a1bb7d73d47324f4e6adcd4..3228855fc8ec5290fcc60384bd1fdac7b7d4e4cc 100644 --- a/apps/server/src/persistence/Services/ProjectionProjects.ts +++ b/apps/server/src/persistence/Services/ProjectionProjects.ts @@ -42,11 +42,6 @@ export const GetProjectionProjectInput = Schema.Struct({ }); export type GetProjectionProjectInput = typeof GetProjectionProjectInput.Type; -export const DeleteProjectionProjectInput = Schema.Struct({ - projectId: ProjectId, -}); -export type DeleteProjectionProjectInput = typeof DeleteProjectionProjectInput.Type; - /** * ProjectionProjectRepositoryShape - Service API for projected project records. */ @@ -64,23 +59,6 @@ export interface ProjectionProjectRepositoryShape { readonly getById: ( input: GetProjectionProjectInput, ) => Effect.Effect, ProjectionRepositoryError>; - - /** - * List all projected project rows. - * - * Returned in deterministic creation order. - */ - readonly listAll: () => Effect.Effect< - ReadonlyArray, - ProjectionRepositoryError - >; - - /** - * Soft-delete a projected project row by id. - */ - readonly deleteById: ( - input: DeleteProjectionProjectInput, - ) => Effect.Effect; } /** diff --git a/apps/server/src/persistence/Services/ProjectionState.ts b/apps/server/src/persistence/Services/ProjectionState.ts index 9372acdeeb656065ca77be793ac009ccd9225b62..52dbc4cf637c4a4988adc8f8354330348af01e82 100644 --- a/apps/server/src/persistence/Services/ProjectionState.ts +++ b/apps/server/src/persistence/Services/ProjectionState.ts @@ -53,13 +53,6 @@ export interface ProjectionStateRepositoryShape { * List all projector cursor rows. */ readonly listAll: () => Effect.Effect, ProjectionRepositoryError>; - - /** - * Read the minimum applied sequence across all projectors. - * - * Returns `null` when no projector state rows exist. - */ - readonly minLastAppliedSequence: () => Effect.Effect; } /** diff --git a/apps/server/src/persistence/Services/ProjectionThreads.ts b/apps/server/src/persistence/Services/ProjectionThreads.ts index 2c8186a321c08fd37f35a6124d94d82a10004792..895fe596db2416601dddc22e4037124325c6c509 100644 --- a/apps/server/src/persistence/Services/ProjectionThreads.ts +++ b/apps/server/src/persistence/Services/ProjectionThreads.ts @@ -65,16 +65,6 @@ export const GetProjectionThreadInput = Schema.Struct({ }); export type GetProjectionThreadInput = typeof GetProjectionThreadInput.Type; -export const DeleteProjectionThreadInput = Schema.Struct({ - threadId: ThreadId, -}); -export type DeleteProjectionThreadInput = typeof DeleteProjectionThreadInput.Type; - -export const ListProjectionThreadsByProjectInput = Schema.Struct({ - projectId: ProjectId, -}); -export type ListProjectionThreadsByProjectInput = typeof ListProjectionThreadsByProjectInput.Type; - /** * ProjectionThreadRepositoryShape - Service API for projected thread records. */ @@ -92,22 +82,6 @@ export interface ProjectionThreadRepositoryShape { readonly getById: ( input: GetProjectionThreadInput, ) => Effect.Effect, ProjectionRepositoryError>; - - /** - * List projected threads for a project. - * - * Returned in deterministic creation order. - */ - readonly listByProjectId: ( - input: ListProjectionThreadsByProjectInput, - ) => Effect.Effect, ProjectionRepositoryError>; - - /** - * Soft-delete a projected thread row by id. - */ - readonly deleteById: ( - input: DeleteProjectionThreadInput, - ) => Effect.Effect; } /** diff --git a/apps/server/src/provider/acp/XAiAcpExtension.ts b/apps/server/src/provider/acp/XAiAcpExtension.ts index 620545569aaacaa184c433015fbaf0a383c323e8..08bbd674e9b037fa7d1cb293723d06999527d5ff 100644 --- a/apps/server/src/provider/acp/XAiAcpExtension.ts +++ b/apps/server/src/provider/acp/XAiAcpExtension.ts @@ -110,10 +110,6 @@ interface XAiAskUserQuestionCancelledResponse { readonly outcome: "cancelled"; } -export type XAiAskUserQuestionResponse = - | XAiAskUserQuestionAcceptedResponse - | XAiAskUserQuestionCancelledResponse; - interface NormalizedXAiAnswer { readonly questionText: string; readonly selectedLabels: ReadonlyArray; diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 512e55a64cbb597b61ec25dd5798d93d7c0582a0..2618020d388d78662edad3cef52d5537c2b828ff 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -123,10 +123,7 @@ import * as Keybindings from "./keybindings.ts"; import * as ExternalLauncher from "./process/externalLauncher.ts"; import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts"; import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; -import { - OrchestrationListenerCallbackError, - OrchestrationThreadSettleBlockedError, -} from "./orchestration/Errors.ts"; +import { OrchestrationThreadSettleBlockedError } from "./orchestration/Errors.ts"; import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import { ThreadDeletionReactor } from "./orchestration/Services/ThreadDeletionReactor.ts"; import * as PullRequestSyncReactor from "./orchestration/PullRequestSyncReactor.ts"; @@ -7358,8 +7355,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.flatMap(() => command.commandId === failedCommandId ? Effect.fail( - new OrchestrationListenerCallbackError({ - listener: "domain-event", + new PersistenceSqlError({ + operation: "OrchestrationEventStore.append:query", detail: "thread creation failed", }), ) @@ -10682,8 +10679,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { effects.push(`dispatch:${command.type}`); if (command.type === "thread.session.stop") { return Effect.fail( - new OrchestrationListenerCallbackError({ - listener: "domain-event", + new PersistenceSqlError({ + operation: "OrchestrationEventStore.append:query", detail: "simulated archive stop failure", }), ); @@ -11629,8 +11626,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { setupActivityAppendAttempt += 1; if (setupActivityAppendAttempt === 2) { return Effect.fail( - new OrchestrationListenerCallbackError({ - listener: "domain-event", + new PersistenceSqlError({ + operation: "OrchestrationEventStore.append:query", detail: "failed to append setup-script.started activity", }), ); @@ -12155,8 +12152,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { dispatchedCommands.push(command); if (command.type === "thread.delete") { return Effect.fail( - new OrchestrationListenerCallbackError({ - listener: "domain-event", + new PersistenceSqlError({ + operation: "OrchestrationEventStore.append:query", detail: "thread cleanup exploded", }), ); diff --git a/apps/server/src/textGeneration/TextGeneration.ts b/apps/server/src/textGeneration/TextGeneration.ts index 27fc28df7bf7c30ee7e90a3fad8350b38e775fca..37634cea7e4e510e21fac585e5d08510b9d51e2e 100644 --- a/apps/server/src/textGeneration/TextGeneration.ts +++ b/apps/server/src/textGeneration/TextGeneration.ts @@ -10,8 +10,6 @@ import * as SourceControlProviderRegistry from "../sourceControl/SourceControlPr import * as ThreadTitleLinks from "./ThreadTitleLinks.ts"; import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts"; -export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode"; - export interface CommitMessageGenerationInput { cwd: string; branch: string | null; diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.ts b/apps/server/src/workspace/WorkspaceSearchIndex.ts index cac501aede0c75658b56461040046984c7784480..a387dd189b3b95ef651b40327f373587d5d5fda9 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.ts @@ -107,12 +107,6 @@ export class WorkspaceSearchIndexDestroyFailed extends Schema.TaggedError ({ @@ -32,13 +32,16 @@ describe("managed relay authentication", () => { it("clears all token access synchronously before account cleanup can fail", async () => { activateManagedRelayAuthentication("account-1", async () => "account-1-token"); expect(appAtomRegistry.get(managedRelaySessionAtom)?.accountId).toBe("account-1"); - expect(await readManagedRelayClerkToken()).toBe("account-1-token"); + const token = await Effect.fromNullishOr(appAtomRegistry.get(managedRelaySessionAtom)).pipe( + Effect.flatMap((session) => session.readClerkToken()), + Effect.runPromise, + ); + expect(token).toBe("account-1-token"); deactivateManagedRelayAuthentication(); const cleanup = Promise.reject(new Error("Persistence removal failed.")).catch(() => undefined); expect(appAtomRegistry.get(managedRelaySessionAtom)).toBeNull(); - expect(await readManagedRelayClerkToken()).toBeNull(); await cleanup; }); diff --git a/apps/web/src/cloud/managedAuth.tsx b/apps/web/src/cloud/managedAuth.tsx index 2f631214501b4cce3da804aaad8bba65342e8ecf..f9bf1c966f40b84321a0aa118d4e81d5437f8882 100644 --- a/apps/web/src/cloud/managedAuth.tsx +++ b/apps/web/src/cloud/managedAuth.tsx @@ -14,14 +14,7 @@ import { appAtomRegistry } from "../rpc/atomRegistry"; import { useAtomCommand } from "../state/use-atom-command"; import { resolveRelayClerkTokenOptions } from "./publicConfig"; -let relayTokenProvider: (() => Promise) | null = null; - -export async function readManagedRelayClerkToken(): Promise { - return relayTokenProvider?.() ?? null; -} - export function deactivateManagedRelayAuthentication(): void { - relayTokenProvider = null; setManagedRelaySession(appAtomRegistry, null); } @@ -29,7 +22,6 @@ export function activateManagedRelayAuthentication( accountId: string, readClerkToken: () => Promise, ): void { - relayTokenProvider = readClerkToken; setManagedRelaySession(appAtomRegistry, { accountId, readClerkToken, diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 799bd0bde7f4edceb2f5a696978592f242a155e9..a2897ce3ddb0d64717e8a22ff54d5709f0ff2f50 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -32,7 +32,6 @@ import { usePreviewMiniPlayerStore, } from "../previewMiniPlayerStore"; import { - MAX_HIDDEN_MOUNTED_PREVIEW_THREADS, MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, agentControlledBrowserCloseConfirmation, branchMismatchKey, @@ -52,7 +51,6 @@ import { shouldRefocusComposerOnWindowFocus, isBranchMismatchDismissedForSession, reconcileMountedTerminalThreadIds, - reconcileRetainedMountedThreadIds, recallCheckoutIsRepo, rememberCheckoutIsRepo, resolveBackgroundDraftWorkspaceOptions, @@ -1887,50 +1885,6 @@ describe("reconcileMountedTerminalThreadIds", () => { }); }); -describe("reconcileRetainedMountedThreadIds", () => { - it("retains hidden open threads and adds the active open thread", () => { - expect( - reconcileRetainedMountedThreadIds({ - currentThreadIds: [ThreadId.make("thread-hidden")], - openThreadIds: [ThreadId.make("thread-hidden")], - activeThreadId: ThreadId.make("thread-active"), - activeThreadOpen: true, - maxHiddenThreadCount: MAX_HIDDEN_MOUNTED_PREVIEW_THREADS, - }), - ).toEqual([ThreadId.make("thread-hidden"), ThreadId.make("thread-active")]); - }); - - it("can retain the active thread as hidden when it is inactive", () => { - expect( - reconcileRetainedMountedThreadIds({ - currentThreadIds: [ThreadId.make("thread-active")], - openThreadIds: [ThreadId.make("thread-active")], - activeThreadId: ThreadId.make("thread-active"), - activeThreadOpen: false, - maxHiddenThreadCount: MAX_HIDDEN_MOUNTED_PREVIEW_THREADS, - retainInactiveActiveThread: true, - }), - ).toEqual([ThreadId.make("thread-active")]); - }); - - it("evicts the oldest hidden threads beyond the configured cap", () => { - const currentThreadIds = Array.from( - { length: MAX_HIDDEN_MOUNTED_PREVIEW_THREADS + 2 }, - (_, index) => ThreadId.make(`thread-${index + 1}`), - ); - - expect( - reconcileRetainedMountedThreadIds({ - currentThreadIds, - openThreadIds: currentThreadIds, - activeThreadId: null, - activeThreadOpen: false, - maxHiddenThreadCount: MAX_HIDDEN_MOUNTED_PREVIEW_THREADS, - }), - ).toEqual(currentThreadIds.slice(-MAX_HIDDEN_MOUNTED_PREVIEW_THREADS)); - }); -}); - describe("shouldWriteThreadErrorToCurrentServerThread", () => { it("writes errors for a shell-derived active server thread", () => { const routeThreadRef = { environmentId, threadId }; diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index bc68a9fcfa0171309afdb8152633bb3557c8527b..928d99c549a62ce8171ce6d240ebee882c7ae137 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -60,7 +60,7 @@ import { export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project"; export const MAX_HIDDEN_MOUNTED_TERMINAL_THREADS = 10; -export const MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3; + export const ENVIRONMENT_RECONNECT_WARNING_GRACE_MS = 2_000; export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); @@ -678,31 +678,15 @@ export function reconcileMountedTerminalThreadIds(input: { activeThreadId: string | null; activeThreadTerminalOpen: boolean; maxHiddenThreadCount?: number; -}): string[] { - return reconcileRetainedMountedThreadIds({ - currentThreadIds: input.currentThreadIds, - openThreadIds: input.openThreadIds, - activeThreadId: input.activeThreadId, - activeThreadOpen: input.activeThreadTerminalOpen, - maxHiddenThreadCount: input.maxHiddenThreadCount ?? MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, - }); -} - -export function reconcileRetainedMountedThreadIds(input: { - currentThreadIds: ReadonlyArray; - openThreadIds: ReadonlyArray; - activeThreadId: string | null; - activeThreadOpen: boolean; - maxHiddenThreadCount: number; - retainInactiveActiveThread?: boolean; }): string[] { const openThreadIdSet = new Set(input.openThreadIds); const hiddenThreadIds = input.currentThreadIds.filter( - (threadId) => - (threadId !== input.activeThreadId || input.retainInactiveActiveThread === true) && - openThreadIdSet.has(threadId), + (threadId) => threadId !== input.activeThreadId && openThreadIdSet.has(threadId), + ); + const maxHiddenThreadCount = Math.max( + 0, + input.maxHiddenThreadCount ?? MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, ); - const maxHiddenThreadCount = Math.max(0, input.maxHiddenThreadCount); const nextThreadIds = hiddenThreadIds.length > maxHiddenThreadCount ? hiddenThreadIds.slice(-maxHiddenThreadCount) @@ -710,7 +694,7 @@ export function reconcileRetainedMountedThreadIds(input: { if ( input.activeThreadId && - input.activeThreadOpen && + input.activeThreadTerminalOpen && !nextThreadIds.includes(input.activeThreadId) ) { nextThreadIds.push(input.activeThreadId); diff --git a/apps/web/src/components/preview/PreviewPanelShell.test.ts b/apps/web/src/components/preview/PreviewPanelShell.test.ts index 23deb066a2ab6514c0c65f674bfc555e395f8f63..765e0b0a1b02592808639dcdef567c12d22fc0a8 100644 --- a/apps/web/src/components/preview/PreviewPanelShell.test.ts +++ b/apps/web/src/components/preview/PreviewPanelShell.test.ts @@ -1,8 +1,6 @@ -import { jsx } from "react/jsx-runtime"; -import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vite-plus/test"; -import { getPreviewPanelMaxWidth, PreviewPanelShell } from "./PreviewPanelShell"; +import { getPreviewPanelMaxWidth } from "./PreviewPanelShell"; describe("getPreviewPanelMaxWidth", () => { it("allows the panel to use 70% of an ultra-wide viewport without a pixel ceiling", () => { @@ -13,14 +11,6 @@ describe("getPreviewPanelMaxWidth", () => { expect(getPreviewPanelMaxWidth(2_001)).toBe(1_400); }); - it("keeps inline panels inside their containing workspace", () => { - const markup = renderToStaticMarkup( - jsx(PreviewPanelShell, { mode: "inline", defaultWidth: 1_000, children: "Panel" }), - ); - - expect(markup).toContain("max-w-full"); - }); - it("reserves the sibling column minimum when the flex row is known", () => { // Fullscreen 14" MacBook: viewport 1512, sidebar ~256 → row of 1256. // The 70% fraction (1058) would leave the chat column only ~198px; diff --git a/apps/web/src/components/settings/settingsLayout.test.tsx b/apps/web/src/components/settings/settingsLayout.test.tsx index 51bb90b33ab31588ccc7518f757653680db484ae..4195a359d1b43c85366c10d1332c95259d3184d6 100644 --- a/apps/web/src/components/settings/settingsLayout.test.tsx +++ b/apps/web/src/components/settings/settingsLayout.test.tsx @@ -1,12 +1,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; -import { - scrollToSettingsTarget, - SettingsRow, - SettingsSearchTargetProvider, - SettingsUnavailableGroup, -} from "./settingsLayout"; +import { scrollToSettingsTarget, SettingsRow, SettingsUnavailableGroup } from "./settingsLayout"; afterEach(() => { vi.unstubAllGlobals(); @@ -27,19 +22,6 @@ describe("unavailable settings", () => { }); describe("settings search targets", () => { - it("does not persist destination styling in the rendered row", () => { - const markup = renderToStaticMarkup( - - - - , - ); - - expect(markup).toContain('id="word-wrap" tabindex="-1"'); - expect(markup).not.toContain("data-settings-search-target"); - expect(markup).not.toContain("settings-search-target-pulse"); - }); - it("scrolls directly to a section header and restarts the destination pulse", () => { const sectionScrollIntoView = vi.fn(); const headerScrollIntoView = vi.fn(); diff --git a/apps/web/src/components/settings/settingsLayout.tsx b/apps/web/src/components/settings/settingsLayout.tsx index 94938b637db77e47358641b1b71595b7145cbaaa..c805e37f1bbd4304c6112ae5af59d70c378e19f9 100644 --- a/apps/web/src/components/settings/settingsLayout.tsx +++ b/apps/web/src/components/settings/settingsLayout.tsx @@ -57,7 +57,7 @@ const SettingsSearchTargetContext = createContext; -} - function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) { return ( - ); -} - function AutocompleteGroup({ className, ...props }: AutocompletePrimitive.Group.Props) { return ( - ); -} - -function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) { - return ; -} - function AutocompleteList({ className, ...props }: AutocompletePrimitive.List.Props) { return ( @@ -216,19 +196,6 @@ function AutocompleteClear({ className, ...props }: AutocompletePrimitive.Clear. ); } -function AutocompleteStatus({ className, ...props }: AutocompletePrimitive.Status.Props) { - return ( - - ); -} - function AutocompleteCollection({ ...props }: AutocompletePrimitive.Collection.Props) { return ; } @@ -249,23 +216,16 @@ function AutocompleteTrigger({ ); } -const useAutocompleteFilter = AutocompletePrimitive.useFilter; - export { Autocomplete, AutocompleteInput, AutocompleteTrigger, AutocompletePopup, AutocompleteItem, - AutocompleteSeparator, AutocompleteGroup, AutocompleteGroupLabel, AutocompleteEmpty, - AutocompleteValue, AutocompleteList, AutocompleteClear, - AutocompleteStatus, - AutocompleteRow, AutocompleteCollection, - useAutocompleteFilter, }; diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx index 863726f32413e9ecd7aadf3c6cf93ea4e03b1f7d..42a3b815e2ff7bed864d2a7334fd3afa88530cf8 100644 --- a/apps/web/src/components/ui/combobox.tsx +++ b/apps/web/src/components/ui/combobox.tsx @@ -28,31 +28,6 @@ function Combobox( ); } -function ComboboxChipsInput({ - className, - size, - ...props -}: Omit & { - size?: "sm" | "default" | "lg" | number; - ref?: React.Ref; -}) { - const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; - - return ( - - ); -} - function ComboboxInput({ className, inputClassName, @@ -240,36 +215,6 @@ function ComboboxItem({ ); } -function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) { - return ( - - ); -} - -function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { - return ( - - ); -} - -function ComboboxGroupLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) { - return ( - - ); -} - function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { return ( ; -} - -function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { - return ; -} - function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { return ( @@ -334,102 +271,19 @@ function ComboboxStatus({ className, ...props }: ComboboxPrimitive.Status.Props) ); } -function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) { - return ; -} - -function ComboboxChips({ - className, - children, - startAddon, - ...props -}: ComboboxPrimitive.Chips.Props & { - startAddon?: React.ReactNode; -}) { - const { chipsRef } = React.use(ComboboxContext); - - return ( - | null} - {...props} - > - {startAddon && ( - - )} - {children} - - ); -} - -function ComboboxChip({ children, ...props }: ComboboxPrimitive.Chip.Props) { - const labelId = React.useId(); - - return ( - - {children} - - - ); -} - -function ComboboxChipRemove({ - labelId, - ...props -}: ComboboxPrimitive.ChipRemove.Props & { labelId: string }) { - const removeLabelId = `${labelId}-remove`; - - return ( - - - Remove - - - ); -} - const useComboboxFilter = ComboboxPrimitive.useFilter; export { Combobox, - ComboboxChipsInput, ComboboxInput, ComboboxSearchInput, ComboboxTrigger, ComboboxPopup, ComboboxItem, - ComboboxSeparator, - ComboboxGroup, - ComboboxGroupLabel, ComboboxEmpty, - ComboboxValue, ComboboxList, ComboboxListVirtualized, ComboboxClear, ComboboxStatus, - ComboboxRow, - ComboboxCollection, - ComboboxChips, - ComboboxChip, useComboboxFilter, }; diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index c1952e9c6f363ca60f7bdb4680eed4ec722a58c5..f8e9b72b1b4cf34f6eaac8f0a05eb7d7efa2ec21 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -7,13 +7,11 @@ import { cn } from "~/lib/utils"; import { Autocomplete, AutocompleteCollection, - AutocompleteEmpty, AutocompleteGroup, AutocompleteGroupLabel, AutocompleteInput, AutocompleteItem, AutocompleteList, - AutocompleteSeparator, } from "~/components/ui/autocomplete"; import { DIALOG_BACKDROP_CLASS, DIALOG_POPUP_CLASS } from "~/components/ui/dialog-styles"; import { Button } from "~/components/ui/button"; @@ -22,8 +20,6 @@ const CommandDialog = CommandDialogPrimitive.Root; const CommandDialogPortal = CommandDialogPrimitive.Portal; -const CommandCreateHandle = CommandDialogPrimitive.createHandle; - function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) { return ; } @@ -135,16 +131,6 @@ function CommandList({ className, ...props }: React.ComponentProps) { - return ( - - ); -} - function CommandPanel({ className, ...props }: React.ComponentProps<"div">) { return (
) { - return ( - - ); -} - function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) { return ( ) { - const defaultProps = { - className: cn( - "relative inline-flex items-center whitespace-nowrap gap-2 rounded-lg border border-input bg-muted not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-muted-foreground text-base sm:text-sm shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:bg-input/64 dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 [&_svg]:-mx-0.5", - className, - ), - "data-slot": "group-text", - }; - return useRender({ - defaultTagName: "div", - props: mergeProps(defaultProps, props), - render, - }); -} - function GroupSeparator({ className, orientation = "vertical", @@ -85,8 +67,6 @@ function GroupSeparator({ export { Group, Group as ButtonGroup, - GroupText, - GroupText as ButtonGroupText, GroupSeparator, GroupSeparator as ButtonGroupSeparator, groupVariants, diff --git a/apps/web/src/components/ui/input-group.tsx b/apps/web/src/components/ui/input-group.tsx index 04e34e5611ee85d3f8cf8e5c553a9d0b22bbda85..9a9228ede61d0f561f763e01b899d7293ebcf3a8 100644 --- a/apps/web/src/components/ui/input-group.tsx +++ b/apps/web/src/components/ui/input-group.tsx @@ -5,7 +5,6 @@ import type * as React from "react"; import { cn } from "~/lib/utils"; import { Input, type InputProps } from "~/components/ui/input"; -import { Textarea, type TextareaProps } from "~/components/ui/textarea"; const inputGroupVariants = cva( "relative inline-flex w-full min-w-0 items-center rounded-[var(--control-radius)] border text-base text-foreground ring-ring/24 transition-shadow has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--control-radius)-1px)]", @@ -90,24 +89,8 @@ function InputGroupAddon({ ); } -function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { - return ( - - ); -} - function InputGroupInput({ className, ...props }: InputProps) { return ; } -function InputGroupTextarea({ className, ...props }: TextareaProps) { - return