1import type { RelayAgentActivityState } from "@t3tools/contracts/relay";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5
6import * as RelayDb from "../db.ts";
7import * as AgentActivityRows from "./AgentActivityRows.ts";
8
9const state: RelayAgentActivityState = {
10 environmentId: "env-1" as RelayAgentActivityState["environmentId"],
11 threadId: "thread-1" as RelayAgentActivityState["threadId"],
12 projectTitle: "Project",
13 threadTitle: "Thread",
14 modelTitle: "gpt-5.4",
15 phase: "running",
16 headline: "Running",
17 updatedAt: "2026-06-20T00:00:00.000Z",
18 deepLink: "/threads/env-1/thread-1",
19};
20
21describe("AgentActivityRows", () => {
22 it.effect("validates database JSON objects and ignores invalid activity rows", () => {
23 const queryRows = [
24 { stateJson: null },
25 { stateJson: JSON.stringify(state) },
26 { stateJson: { ...state, phase: "invalid" } },
27 { stateJson: { ...state, unrelated: "ignored" } },
28 ];
29 const db = {
30 select: () => ({
31 from: () => ({
32 innerJoin: () => ({
33 where: () => ({
34 orderBy: () => Effect.succeed(queryRows),
35 }),
36 }),
37 }),
38 }),
39 } as unknown as RelayDb.RelayDb["Service"];
40
41 return Effect.gen(function* () {
42 const rows = yield* AgentActivityRows.AgentActivityRows;
43 expect(yield* rows.listForUser({ userId: "user-2" })).toEqual([state]);
44 expect(
45 yield* rows.getForUserThread({
46 userId: "user-2",
47 environmentId: "env-1",
48 threadId: "thread-1",
49 }),
50 ).toEqual(state);
51 }).pipe(
52 Effect.provide(
53 AgentActivityRows.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, db))),
54 ),
55 );
56 });
57
58 it.effect("preserves activity context on persistence failures", () => {
59 const cause = new Error("database unavailable");
60 const failingDb = {
61 insert: () => ({
62 values: () => ({
63 onConflictDoUpdate: () => Effect.fail(cause),
64 }),
65 }),
66 delete: () => ({
67 where: () => Effect.fail(cause),
68 }),
69 select: () => ({
70 from: () => ({
71 innerJoin: () => ({
72 where: () => ({
73 orderBy: () => Effect.fail(cause),
74 }),
75 }),
76 }),
77 }),
78 } as unknown as RelayDb.RelayDb["Service"];
79
80 return Effect.gen(function* () {
81 const rows = yield* AgentActivityRows.AgentActivityRows;
82
83 const upsertError = yield* rows
84 .upsert({ environmentPublicKey: "public-key", state })
85 .pipe(Effect.flip);
86 expect(upsertError).toMatchObject({
87 environmentId: "env-1",
88 threadId: "thread-1",
89 cause,
90 });
91 expect(upsertError.message).toBe(
92 "Failed to persist agent activity state for environment env-1, thread thread-1.",
93 );
94
95 const deleteError = yield* rows
96 .remove({
97 environmentId: "env-1",
98 environmentPublicKey: "public-key",
99 threadId: "thread-1",
100 })
101 .pipe(Effect.flip);
102 expect(deleteError).toMatchObject({
103 environmentId: "env-1",
104 threadId: "thread-1",
105 cause,
106 });
107 expect(deleteError.message).toBe(
108 "Failed to delete agent activity state for environment env-1, thread thread-1.",
109 );
110
111 const listError = yield* rows.listForUser({ userId: "user-2" }).pipe(Effect.flip);
112 expect(listError).toMatchObject({ userId: "user-2", cause });
113 expect(listError.message).toBe("Failed to list agent activity state for user user-2.");
114
115 const getError = yield* rows
116 .getForUserThread({
117 userId: "user-2",
118 environmentId: "env-1",
119 threadId: "thread-1",
120 })
121 .pipe(Effect.flip);
122 expect(getError).toMatchObject({ userId: "user-2", cause });
123 }).pipe(
124 Effect.provide(
125 AgentActivityRows.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, failingDb))),
126 ),
127 );
128 });
129});