359 lines · 12.8 KB · TypeScript
RawDownload
import * as NodeV8 from "node:v8"; import {  CommandId,  EventId,  MessageId,  ProjectId,  ThreadId,  type OrchestrationEvent,} from "@t3tools/contracts";import { assert, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as SqlClient from "effect/unstable/sql/SqlClient"; import { PersistenceDecodeError } from "../Errors.ts";import { OrchestrationEventStore } from "../Services/OrchestrationEventStore.ts";import { OrchestrationEventStoreLive } from "./OrchestrationEventStore.ts";import { SqlitePersistenceMemory } from "./Sqlite.ts";const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); function messageEvent(threadId: ThreadId, id: string): Omit<OrchestrationEvent, "sequence"> {  const now = "2026-01-01T00:00:00.000Z";  return {    type: "thread.message-sent",    eventId: EventId.make(id),    aggregateKind: "thread",    aggregateId: threadId,    occurredAt: now,    commandId: null,    causationEventId: null,    correlationId: null,    metadata: {},    payload: {      threadId,      messageId: MessageId.make(id),      role: "assistant",      text: id,      turnId: null,      streaming: false,      createdAt: now,      updatedAt: now,    },  };} const layer = it.layer(  OrchestrationEventStoreLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),); layer("OrchestrationEventStore", (it) => {  it.effect("stores json columns as strings and replays CLI-origin events", () =>    Effect.gen(function* () {      const eventStore = yield* OrchestrationEventStore;      const sql = yield* SqlClient.SqlClient;      const now = "2026-01-01T00:00:00.000Z";       const appended = yield* eventStore.append({        type: "project.created",        eventId: EventId.make("evt-store-roundtrip"),        aggregateKind: "project",        aggregateId: ProjectId.make("project-roundtrip"),        occurredAt: now,        commandId: CommandId.make("cmd-store-roundtrip"),        causationEventId: null,        correlationId: CommandId.make("cmd-store-roundtrip"),        metadata: {          adapterKey: "codex",          origin: {            surface: "cli",          },        },        payload: {          projectId: ProjectId.make("project-roundtrip"),          title: "Roundtrip Project",          workspaceRoot: "/tmp/project-roundtrip",          defaultModelSelection: null,          scripts: [],          createdAt: now,          updatedAt: now,        },      });       const storedRows = yield* sql<{        readonly payloadJson: string;        readonly metadataJson: string;      }>`        SELECT          payload_json AS "payloadJson",          metadata_json AS "metadataJson"        FROM orchestration_events        WHERE event_id = ${appended.eventId}      `;      assert.equal(storedRows.length, 1);      assert.equal(typeof storedRows[0]?.payloadJson, "string");      assert.equal(typeof storedRows[0]?.metadataJson, "string");       const replayed = yield* Stream.runCollect(eventStore.readFromSequence(0, 10)).pipe(        Effect.map((chunk) => Array.from(chunk)),      );      assert.equal(replayed.length, 1);      assert.equal(replayed[0]?.type, "project.created");      assert.equal(replayed[0]?.metadata.adapterKey, "codex");      assert.deepEqual(replayed[0]?.metadata.origin, { surface: "cli" });    }),  );   it.effect("fails with PersistenceDecodeError when stored json is invalid", () =>    Effect.gen(function* () {      const eventStore = yield* OrchestrationEventStore;      const sql = yield* SqlClient.SqlClient;      const now = "2026-01-01T00:00:00.000Z";       const invalidRows = yield* sql<{ readonly sequence: number }>`        INSERT INTO orchestration_events (          event_id,          aggregate_kind,          stream_id,          stream_version,          event_type,          occurred_at,          command_id,          causation_event_id,          correlation_id,          actor_kind,          payload_json,          metadata_json        )        VALUES (          ${EventId.make("evt-store-invalid-json")},          ${"project"},          ${ProjectId.make("project-invalid-json")},          ${0},          ${"project.created"},          ${now},          ${CommandId.make("cmd-store-invalid-json")},          ${null},          ${null},          ${"server"},          ${"{"},          ${"{}"}        )        RETURNING sequence      `;       const replayResult = yield* Effect.result(        Stream.runCollect(eventStore.readFromSequence(0, 10)),      );      assert.equal(replayResult._tag, "Failure");      if (replayResult._tag === "Failure") {        assert.ok(isPersistenceDecodeError(replayResult.failure));        assert.ok(          replayResult.failure.operation.includes(            "OrchestrationEventStore.readFromSequence:decodeRows",          ),        );      }      const scopedResult = yield* eventStore        .readAggregateRange({          aggregateKind: "project",          aggregateId: "project-invalid-json",          fromSequenceExclusive: 0,          toSequenceInclusive: invalidRows[0]!.sequence,        })        .pipe(Stream.runCollect, Effect.result);      assert.equal(scopedResult._tag, "Failure");      if (scopedResult._tag === "Failure") {        assert.ok(isPersistenceDecodeError(scopedResult.failure));        assert.ok(          scopedResult.failure.operation.includes(            "OrchestrationEventStore.readAggregateRange:decodeRows",          ),        );      }    }),  );   it.effect("reads one aggregate through the captured head across pruned global gaps", () =>    Effect.gen(function* () {      const store = yield* OrchestrationEventStore;      const sql = yield* SqlClient.SqlClient;      const threadId = ThreadId.make("shared-stream-id");      const first = yield* store.append(messageEvent(threadId, "scoped-first"));      const pruned = yield* store.append(        messageEvent(ThreadId.make("pruned-thread"), "pruned-event"),      );      const second = yield* store.append(messageEvent(threadId, "scoped-second"));      // The same stream ID in a different aggregate is not part of this thread.      // Its invalid JSON must never reach the event decoder.      yield* sql`        INSERT INTO orchestration_events (          event_id, aggregate_kind, stream_id, stream_version, event_type, occurred_at,          actor_kind, payload_json, metadata_json        ) VALUES (          'same-id-project', 'project', ${threadId}, 0, 'project.created',          '2026-01-01T00:00:00.000Z', 'server', '{', '{'        ), (          'unrelated-invalid', 'thread', 'unrelated-invalid-thread', 0, 'thread.activity-appended',          '2026-01-01T00:00:00.000Z', 'server', '{', '{'        )      `;      const last = yield* store.append(messageEvent(threadId, "scoped-last"));      yield* sql`DELETE FROM orchestration_events WHERE sequence = ${pruned.sequence}`;      yield* store.append(messageEvent(threadId, "after-captured-head"));       const events = yield* store        .readAggregateRange({          aggregateKind: "thread",          aggregateId: threadId,          fromSequenceExclusive: first.sequence,          toSequenceInclusive: last.sequence,          limit: 100,        })        .pipe(Stream.runCollect);      assert.deepEqual(        events.map((event) => event.sequence),        [second.sequence, last.sequence],      );    }),  );   it.effect("bounds thread replay metadata and counts UTF-8 bytes without decoding payloads", () =>    Effect.gen(function* () {      const store = yield* OrchestrationEventStore;      const sql = yield* SqlClient.SqlClient;      const rows = yield* sql<{ readonly sequence: number }>`        INSERT INTO orchestration_events (          event_id, aggregate_kind, stream_id, stream_version, event_type, occurred_at,          actor_kind, payload_json, metadata_json        ) VALUES          ('stats-1', 'thread', 'stats-thread', 0, 'thread.message-sent',            '2026-01-01T00:00:00.000Z', 'provider', '{"output":"😀"}', '{}'),          ('stats-unrelated', 'thread', 'another-thread', 0, 'thread.created',            '2026-01-01T00:00:00.000Z', 'provider', printf('%.*c', 10000, 'x'), '{}'),          ('stats-2', 'thread', 'stats-thread', 1, 'thread.activity-appended',            '2026-01-01T00:00:00.000Z', 'provider', '{', '{}'),          ('stats-other-kind', 'project', 'stats-thread', 0, 'project.deleted',            '2026-01-01T00:00:00.000Z', 'provider', printf('%.*c', 20000, 'x'), '{}'),          ('stats-3', 'thread', 'stats-thread', 2, 'thread.deleted',            '2026-01-01T00:00:00.000Z', 'provider', '{"output":"é"}', '{}'),          ('stats-4', 'thread', 'stats-thread', 3, 'thread.created',            '2026-01-01T00:00:00.000Z', 'provider', printf('%.*c', 2000, 'x'), '{}')        RETURNING sequence      `;      const range = {        aggregateKind: "thread" as const,        aggregateId: "stats-thread",        fromSequenceExclusive: 0,        toSequenceInclusive: rows.at(-1)!.sequence,      };      assert.deepEqual(yield* store.getAggregateReplayStats({ ...range, maxEvents: 2 }), {        eventCount: 3,        payloadBytes: 33,        hasCreateEvent: false,      });      assert.deepEqual(yield* store.getAggregateReplayStats({ ...range, maxEvents: 10 }), {        eventCount: 4,        payloadBytes: 2033,        hasCreateEvent: true,      });      assert.deepEqual(        yield* store.getAggregateReplayStats({          ...range,          toSequenceInclusive: rows[2]!.sequence,          maxEvents: 10,        }),        {          eventCount: 2,          payloadBytes: 18,          hasCreateEvent: false,        },      );    }),  );   it.effect("keeps later pages below the captured head when new events are appended", () =>    Effect.gen(function* () {      const store = yield* OrchestrationEventStore;      const threadId = ThreadId.make("paged-thread");      const persisted = yield* Effect.forEach(        Array.from({ length: 502 }, (_, index) => index),        (index) => store.append(messageEvent(threadId, `paged-${index}`)),      );      const head = persisted.at(-1)!.sequence;      let appendedDuringReplay = false;      const replayed = yield* store        .readAggregateRange({          aggregateKind: "thread",          aggregateId: threadId,          fromSequenceExclusive: 0,          toSequenceInclusive: head,          limit: 1_000,        })        .pipe(          Stream.tap(() => {            if (appendedDuringReplay) return Effect.void;            appendedDuringReplay = true;            return store.append(messageEvent(threadId, "appended-during-replay"));          }),          Stream.runCollect,        );      assert.deepEqual(        replayed.map((event) => event.sequence),        persisted.map((event) => event.sequence),      );      const limited = store.readFromSequence(persisted[0]!.sequence, 501.9);      for (let run = 0; run < 2; run++) {        assert.deepEqual(          (yield* Stream.runCollect(limited)).map((event) => event.sequence),          persisted.slice(1).map((event) => event.sequence),        );      }      assert.deepEqual(yield* Stream.runCollect(store.readFromSequence(0, -1)), []);    }),  );}); for (const reader of ["all", "aggregate"] as const) {  it.effect(`releases consumed pages during ${reader} replay`, () =>    Effect.gen(function* () {      const store = yield* OrchestrationEventStore;      const threadId = ThreadId.make(`retention-${reader}`);      yield* Effect.forEach(        Array.from({ length: 1_501 }, (_, index) => index),        (index) => store.append(messageEvent(threadId, `retention-${reader}-${index}`)),        { discard: true },      );      // oxlint-disable-next-line typescript/no-extraneous-class -- Identifies page markers for V8's heap query.      class ReplayPage {}      let count = 0;      const replay =        reader === "all"          ? store.readAll()          : store.readAggregateRange({              aggregateKind: "thread",              aggregateId: threadId,              fromSequenceExclusive: 0,              toSequenceInclusive: 1_501,              limit: 1_501,            });      yield* Stream.runForEach(replay, (event) =>        Effect.sync(() => {          assert.equal(event.sequence, count + 1);          if (count % 500 === 0) {            // Count live page markers after full GC, without timing or heap-size thresholds.            Object.assign(event, { replayPage: new ReplayPage() });            assert.isAtMost(NodeV8.queryObjects(ReplayPage, { format: "count" }), 1);          }          count++;        }),      );      assert.equal(count, 1_501);    }).pipe(      Effect.provide(OrchestrationEventStoreLive.pipe(Layer.provide(SqlitePersistenceMemory))),    ),  );}