import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as DesktopConfig from "./DesktopConfig.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; import * as DesktopObservability from "./DesktopObservability.ts"; const DesktopBackendChildLogRecord = Schema.Struct({ message: Schema.String, level: Schema.Literals(["INFO", "ERROR"]), timestamp: Schema.String, annotations: Schema.Record(Schema.String, Schema.Unknown), spans: Schema.Record(Schema.String, Schema.Unknown), fiberId: Schema.String, }); const decodeDesktopBackendChildLogRecord = Schema.decodeEffect( Schema.fromJsonString(DesktopBackendChildLogRecord), ); const TraceRecordLine = Schema.Struct({ name: Schema.String, attributes: Schema.Record(Schema.String, Schema.Unknown), events: Schema.Array( Schema.Struct({ name: Schema.String, attributes: Schema.Record(Schema.String, Schema.Unknown), }), ), }); const decodeTraceRecordLine = Schema.decodeUnknownSync(Schema.fromJsonString(TraceRecordLine)); const environmentInput = (baseDir: string) => ({ dirname: "/repo/apps/desktop/dist-electron", homeDirectory: baseDir, platform: "darwin", processArch: "arm64", appVersion: "1.2.3", appPath: "/repo", isPackaged: false, resourcesPath: "/repo/resources", runningUnderArm64Translation: false, }) satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; const makeEnvironmentLayer = (baseDir: string) => DesktopEnvironment.layer(environmentInput(baseDir)).pipe( Layer.provide( Layer.mergeAll( NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir, VITE_DEV_SERVER_URL: "http://127.0.0.1:5733", }), ), ), ); describe("DesktopObservability", () => { it.effect("persists desktop Effect logs as span events in desktop.trace.ndjson", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-desktop-observability-test-", }); const environmentLayer = makeEnvironmentLayer(baseDir); const tracePath = yield* Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; return environment.path.join(environment.logDir, "desktop.trace.ndjson"); }).pipe(Effect.provide(environmentLayer)); const logPath = yield* Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; return environment.path.join(environment.logDir, "desktop-main.log"); }).pipe(Effect.provide(environmentLayer)); yield* Effect.scoped( Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ "desktop.test": true }); yield* Effect.logInfo("desktop trace event"); }).pipe( Effect.withSpan("desktop-observability-test"), Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), ), ); const records = (yield* fileSystem.readFileString(tracePath)) .trim() .split("\n") .filter((line) => line.length > 0) .map((line) => decodeTraceRecordLine(line)); const record = records.find((entry) => entry.name === "desktop-observability-test"); assert.notEqual(record, undefined); if (!record) { return; } assert.equal(record.attributes["desktop.test"], true); assert.equal( record.events.some((event) => event.name === "desktop trace event"), true, ); assert.isFalse(yield* fileSystem.exists(logPath)); }).pipe( Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)), ), ); it.effect("persists backend child output as structured JSON records in development", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-desktop-backend-output-log-test-", }); const environmentLayer = makeEnvironmentLayer(baseDir); const logPath = yield* Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; return environment.path.join(environment.logDir, "server-child.log"); }).pipe(Effect.provide(environmentLayer)); yield* Effect.gen(function* () { const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory; const outputLog = yield* factory.forInstance("primary"); yield* outputLog.writeSessionBoundary({ phase: "START", details: "pid=123 port=3773 cwd=/repo", }); yield* outputLog.writeOutputChunk("stdout", new TextEncoder().encode("hello server\n")); }).pipe( Effect.annotateLogs({ runId: "test-run" }), Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), ); const log = yield* fileSystem.readFileString(logPath); const lines = log.trimEnd().split("\n"); const boundary = yield* decodeDesktopBackendChildLogRecord(lines[0] ?? ""); const output = yield* decodeDesktopBackendChildLogRecord(lines[1] ?? ""); assert.equal(boundary.message, "backend child process session start"); assert.equal(boundary.level, "INFO"); assert.equal(boundary.annotations.component, "desktop-backend-child"); assert.equal(boundary.annotations.runId, "test-run"); assert.equal(boundary.annotations.instanceId, "primary"); assert.equal(boundary.annotations.phase, "START"); assert.equal(boundary.annotations.details, "pid=123 port=3773 cwd=/repo"); assert.equal(output.message, "backend child process output"); assert.equal(output.level, "INFO"); assert.equal(output.annotations.component, "desktop-backend-child"); assert.equal(output.annotations.runId, "test-run"); assert.equal(output.annotations.instanceId, "primary"); assert.equal(output.annotations.stream, "stdout"); assert.equal(output.annotations.text, "hello server\n"); }).pipe( Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)), ), ); });