1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { ProviderDriverKind, ThreadId } from "@t3tools/contracts";
3import { assert, it } from "@effect/vitest";
4import * as Cause from "effect/Cause";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Logger from "effect/Logger";
8import * as Schema from "effect/Schema";
9import * as AcpErrors from "effect-acp/errors";
10
11import type * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
12import { makeAcpNativeLoggerFactory } from "./nativeLogging.ts";
13
14const nodeServicesIt = it.layer(NodeServices.layer);
15const encodeUnknownJson = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
16
17nodeServicesIt("ACP native logging", (it) => {
18 it.effect("records bounded request and protocol diagnostics without raw payloads", () =>
19 Effect.gen(function* () {
20 const records: Array<unknown> = [];
21 const nativeEventLogger: ProviderEventLoggers.EventNdjsonLogger = {
22 filePath: "/tmp/provider-native.ndjson",
23 write: (event) => Effect.sync(() => void records.push(event)),
24 close: () => Effect.void,
25 };
26 const makeLogger = yield* makeAcpNativeLoggerFactory();
27 const logger = makeLogger({
28 nativeEventLogger,
29 provider: ProviderDriverKind.make("cursor"),
30 threadId: ThreadId.make("thread-1"),
31 verboseProtocolLogging: true,
32 });
33 const secret = "secret-token-value";
34 const requestLogger = logger.requestLogger;
35 const protocolLogger = logger.protocolLogging?.logger;
36 assert.exists(requestLogger);
37 assert.exists(protocolLogger);
38 if (!requestLogger || !protocolLogger) return;
39
40 yield* requestLogger({
41 method: "session/prompt",
42 payload: { prompt: secret, sessionId: secret },
43 status: "failed",
44 cause: Cause.fail(AcpErrors.AcpRequestError.internalError(secret, { token: secret })),
45 });
46 yield* protocolLogger({
47 direction: "incoming",
48 stage: "raw",
49 payload: `{"token":"${secret}"}`,
50 });
51 yield* protocolLogger({
52 direction: "outgoing",
53 stage: "decoded",
54 payload: {
55 _tag: "Request",
56 tag: "session/prompt",
57 payload: { prompt: secret },
58 },
59 });
60
61 const serialized = encodeUnknownJson(records);
62 assert.notInclude(serialized, secret);
63 assert.include(serialized, '"method":"session/prompt"');
64 assert.include(serialized, '"errorTag":"AcpRequestError"');
65 assert.include(serialized, '"reasonCount":1');
66 assert.include(serialized, '"valueType":"string"');
67 assert.include(serialized, '"messageTag":"Request"');
68 }),
69 );
70
71 it.effect("keeps request diagnostics without enabling full protocol logging", () =>
72 Effect.gen(function* () {
73 const records: Array<unknown> = [];
74 const makeLogger = yield* makeAcpNativeLoggerFactory();
75 const logger = makeLogger({
76 nativeEventLogger: {
77 filePath: "/tmp/provider-native.ndjson",
78 write: (event) => Effect.sync(() => void records.push(event)),
79 close: () => Effect.void,
80 },
81 provider: ProviderDriverKind.make("grok"),
82 threadId: ThreadId.make("thread-1"),
83 });
84
85 assert.isUndefined(logger.protocolLogging);
86 const requestLogger = logger.requestLogger;
87 assert.exists(requestLogger);
88 if (!requestLogger) return;
89 yield* requestLogger({
90 method: "session/prompt",
91 payload: {},
92 status: "started",
93 });
94 assert.lengthOf(records, 1);
95 }),
96 );
97
98 it.effect("drops transient ACP chunks before formatting verbose protocol logs", () =>
99 Effect.gen(function* () {
100 const records: Array<unknown> = [];
101 const makeLogger = yield* makeAcpNativeLoggerFactory();
102 const logger = makeLogger({
103 nativeEventLogger: {
104 filePath: "/tmp/provider-native.ndjson",
105 write: (event) => Effect.sync(() => void records.push(event)),
106 close: () => Effect.void,
107 },
108 provider: ProviderDriverKind.make("cursor"),
109 threadId: ThreadId.make("thread-1"),
110 verboseProtocolLogging: true,
111 });
112 const protocolLogger = logger.protocolLogging?.logger;
113 assert.exists(protocolLogger);
114 if (!protocolLogger) return;
115
116 for (const updateType of ["agent_message_chunk", "agent_thought_chunk"] as const) {
117 yield* protocolLogger({
118 direction: "incoming",
119 stage: "raw",
120 payload: `${encodeUnknownJson({
121 method: "session/update",
122 params: { update: { sessionUpdate: updateType } },
123 })}\n`,
124 });
125 yield* protocolLogger({
126 direction: "incoming",
127 stage: "decoded",
128 payload: [
129 {
130 _tag: "Request",
131 tag: "session/update",
132 payload: { update: { sessionUpdate: updateType } },
133 },
134 ],
135 });
136 }
137
138 assert.lengthOf(records, 0);
139
140 yield* protocolLogger({
141 direction: "incoming",
142 stage: "decoded",
143 payload: [
144 {
145 _tag: "Request",
146 tag: "session/update",
147 payload: { update: { sessionUpdate: "tool_call" } },
148 },
149 ],
150 });
151 assert.lengthOf(records, 1);
152 }),
153 );
154
155 it.effect("keeps mixed and incomplete raw diagnostics", () =>
156 Effect.gen(function* () {
157 const records: Array<unknown> = [];
158 const makeLogger = yield* makeAcpNativeLoggerFactory();
159 const logger = makeLogger({
160 nativeEventLogger: {
161 filePath: "/tmp/provider-native.ndjson",
162 write: (event) => Effect.sync(() => void records.push(event)),
163 close: () => Effect.void,
164 },
165 provider: ProviderDriverKind.make("cursor"),
166 threadId: ThreadId.make("thread-1"),
167 verboseProtocolLogging: true,
168 });
169 const protocolLogger = logger.protocolLogging?.logger;
170 assert.exists(protocolLogger);
171 if (!protocolLogger) return;
172
173 const transient = encodeUnknownJson({
174 method: "session/update",
175 params: { update: { sessionUpdate: "agent_message_chunk" } },
176 });
177 const lifecycle = encodeUnknownJson({ method: "session/new", params: {} });
178
179 yield* protocolLogger({
180 direction: "incoming",
181 stage: "raw",
182 payload: `${transient}\n${lifecycle}\n`,
183 });
184 yield* protocolLogger({
185 direction: "incoming",
186 stage: "raw",
187 payload: transient,
188 });
189 yield* protocolLogger({
190 direction: "incoming",
191 stage: "raw",
192 payload: `${transient}\n{malformed}\n`,
193 });
194
195 assert.lengthOf(records, 3);
196 }),
197 );
198
199 it.effect("filters transient entries from mixed decoded batches", () =>
200 Effect.gen(function* () {
201 const records: Array<unknown> = [];
202 const makeLogger = yield* makeAcpNativeLoggerFactory();
203 const logger = makeLogger({
204 nativeEventLogger: {
205 filePath: "/tmp/provider-native.ndjson",
206 write: (event) => Effect.sync(() => void records.push(event)),
207 close: () => Effect.void,
208 },
209 provider: ProviderDriverKind.make("grok"),
210 threadId: ThreadId.make("thread-1"),
211 verboseProtocolLogging: true,
212 });
213 const protocolLogger = logger.protocolLogging?.logger;
214 assert.exists(protocolLogger);
215 if (!protocolLogger) return;
216
217 yield* protocolLogger({
218 direction: "incoming",
219 stage: "decoded",
220 payload: [
221 {
222 _tag: "Request",
223 tag: "session/update",
224 payload: { update: { sessionUpdate: "agent_thought_chunk" } },
225 },
226 {
227 _tag: "Request",
228 tag: "session/new",
229 payload: {},
230 },
231 ],
232 });
233
234 assert.lengthOf(records, 1);
235 assert.include(encodeUnknownJson(records), '"itemCount":1');
236 }),
237 );
238
239 it.effect("logs a structural tag when the native writer defects", () => {
240 const messages: Array<unknown> = [];
241 const logCapture = Logger.make<unknown, void>(({ message }) => {
242 if (Array.isArray(message)) {
243 messages.push(...message);
244 } else {
245 messages.push(message);
246 }
247 });
248 const secret = "secret-writer-failure";
249
250 return Effect.gen(function* () {
251 const makeLogger = yield* makeAcpNativeLoggerFactory();
252 const logger = makeLogger({
253 nativeEventLogger: {
254 filePath: "/tmp/provider-native.ndjson",
255 write: () => Effect.die(new Error(secret)),
256 close: () => Effect.void,
257 },
258 provider: ProviderDriverKind.make("cursor"),
259 threadId: ThreadId.make("thread-1"),
260 });
261 const requestLogger = logger.requestLogger;
262 assert.exists(requestLogger);
263 if (!requestLogger) return;
264
265 yield* requestLogger({
266 method: "session/prompt",
267 payload: {},
268 status: "started",
269 });
270
271 const serialized = encodeUnknownJson(messages);
272 assert.notInclude(serialized, secret);
273 assert.include(serialized, '"errorTag":"Die"');
274 assert.include(serialized, '"reasonCount":1');
275 }).pipe(Effect.provide(Logger.layer([logCapture], { mergeWithExisting: false })));
276 });
277
278 it.effect("preserves native writer interruption", () =>
279 Effect.gen(function* () {
280 const makeLogger = yield* makeAcpNativeLoggerFactory();
281 const logger = makeLogger({
282 nativeEventLogger: {
283 filePath: "/tmp/provider-native.ndjson",
284 write: () => Effect.interrupt,
285 close: () => Effect.void,
286 },
287 provider: ProviderDriverKind.make("cursor"),
288 threadId: ThreadId.make("thread-1"),
289 });
290 const requestLogger = logger.requestLogger;
291 assert.exists(requestLogger);
292 if (!requestLogger) return;
293
294 const exit = yield* requestLogger({
295 method: "session/prompt",
296 payload: {},
297 status: "started",
298 }).pipe(Effect.exit);
299
300 assert.isTrue(Exit.isFailure(exit));
301 if (Exit.isFailure(exit)) {
302 assert.isTrue(Cause.hasInterruptsOnly(exit.cause));
303 }
304 }),
305 );
306});