From 2ba8472afb9936ba90fc1c507c4a396ed5355412 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 16:06:42 +0000 Subject: [PATCH 1/3] Use idiomatic Effect filesystem and duration APIs Co-authored-by: Julius Marminge --- .../src/telemetry/AnalyticsService.test.ts | 62 +++++++++++++++++++ apps/server/src/telemetry/AnalyticsService.ts | 5 +- .../src/workspace/WorkspaceFileSystem.test.ts | 8 ++- .../src/workspace/WorkspaceFileSystem.ts | 50 ++++++++------- 4 files changed, 97 insertions(+), 28 deletions(-) diff --git a/apps/server/src/telemetry/AnalyticsService.test.ts b/apps/server/src/telemetry/AnalyticsService.test.ts index d69bab32feb..6378e97e845 100644 --- a/apps/server/src/telemetry/AnalyticsService.test.ts +++ b/apps/server/src/telemetry/AnalyticsService.test.ts @@ -2,8 +2,10 @@ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import * as ConfigProvider from "effect/ConfigProvider"; +import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as TestClock from "effect/testing/TestClock"; import * as HttpServer from "effect/unstable/http/HttpServer"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; @@ -117,4 +119,64 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { ); }), ); + + it.effect("periodic flush runs on the TestClock-controlled interval", () => + Effect.gen(function* () { + const capturedRequests: Array = []; + const receivedRequest = yield* Deferred.make(); + const serverConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { + prefix: "t3-telemetry-interval-", + }); + + const telemetryLayer = AnalyticsService.layer.pipe(Layer.provideMerge(serverConfigLayer)); + const configLayer = ConfigProvider.layer( + ConfigProvider.fromUnknown({ + T3CODE_TELEMETRY_ENABLED: true, + T3CODE_POSTHOG_KEY: "phc_test_key", + T3CODE_POSTHOG_HOST: "", + T3CODE_TELEMETRY_FLUSH_BATCH_SIZE: 20, + }), + ); + const batchServerLayer = HttpServer.serve( + Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + if (request.method !== "POST") { + return HttpServerResponse.empty({ status: 404 }); + } + + const payload = yield* request.json.pipe( + Effect.map((body) => body as RecordedBatchRequest["body"]), + Effect.orElseSucceed(() => null), + ); + + capturedRequests.push({ path: request.url, body: payload }); + yield* Deferred.succeed(receivedRequest, undefined); + + return HttpServerResponse.jsonUnsafe({}); + }), + ); + const runtimeLayer = telemetryLayer.pipe( + Layer.provide(configLayer), + Layer.provideMerge(NodeHttpServer.layerTest), + ); + + yield* Effect.gen(function* () { + yield* Layer.launch(batchServerLayer).pipe(Effect.forkScoped); + const analytics = yield* AnalyticsService.AnalyticsService; + + yield* analytics.record("test.flush.interval", { index: 1 }); + assert.equal(capturedRequests.length, 0); + + yield* TestClock.adjust(AnalyticsService.ANALYTICS_FLUSH_INTERVAL); + yield* Deferred.await(receivedRequest).pipe(Effect.timeout("1 second"), TestClock.withLive); + }).pipe(Effect.provide(runtimeLayer), Effect.provide(TestClock.layer())); + + const batchRequests = capturedRequests.filter( + (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } => + Array.isArray(request.body?.batch), + ); + assert.equal(batchRequests.length, 1); + assert.equal(batchRequests[0]?.body.batch[0]?.event, "test.flush.interval"); + }), + ); }); diff --git a/apps/server/src/telemetry/AnalyticsService.ts b/apps/server/src/telemetry/AnalyticsService.ts index 5fdc7bdeb19..c5625892312 100644 --- a/apps/server/src/telemetry/AnalyticsService.ts +++ b/apps/server/src/telemetry/AnalyticsService.ts @@ -10,6 +10,7 @@ import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/ho import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; @@ -28,6 +29,8 @@ interface BufferedAnalyticsEvent { readonly capturedAt: string; } +export const ANALYTICS_FLUSH_INTERVAL = Duration.seconds(1); + const TelemetryEnvConfig = Config.all({ posthogKey: Config.string("T3CODE_POSTHOG_KEY").pipe( Config.withDefault("phc_XOWci4oZP4VvLiEyrFqkFjP4CZn55mjYYBMREK5Wd6m"), @@ -172,7 +175,7 @@ export const make = Effect.gen(function* () { }, ); - yield* Effect.forever(Effect.sleep(1000).pipe(Effect.flatMap(() => flush)), { + yield* Effect.forever(Effect.sleep(ANALYTICS_FLUSH_INTERVAL).pipe(Effect.flatMap(() => flush)), { disableYield: true, }).pipe(Effect.forkScoped); diff --git a/apps/server/src/workspace/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/WorkspaceFileSystem.test.ts index cecffbc1993..afaaad88198 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.test.ts @@ -1,9 +1,10 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; -import { it, describe, expect } from "@effect/vitest"; +import { assert, it, describe, expect } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; import * as ServerConfig from "../config.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; @@ -185,8 +186,9 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i operationPath: resolvedPath, operation: "realpath-target", }); - expect(error.cause).toBeInstanceOf(Error); - expect((error.cause as NodeJS.ErrnoException).code).toBe("ENOENT"); + assert.instanceOf(error.cause, PlatformError.PlatformError); + assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "NotFound"); + assert.equal((error.cause as PlatformError.PlatformError).method, "realPath"); }), ); }); diff --git a/apps/server/src/workspace/WorkspaceFileSystem.ts b/apps/server/src/workspace/WorkspaceFileSystem.ts index e2dc9cbbb39..825212f86fa 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.ts @@ -140,30 +140,32 @@ export const make = Effect.gen(function* () { relativePath: input.relativePath, }); - const realWorkspaceRoot = yield* Effect.tryPromise({ - try: () => NodeFSP.realpath(input.cwd), - catch: (cause) => - new WorkspaceFileSystemOperationError({ - workspaceRoot: input.cwd, - relativePath: input.relativePath, - resolvedPath: target.absolutePath, - operationPath: input.cwd, - operation: "realpath-workspace-root", - cause, - }), - }); - const realTargetPath = yield* Effect.tryPromise({ - try: () => NodeFSP.realpath(target.absolutePath), - catch: (cause) => - new WorkspaceFileSystemOperationError({ - workspaceRoot: input.cwd, - relativePath: input.relativePath, - resolvedPath: target.absolutePath, - operationPath: target.absolutePath, - operation: "realpath-target", - cause, - }), - }); + const realWorkspaceRoot = yield* fileSystem.realPath(input.cwd).pipe( + Effect.mapError( + (cause) => + new WorkspaceFileSystemOperationError({ + workspaceRoot: input.cwd, + relativePath: input.relativePath, + resolvedPath: target.absolutePath, + operationPath: input.cwd, + operation: "realpath-workspace-root", + cause, + }), + ), + ); + const realTargetPath = yield* fileSystem.realPath(target.absolutePath).pipe( + Effect.mapError( + (cause) => + new WorkspaceFileSystemOperationError({ + workspaceRoot: input.cwd, + relativePath: input.relativePath, + resolvedPath: target.absolutePath, + operationPath: target.absolutePath, + operation: "realpath-target", + cause, + }), + ), + ); const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath); if ( relativeRealPath.startsWith(`..${path.sep}`) || From bed379a80b2ced818fdb559a7367aa9789e9f193 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 16:08:12 +0000 Subject: [PATCH 2/3] Assert platform realPath failure details Co-authored-by: Julius Marminge --- apps/server/src/workspace/WorkspaceFileSystem.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/workspace/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/WorkspaceFileSystem.test.ts index afaaad88198..a1bb94980cc 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.test.ts @@ -188,7 +188,7 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i }); assert.instanceOf(error.cause, PlatformError.PlatformError); assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "NotFound"); - assert.equal((error.cause as PlatformError.PlatformError).method, "realPath"); + assert.equal((error.cause as PlatformError.PlatformError).reason.method, "realPath"); }), ); }); From 7e514b697c438ed74790340019ad66247992a7da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 16:09:50 +0000 Subject: [PATCH 3/3] Merge telemetry test clock layer Co-authored-by: Julius Marminge --- apps/server/src/telemetry/AnalyticsService.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/telemetry/AnalyticsService.test.ts b/apps/server/src/telemetry/AnalyticsService.test.ts index 6378e97e845..3a2cca2397c 100644 --- a/apps/server/src/telemetry/AnalyticsService.test.ts +++ b/apps/server/src/telemetry/AnalyticsService.test.ts @@ -74,6 +74,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { const runtimeLayer = telemetryLayer.pipe( Layer.provide(configLayer), Layer.provideMerge(NodeHttpServer.layerTest), + Layer.provideMerge(TestClock.layer()), ); yield* Effect.gen(function* () { @@ -169,7 +170,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { yield* TestClock.adjust(AnalyticsService.ANALYTICS_FLUSH_INTERVAL); yield* Deferred.await(receivedRequest).pipe(Effect.timeout("1 second"), TestClock.withLive); - }).pipe(Effect.provide(runtimeLayer), Effect.provide(TestClock.layer())); + }).pipe(Effect.provide(runtimeLayer)); const batchRequests = capturedRequests.filter( (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>