Skip to content
Source
Sign inSign up

AntigravityAdapterV2.test.ts

1,276 lines52.2 KB

1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, describe, it } from "@effect/vitest";
4import {
5 ChatFileAttachment,
6 MessageId,
7 NodeId,
8 ProjectId,
9 ProviderInstanceId,
10 ProviderSessionId,
11 RunAttemptId,
12 RunId,
13 ThreadId,
14} from "@t3tools/contracts";
15import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
16import { htmlRenderFromToolItem } from "@t3tools/shared/toolOutput";
17import * as SecureRandom from "effect/crypto/SecureRandom";
18import * as DateTime from "effect/DateTime";
19import * as Deferred from "effect/Deferred";
20import * as Effect from "effect/Effect";
21import * as Exit from "effect/Exit";
22import * as FileSystem from "effect/FileSystem";
23import * as Fiber from "effect/Fiber";
24import * as Layer from "effect/Layer";
25import * as Path from "effect/Path";
26import * as Ref from "effect/Ref";
27import * as Stream from "effect/Stream";
28import { ChildProcessSpawner } from "effect/process";
29import type * as EffectAcpSchema from "effect-acp/compat";
30import * as EffectAcpErrors from "effect-acp/errors";
31​
32import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
33import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
34import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
35import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
36import { makeAntigravityAcpRuntime } from "../../provider/acp/AntigravityAcpSupport.ts";
37import { normalizeAntigravitySessionUpdate } from "../../provider/acp/AntigravityProtocol.ts";
38import { ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE } from "../../provider/antigravityAuthSupport.ts";
39import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
40import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
41import {
42 makeAntigravityAcpAdapterFlavor,
43 makeAntigravityAdapterV2,
44} from "./AntigravityAdapterV2.ts";
45​
46const flavor = makeAntigravityAcpAdapterFlavor({
47 instanceId: ProviderInstanceId.make("antigravity-test"),
48 random: undefined as never,
49 fileSystem: undefined as never,
50 path: undefined as never,
51 host: undefined as never,
52 selfInvocation: undefined as never,
53 makeRuntime: () => Effect.die("not spawned in this test"),
54 withProcess: (_stop, task) => task,
55 defaultModel: Effect.undefined,
56});
57​
58function permissionRequest(
59 toolCallId: string,
60 options: ReadonlyArray<EffectAcpSchema.PermissionOption>,
61): EffectAcpSchema.RequestPermissionRequest {
62 return {
63 sessionId: "session-1",
64 options,
65 toolCall: { toolCallId, title: "Which branch?" },
66 };
68​
69describe("AntigravityAdapterV2 flavor", () => {
70 it("keeps a successful subagent launch running until the parent turn becomes idle", () => {
71 const batch = flavor.extractSubagentUpdate?.({
72 toolCallId: "trajectory:4",
73 title: "Running start_subagent",
74 kind: "other",
75 status: "completed",
76 data: { rawOutput: "Started two agents." },
77 });
78 assert.equal(batch?.status, "running");
79 assert.equal(batch?.prompt, "Started two agents.");
80 assert.isNull(batch?.result);
81 assert.isTrue(flavor.subagentsIdleOnTurnCompletion);
82​
83 // Permission prompts carry the launch; later updates for the same call do not.
84 const launch = flavor.extractSubagentUpdate?.({
85 toolCallId: "35bea692fa95447fbdae6333fec5515f",
86 title: "Run invoke_subagent?",
87 kind: "other",
88 status: "pending",
89 data: {
90 rawInput: {
91 Subagents: [{ TypeName: "research", Role: "Readme Reader", Prompt: "Read README.md" }],
92 },
93 },
94 });
95 assert.equal(launch?.title, "Readme Reader");
96 assert.equal(launch?.prompt, "Read README.md");
97 });
98​
99 it("routes interaction_* permission requests to the question card", () => {
100 const question = flavor.extractPermissionQuestion?.(
101 permissionRequest("interaction_1", [
102 { optionId: "main", name: "main", kind: "allow_once" },
103 { optionId: "dev", name: "dev", kind: "allow_once" },
104 ]),
105 );
106 assert.isDefined(question);
107 assert.deepEqual(
108 question?.question.options.map((option) => option.label),
109 ["main", "dev"],
110 );
111 assert.deepEqual(question?.respond({ interaction_1: "dev" }), {
112 outcome: { outcome: "selected", optionId: "dev" },
113 });
114 assert.isUndefined(question?.respond({ interaction_1: "nope" }));
115 assert.isUndefined(
116 flavor.extractPermissionQuestion?.(
117 permissionRequest("tool_1", [{ optionId: "allow", name: "Allow", kind: "allow_once" }]),
118 ),
119 );
120 });
121​
122 it("keeps the question card to the agent's own options so a typed answer cannot be sent", () => {
123 const question = flavor.extractPermissionQuestion?.(
124 permissionRequest("interaction_2", [
125 { optionId: "opt-main", name: "main", kind: "allow_once" },
126 { optionId: "opt-dev", name: "dev", kind: "allow_once" },
127 ]),
128 );
129 assert.strictEqual(question?.question.allowCustomAnswer, false);
130 assert.deepEqual(
131 question?.question.options.map((option) => option.value),
132 ["opt-main", "opt-dev"],
133 );
134 assert.deepEqual(question?.respond({ interaction_2: "opt-dev" }), {
135 outcome: { outcome: "selected", optionId: "opt-dev" },
136 });
137 });
138});
139​
140const layerSession = Layer.mergeAll(
141 NodeServices.layer,
142 NodeCrypto.layer,
143 IdAllocator.layer,
144 McpProviderSessions.layer,
145 TestProviderHost.layer().pipe(
146 Layer.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
147 ),
148);
149​
150describe("AntigravityAdapterV2 compaction", () => {
151 it.live.each([false, true])(
152 "negotiates native goals (%s) without enabling manual compaction",
153 (nativeGoals) =>
154 Effect.gen(function* () {
155 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
156 const path = yield* Path.Path;
157 const crypto = yield* SecureRandom.SecureRandom;
158 const mockAgentPath = yield* path.fromFileUrl(
159 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
160 );
161 const instanceId = ProviderInstanceId.make("antigravity-compaction-test");
162 const goalSupport = yield* Ref.make(false);
163 const startupMilestone = yield* Deferred.make<void>();
164 const consumeStartupCommands = yield* Deferred.make<void>();
165 const adapter = yield* makeAntigravityAdapterV2({
166 instanceId,
167 selfInvocation: yield* resolveSelfInvocation(),
168 makeRuntime: (input) =>
169 makeAntigravityAcpRuntime({
170 ...input,
171 childProcessSpawner,
172 spawn: {
173 command: process.execPath,
174 args: [mockAgentPath],
175 cwd: input.cwd,
176 env: { T3_ACP_ANTIGRAVITY: "1" },
177 },
178 }).pipe(
179 Effect.provideService(SecureRandom.SecureRandom, crypto),
180 Effect.map((runtime) => ({
181 ...runtime,
182 drainEvents: Deferred.succeed(startupMilestone, undefined).pipe(
183 Effect.andThen(runtime.drainEvents),
184 ),
185 })),
186 ),
187 withProcess: (_stop, task) => task,
188 defaultModel: Effect.undefined,
189 supportsNativeGoals: Ref.get(goalSupport).pipe(
190 Effect.tap(() => Deferred.succeed(startupMilestone, undefined)),
191 ),
192 onSessionEvent: (event) =>
193 event._tag === "AvailableCommandsUpdated"
194 ? Deferred.await(consumeStartupCommands).pipe(
195 Effect.andThen(Ref.set(goalSupport, nativeGoals)),
196 )
197 : Effect.void,
198 });
199 const opening = yield* adapter
200 .openSession({
201 threadId: ThreadId.make("thread-antigravity-compaction"),
202 providerSessionId: ProviderSessionId.make("provider-session-antigravity-compaction"),
203 modelSelection: { instanceId, model: "gemini-test-low" },
204 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
205 runtimeMode: "full-access",
206 interactionMode: "default",
207 cwd: process.cwd(),
208 }),
209 })
210 .pipe(Effect.forkScoped);
211 yield* Deferred.await(startupMilestone);
212 yield* Deferred.succeed(consumeStartupCommands, undefined);
213 const session = yield* Fiber.join(opening);
214 assert.isUndefined(session.compactThread);
215 assert.equal(session.providerSession.capabilities.turns.supportsNativeGoals, nativeGoals);
216 assert.equal((yield* adapter.getCapabilities()).turns.supportsNativeGoals, nativeGoals);
217 }).pipe(Effect.provide(layerSession)),
218 );
219});
220​
221describe("AntigravityAdapterV2 session open", () => {
222 it.effect.each([
223 {
224 name: "browser sign-in",
225 cause: new EffectAcpErrors.AcpTransportError({
226 detail: ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE,
227 cause: undefined,
228 }),
229 requiresSignIn: true,
230 },
231 {
232 name: "ACP authentication required",
233 cause: EffectAcpErrors.AcpRequestError.authRequired(),
234 requiresSignIn: true,
235 },
236 {
237 name: "transport failure",
238 cause: new EffectAcpErrors.AcpTransportError({
239 detail: "Connection closed",
240 cause: undefined,
241 }),
242 requiresSignIn: undefined,
243 },
244 ] as const)("classifies $name during session open", ({ cause, requiresSignIn }) =>
245 Effect.gen(function* () {
246 const instanceId = ProviderInstanceId.make("antigravity-sign-in-test");
247 const adapter = yield* makeAntigravityAdapterV2({
248 instanceId,
249 selfInvocation: yield* resolveSelfInvocation(),
250 makeRuntime: () => Effect.fail(cause),
251 withProcess: (_stop, task) => task,
252 defaultModel: Effect.undefined,
253 });
254 const threadId = ThreadId.make("thread-antigravity-sign-in");
255 const error = yield* adapter
256 .openSession({
257 threadId,
258 providerSessionId: ProviderSessionId.make("provider-session-antigravity-sign-in"),
259 modelSelection: { instanceId, model: "gemini-test-low" },
260 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
261 runtimeMode: "full-access",
262 interactionMode: "default",
263 cwd: process.cwd(),
264 }),
265 })
266 .pipe(Effect.flip);
267​
268 assert.equal(error._tag, "ProviderAdapterOpenSessionError");
269 assert.equal(
270 error._tag === "ProviderAdapterOpenSessionError" && error.requiresSignIn,
271 requiresSignIn,
272 );
273 }).pipe(Effect.provide(layerSession)),
274 );
275});
276​
277describe("AntigravityAdapterV2 client file system", () => {
278 it.effect("serves confined reads and leaves edits to the agent's diff tools", () =>
279 Effect.gen(function* () {
280 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
281 const fileSystem = yield* FileSystem.FileSystem;
282 const path = yield* Path.Path;
283 const host = yield* ProviderHost.ProviderHost;
284 const mockAgentPath = yield* path.fromFileUrl(
285 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
286 );
287 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
288 // effect-acp keeps the last handler registered per method; so does this.
289 let readTextFile: Parameters<RuntimeService["handleReadTextFile"]>[0] | undefined;
290 let writeTextFile: Parameters<RuntimeService["handleWriteTextFile"]>[0] | undefined;
291 const logs = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-antigravity-log-" });
292 const requestLog = path.join(logs, "requests.ndjson");
293 const crypto = yield* SecureRandom.SecureRandom;
294 const instanceId = ProviderInstanceId.make("antigravity-containment-test");
295 const adapter = yield* makeAntigravityAdapterV2({
296 instanceId,
297 selfInvocation: yield* resolveSelfInvocation(),
298 makeRuntime: (input) =>
299 makeAntigravityAcpRuntime({
300 ...input,
301 childProcessSpawner,
302 spawn: {
303 command: process.execPath,
304 args: [mockAgentPath],
305 cwd: input.cwd,
306 env: { T3_ACP_ANTIGRAVITY: "1", T3_ACP_REQUEST_LOG_PATH: requestLog },
307 },
308 }).pipe(
309 Effect.provideService(SecureRandom.SecureRandom, crypto),
310 Effect.map((runtime): RuntimeService => ({
311 ...runtime,
312 handleReadTextFile: (handler) =>
313 Effect.sync(() => {
314 readTextFile = handler;
315 }).pipe(Effect.andThen(runtime.handleReadTextFile(handler))),
316 handleWriteTextFile: (handler) =>
317 Effect.sync(() => {
318 writeTextFile = handler;
319 }).pipe(Effect.andThen(runtime.handleWriteTextFile(handler))),
320 })),
321 ),
322 withProcess: (_stop, task) => task,
323 defaultModel: Effect.undefined,
324 });
325 const workspace = yield* fileSystem.makeTempDirectoryScoped({
326 prefix: "t3-antigravity-workspace-",
327 });
328 const outside = yield* fileSystem.makeTempDirectoryScoped({
329 prefix: "t3-antigravity-outside-",
330 });
331 const outsideFile = path.join(outside, "secret.txt");
332 yield* fileSystem.writeFileString(outsideFile, "secret");
333 const attachment = path.join(host.paths.attachmentsDir, "pasted.txt");
334 yield* fileSystem.writeFileString(attachment, "pasted");
335​
336 const threadId = ThreadId.make("thread-antigravity-containment");
337 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
338 runtimeMode: "full-access",
339 interactionMode: "default",
340 cwd: workspace,
341 });
342 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
343 const session = yield* adapter.openSession({
344 threadId,
345 providerSessionId: ProviderSessionId.make("provider-session-antigravity-containment"),
346 modelSelection,
347 runtimePolicy,
348 });
349 yield* session.ensureThread({ threadId, modelSelection, runtimePolicy });
350 const initialize = (yield* fileSystem.readFileString(requestLog))
351 .trim()
352 .split("\n")
353 .map(
354 (line) =>
355 JSON.parse(line) as {
356 method?: string;
357 params?: { clientCapabilities?: { fs?: unknown }; clientInfo?: { name?: string } };
358 },
359 )
360 .find((request) => request.method === "initialize");
361 assert.deepEqual(initialize?.params?.clientCapabilities?.fs, {
362 readTextFile: true,
363 writeTextFile: false,
364 });
365 assert.equal(initialize?.params?.clientInfo?.name, "zed");
366 assert.isUndefined(writeTextFile);
367 if (readTextFile === undefined) {
368 return yield* Effect.die("Antigravity sessions must serve client file reads");
369 }
370 const context = (method: string) => ({ requestId: `test-${method}`, method });
371​
372 const insidePath = path.join(workspace, "src", "inside.ts");
373 yield* fileSystem.makeDirectory(path.dirname(insidePath), { recursive: true });
374 yield* fileSystem.writeFileString(insidePath, "inside");
375 const pasted = yield* readTextFile(
376 { sessionId: "mock-session-1", path: attachment },
377 context("fs/read_text_file"),
378 );
379 assert.equal(pasted.content, "pasted");
380 yield* fileSystem.writeFileString(insidePath, "\uFEFFfirst\né中🙂\nlast");
381 const sliced = yield* readTextFile(
382 { sessionId: "mock-session-1", path: insidePath, line: 2, limit: 1 },
383 context("fs/read_text_file"),
384 );
385 assert.equal(sliced.content, "é中🙂");
386 yield* fileSystem.writeFileString(insidePath, "inside");
387 for (const [name, bytes] of [
388 ["image.png", new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
389 ["data.bin", new Uint8Array([0x61, 0, 0x62])],
390 ] as const) {
391 const binaryPath = path.join(workspace, name);
392 yield* fileSystem.writeFile(binaryPath, bytes);
393 const binaryRead = yield* readTextFile(
394 { sessionId: "mock-session-1", path: binaryPath },
395 context("fs/read_text_file"),
396 ).pipe(Effect.flip);
397 assert.propertyVal(binaryRead, "code", -32602, `${name} is refused as binary`);
398 }
399​
400 const outsideRead = yield* readTextFile(
401 { sessionId: "mock-session-1", path: outsideFile },
402 context("fs/read_text_file"),
403 ).pipe(Effect.exit);
404 assert.isTrue(Exit.isFailure(outsideRead));
405 const linkPath = path.join(workspace, "linked-secret.txt");
406 yield* fileSystem.symlink(outsideFile, linkPath);
407 const linkedRead = yield* readTextFile(
408 { sessionId: "mock-session-1", path: linkPath },
409 context("fs/read_text_file"),
410 ).pipe(Effect.exit);
411 assert.isTrue(Exit.isFailure(linkedRead), "a read through an escaping symlink is denied");
412 const insideLink = path.join(workspace, "inside-link.ts");
413 yield* fileSystem.symlink(insidePath, insideLink);
414 const viaInsideLink = yield* readTextFile(
415 { sessionId: "mock-session-1", path: insideLink },
416 context("fs/read_text_file"),
417 );
418 assert.equal(viaInsideLink.content, "inside");
419 }).pipe(Effect.provide(layerSession)),
420 );
421});
422​
423describe("AntigravityAdapterV2 workspace changes", () => {
424 it.effect("confines file requests to the workspace of the turn in progress", () =>
425 Effect.gen(function* () {
426 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
427 const fileSystem = yield* FileSystem.FileSystem;
428 const path = yield* Path.Path;
429 const crypto = yield* SecureRandom.SecureRandom;
430 const mockAgentPath = yield* path.fromFileUrl(
431 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
432 );
433 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
434 const promptReceived = yield* Deferred.make<Parameters<RuntimeService["prompt"]>[0]>();
435 const host = yield* ProviderHost.ProviderHost;
436 const document = ChatFileAttachment.make({
437 type: "file",
438 id: "native-pdf",
439 name: "diagram.pdf",
440 mimeType: "application/pdf",
441 sizeBytes: 9,
442 });
443 const documentPath = host.resolveAttachmentPath(document)!;
444 yield* fileSystem.writeFileString(documentPath, "%PDF-1.7");
445 let readTextFile: Parameters<RuntimeService["handleReadTextFile"]>[0] | undefined;
446 const instanceId = ProviderInstanceId.make("antigravity-workspace-change-test");
447 const adapter = yield* makeAntigravityAdapterV2({
448 instanceId,
449 selfInvocation: yield* resolveSelfInvocation(),
450 makeRuntime: (input) =>
451 makeAntigravityAcpRuntime({
452 ...input,
453 childProcessSpawner,
454 spawn: {
455 command: process.execPath,
456 args: [mockAgentPath],
457 cwd: input.cwd,
458 env: { T3_ACP_ANTIGRAVITY: "1", T3_ACP_HANG_PROMPT_FOREVER: "1" },
459 },
460 }).pipe(
461 Effect.provideService(SecureRandom.SecureRandom, crypto),
462 Effect.map((runtime): RuntimeService => ({
463 ...runtime,
464 prompt: (request) =>
465 Deferred.succeed(promptReceived, request).pipe(
466 Effect.andThen(runtime.prompt(request)),
467 ),
468 start: () =>
469 runtime.start().pipe(
470 Effect.map((started) => ({
471 ...started,
472 initializeResult: {
473 ...started.initializeResult,
474 agentCapabilities: {
475 ...started.initializeResult.agentCapabilities,
476 promptCapabilities: { image: true, audio: true, embeddedContext: true },
477 },
478 },
479 })),
480 ),
481 handleReadTextFile: (handler) =>
482 Effect.sync(() => {
483 readTextFile = handler;
484 }).pipe(Effect.andThen(runtime.handleReadTextFile(handler))),
485 })),
486 ),
487 withProcess: (_stop, task) => task,
488 defaultModel: Effect.undefined,
489 });
490 const workspaceA = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ag-a-" });
491 const workspaceB = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ag-b-" });
492 yield* fileSystem.writeFileString(path.join(workspaceA, "a.txt"), "from a");
493 yield* fileSystem.writeFileString(path.join(workspaceB, "b.txt"), "from b");
494 const policyFor = (cwd: string) =>
495 ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
496 runtimeMode: "full-access",
497 interactionMode: "default",
498 cwd,
499 });
500 const threadId = ThreadId.make("thread-antigravity-workspace-change");
501 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
502 const session = yield* adapter.openSession({
503 threadId,
504 providerSessionId: ProviderSessionId.make("provider-session-antigravity-workspace-change"),
505 modelSelection,
506 runtimePolicy: policyFor(workspaceA),
507 });
508 const providerThread = yield* session.ensureThread({
509 threadId,
510 modelSelection,
511 runtimePolicy: policyFor(workspaceA),
512 });
513 const now = yield* DateTime.now;
514 yield* session
515 .startTurn({
516 appThread: {
517 createdBy: "user",
518 creationSource: "web",
519 id: threadId,
520 projectId: ProjectId.make("project-antigravity-workspace-change"),
521 title: "Antigravity workspace change",
522 providerInstanceId: instanceId,
523 modelSelection,
524 runtimeMode: "full-access",
525 interactionMode: "default",
526 branch: null,
527 worktreePath: workspaceB,
528 activeProviderThreadId: providerThread.id,
529 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
530 forkedFrom: null,
531 createdAt: now,
532 updatedAt: now,
533 archivedAt: null,
534 settledOverride: null,
535 settledAt: null,
536 lastVisitedAt: null,
537 deletedAt: null,
538 },
539 threadId,
540 runId: RunId.make("run-antigravity-workspace-change"),
541 runOrdinal: 1,
542 providerTurnOrdinal: 1,
543 attemptId: RunAttemptId.make("attempt-antigravity-workspace-change"),
544 rootNodeId: NodeId.make("node-antigravity-workspace-change"),
545 providerThread,
546 message: {
547 createdBy: "user",
548 creationSource: "web",
549 messageId: MessageId.make("message-antigravity-workspace-change"),
550 text: "read b.txt",
551 attachments: [document],
552 },
553 modelSelection,
554 runtimePolicy: policyFor(workspaceB),
555 })
556 .pipe(Effect.forkScoped);
557 yield* session.events.pipe(
558 Stream.filter((event) => event.type === "provider_turn.updated"),
559 Stream.runHead,
560 );
561 const sent = yield* Deferred.await(promptReceived);
562 assert.deepEqual(
563 sent.prompt.filter((block) => block.type === "resource_link"),
564 [
565 {
566 type: "resource_link",
567 uri: (yield* path.toFileUrl(documentPath)).href,
568 name: "diagram.pdf",
569 mimeType: "application/pdf",
570 size: 8,
571 },
572 ],
573 );
574 assert.include(JSON.stringify(sent.prompt), JSON.stringify(documentPath).slice(1, -1));
575 if (readTextFile === undefined) {
576 return yield* Effect.die("Antigravity sessions must serve client file requests");
577 }
578 const context = { requestId: "test-read", method: "fs/read_text_file" };
579 const fromB = yield* readTextFile(
580 { sessionId: "mock-session-1", path: path.join(workspaceB, "b.txt") },
581 context,
582 );
583 assert.equal(fromB.content, "from b");
584 const fromA = yield* readTextFile(
585 { sessionId: "mock-session-1", path: path.join(workspaceA, "a.txt") },
586 context,
587 ).pipe(Effect.exit);
588 assert.isTrue(Exit.isFailure(fromA), "the previous workspace is no longer readable");
589 }).pipe(Effect.provide(layerSession)),
590 );
591});
592​
593describe("AntigravityAdapterV2 html_render pages", () => {
594 it.effect("shows the page html_render published when the agent reports only a summary", () =>
595 Effect.gen(function* () {
596 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
597 const path = yield* Path.Path;
598 const crypto = yield* SecureRandom.SecureRandom;
599 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
600 const mockAgentPath = yield* path.fromFileUrl(
601 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
602 );
603 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
604 let sessionUpdate: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
605 const instanceId = ProviderInstanceId.make("antigravity-html-render-test");
606 const threadId = ThreadId.make("thread-antigravity-html-render");
607 const page = (name: string) => ({
608 attachmentId: `${threadId}-${name}-html`,
609 title: "Revenue",
610 height: 240,
611 });
612 // Long enough that Antigravity bounds the html the adapter sees.
613 const rows = Array.from({ length: 500 }, (_, row) => `<tr><td>Row ${row}</td></tr>`);
614 const report = { html: `<table>${rows.join("")}</table>`, title: "Revenue", height: 240 };
615 const failed = { ...report, html: "<p>Revenue</p>" };
616 const errored = { ...report, html: "<p>Revenue error</p>" };
617 const adapter = yield* makeAntigravityAdapterV2({
618 instanceId,
619 selfInvocation: yield* resolveSelfInvocation(),
620 makeRuntime: (input) =>
621 makeAntigravityAcpRuntime({
622 ...input,
623 childProcessSpawner,
624 spawn: {
625 command: process.execPath,
626 args: [mockAgentPath],
627 cwd: input.cwd,
628 env: { T3_ACP_ANTIGRAVITY: "1" },
629 },
630 }).pipe(
631 Effect.provideService(SecureRandom.SecureRandom, crypto),
632 Effect.map((runtime): RuntimeService => ({
633 ...runtime,
634 handleSessionUpdate: (handler) =>
635 Effect.sync(() => {
636 sessionUpdate = handler;
637 }).pipe(Effect.andThen(runtime.handleSessionUpdate(handler))),
638 // agy_acp_server 1.3/1.4 without a permission prompt: the call is
639 // titled "Running <tool>" and completes with the step's summary
640 // in place of the MCP result.
641 prompt: () =>
642 Effect.gen(function* () {
643 const send = (update: EffectAcpSchema.SessionUpdate) =>
644 sessionUpdate === undefined
645 ? Effect.die("the adapter must handle session updates")
646 : sessionUpdate(
647 normalizeAntigravitySessionUpdate({
648 sessionId: "mock-session-1",
649 update,
650 }),
651 );
652 const calls = [
653 ["published", report],
654 ["unpublished", { ...report, html: "<p>Costs</p>" }],
655 ["failed", failed],
656 ["error", errored],
657 ] as const;
658 for (const [toolCallId, rawInput] of calls) {
659 yield* send({
660 sessionUpdate: "tool_call",
661 toolCallId,
662 title: "Running html_render",
663 kind: "other",
664 status: "in_progress",
665 rawInput,
666 });
667 }
668 yield* mcpSessions.recordHtmlRender(threadId, report, page("report"));
669 yield* mcpSessions.recordHtmlRender(threadId, failed, page("failed"));
670 yield* mcpSessions.recordHtmlRender(threadId, errored, page("error"));
671 for (const [toolCallId, status] of [
672 ["unpublished", "completed"],
673 ["failed", "failed"],
674 ["published", "completed"],
675 ["published", "completed"],
676 ] as const) {
677 yield* send({
678 sessionUpdate: "tool_call_update",
679 toolCallId,
680 status,
681 rawOutput: `Summary of ${toolCallId}`,
682 });
683 }
684 yield* send({
685 sessionUpdate: "tool_call_update",
686 toolCallId: "error",
687 status: "completed",
688 rawOutput: { result: { isError: true, content: [] } },
689 });
690 for (const [name, rawInput] of [
691 ["failed", failed],
692 ["error", errored],
693 ] as const) {
694 const toolCallId = `${name}-retry`;
695 yield* send({
696 sessionUpdate: "tool_call",
697 toolCallId,
698 title: "Running html_render",
699 kind: "other",
700 status: "in_progress",
701 rawInput,
702 });
703 yield* mcpSessions.recordHtmlRender(threadId, rawInput, page(toolCallId));
704 yield* send({
705 sessionUpdate: "tool_call_update",
706 toolCallId: name,
707 status: name === "failed" ? "failed" : "completed",
708 rawOutput:
709 name === "failed"
710 ? "Summary of failed"
711 : { result: { isError: true, content: [] } },
712 });
713 yield* send({
714 sessionUpdate: "tool_call_update",
715 toolCallId,
716 status: "completed",
717 rawOutput: `Summary of ${toolCallId}`,
718 });
719 }
720 yield* mcpSessions.recordHtmlRender(threadId, report, page("replacement"));
721 yield* send({
722 sessionUpdate: "tool_call_update",
723 toolCallId: "published",
724 status: "completed",
725 rawOutput: { htmlRender: page("replacement") },
726 });
727 yield* send({
728 sessionUpdate: "tool_call_update",
729 toolCallId: "published",
730 status: "completed",
731 rawOutput: "Summary of published",
732 });
733 return { stopReason: "end_turn" as const };
734 }),
735 })),
736 ),
737 withProcess: (_stop, task) => task,
738 defaultModel: Effect.undefined,
739 });
740 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
741 runtimeMode: "full-access",
742 interactionMode: "default",
743 cwd: process.cwd(),
744 });
745 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
746 const session = yield* adapter.openSession({
747 threadId,
748 providerSessionId: ProviderSessionId.make("provider-session-antigravity-html-render"),
749 modelSelection,
750 runtimePolicy,
751 });
752 const providerThread = yield* session.ensureThread({
753 threadId,
754 modelSelection,
755 runtimePolicy,
756 });
757 const now = yield* DateTime.now;
758 yield* session.startTurn({
759 appThread: {
760 createdBy: "user",
761 creationSource: "web",
762 id: threadId,
763 projectId: ProjectId.make("project-antigravity-html-render"),
764 title: "Antigravity html_render",
765 providerInstanceId: instanceId,
766 modelSelection,
767 runtimeMode: "full-access",
768 interactionMode: "default",
769 branch: null,
770 worktreePath: process.cwd(),
771 activeProviderThreadId: providerThread.id,
772 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
773 forkedFrom: null,
774 createdAt: now,
775 updatedAt: now,
776 archivedAt: null,
777 settledOverride: null,
778 settledAt: null,
779 lastVisitedAt: null,
780 deletedAt: null,
781 },
782 threadId,
783 runId: RunId.make("run-antigravity-html-render"),
784 runOrdinal: 1,
785 providerTurnOrdinal: 1,
786 attemptId: RunAttemptId.make("attempt-antigravity-html-render"),
787 rootNodeId: NodeId.make("node-antigravity-html-render"),
788 providerThread,
789 message: {
790 createdBy: "user",
791 creationSource: "web",
792 messageId: MessageId.make("message-antigravity-html-render"),
793 text: "chart the revenue",
794 attachments: [],
795 },
796 modelSelection,
797 runtimePolicy,
798 });
799 const events = Array.from(
800 yield* session.events.pipe(
801 Stream.takeUntil((event) => event.type === "turn.terminal"),
802 Stream.runCollect,
803 ),
804 );
805 const tool = (nativeId: string) => {
806 const event = events.findLast(
807 (event) =>
808 event.type === "turn_item.updated" &&
809 event.turnItem.nativeItemRef?.nativeId === nativeId,
810 );
811 if (event?.type !== "turn_item.updated" || event.turnItem.type !== "dynamic_tool") {
812 return undefined;
813 }
814 const item = event.turnItem;
815 return {
816 status: item.status,
817 toolName: item.toolName,
818 page: htmlRenderFromToolItem(item),
819 output: item.output,
820 };
821 };
822​
823 assert.deepEqual(tool("published"), {
824 status: "completed",
825 toolName: "t3-code.html_render",
826 page: page("replacement"),
827 output: { htmlRender: page("replacement") },
828 });
829 assert.deepEqual(tool("unpublished"), {
830 status: "completed",
831 toolName: "t3-code.html_render",
832 page: undefined,
833 output: "Summary of unpublished",
834 });
835 assert.deepEqual(tool("failed"), {
836 status: "failed",
837 toolName: "t3-code.html_render",
838 page: undefined,
839 output: "Summary of failed",
840 });
841 assert.deepEqual(tool("error"), {
842 status: "completed",
843 toolName: "t3-code.html_render",
844 page: undefined,
845 output: [],
846 });
847 assert.deepEqual(
848 events.flatMap((event) =>
849 event.type === "turn_item.updated" &&
850 event.turnItem.type === "dynamic_tool" &&
851 event.turnItem.nativeItemRef?.nativeId === "published" &&
852 event.turnItem.status === "completed"
853 ? [htmlRenderFromToolItem(event.turnItem)]
854 : [],
855 ),
856 [page("report"), page("report"), page("replacement"), page("replacement")],
857 );
858 for (const name of ["failed-retry", "error-retry"]) {
859 assert.deepEqual(tool(name), {
860 status: "completed",
861 toolName: "t3-code.html_render",
862 page: page(name),
863 output: { htmlRender: page(name) },
864 });
865 }
866 assert.strictEqual(yield* mcpSessions.takeHtmlRender(threadId, report), undefined);
867 assert.strictEqual(yield* mcpSessions.takeHtmlRender(threadId, failed), undefined);
868 assert.strictEqual(yield* mcpSessions.takeHtmlRender(threadId, errored), undefined);
869 }).pipe(Effect.provide(layerSession)),
870 );
871});
872​
873describe("AntigravityAdapterV2 subagent reads", () => {
874 it.effect("completes a read T3 served when the agent later fails it with a step error", () =>
875 Effect.gen(function* () {
876 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
877 const fileSystem = yield* FileSystem.FileSystem;
878 const path = yield* Path.Path;
879 const crypto = yield* SecureRandom.SecureRandom;
880 const mockAgentPath = yield* path.fromFileUrl(
881 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
882 );
883 const workspace = yield* fileSystem.makeTempDirectoryScoped({
884 prefix: "t3-antigravity-reads-",
885 });
886 const outside = yield* fileSystem.makeTempDirectoryScoped({
887 prefix: "t3-antigravity-reads-outside-",
888 });
889 const served = path.join(workspace, "served.ts");
890 yield* fileSystem.writeFileString(served, "export {};");
891 const denied = path.join(outside, "secret.txt");
892 yield* fileSystem.writeFileString(denied, "secret");
893 const unseen = path.join(workspace, "unseen.ts");
894 const retried = path.join(workspace, "retried.ts");
895 const stepError = 'Model produced invalid output. ("model output error")';
896 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
897 let sessionUpdate: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
898 let readTextFile: Parameters<RuntimeService["handleReadTextFile"]>[0] | undefined;
899 const instanceId = ProviderInstanceId.make("antigravity-subagent-reads-test");
900 const threadId = ThreadId.make("thread-antigravity-subagent-reads");
901 const adapter = yield* makeAntigravityAdapterV2({
902 instanceId,
903 selfInvocation: yield* resolveSelfInvocation(),
904 makeRuntime: (input) =>
905 makeAntigravityAcpRuntime({
906 ...input,
907 childProcessSpawner,
908 spawn: {
909 command: process.execPath,
910 args: [mockAgentPath],
911 cwd: input.cwd,
912 env: { T3_ACP_ANTIGRAVITY: "1" },
913 },
914 }).pipe(
915 Effect.provideService(SecureRandom.SecureRandom, crypto),
916 Effect.map((runtime): RuntimeService => ({
917 ...runtime,
918 handleSessionUpdate: (handler) =>
919 Effect.sync(() => {
920 sessionUpdate = handler;
921 }).pipe(Effect.andThen(runtime.handleSessionUpdate(handler))),
922 handleReadTextFile: (handler) =>
923 Effect.sync(() => {
924 readTextFile = handler;
925 }).pipe(Effect.andThen(runtime.handleReadTextFile(handler))),
926 // agy_acp_server announces a subagent's client_view_file on the parent session,
927 // never completes it, then fails every open call with the next step error.
928 prompt: () =>
929 Effect.gen(function* () {
930 if (sessionUpdate === undefined || readTextFile === undefined) {
931 return yield* Effect.die("the adapter must handle updates and reads");
932 }
933 const send = sessionUpdate;
934 const handleRead = readTextFile;
935 const update = (update: EffectAcpSchema.SessionUpdate) =>
936 send(
937 normalizeAntigravitySessionUpdate({ sessionId: "mock-session-1", update }),
938 );
939 // The agent's own view_file never reaches T3, so it must not take a served read.
940 const reads = [
941 ["call_native", served, "Running view_file"],
942 ["call_served", served, "Running client_view_file"],
943 ["call_missing", retried, "Running client_view_file"],
944 ["call_retried", retried, "Running client_view_file"],
945 ["call_denied", denied, "Running client_view_file"],
946 ["call_unseen", unseen, "Running client_view_file"],
947 ] as const;
948 for (const [toolCallId, target, title] of reads) {
949 yield* update({
950 sessionUpdate: "tool_call",
951 toolCallId,
952 title,
953 kind: "read",
954 status: "in_progress",
955 rawInput: { absolute_path: target },
956 locations: [{ path: target }],
957 });
958 }
959 const serve = (target: string) =>
960 handleRead(
961 { sessionId: "subagent-conversation", path: target },
962 { requestId: `read-${target}`, method: "fs/read_text_file" },
963 ).pipe(Effect.exit);
964 yield* serve(served);
965 yield* serve(retried);
966 yield* fileSystem.writeFileString(retried, "export {};").pipe(Effect.orDie);
967 yield* serve(retried);
968 yield* serve(denied);
969 for (const [toolCallId] of reads) {
970 yield* update({
971 sessionUpdate: "tool_call_update",
972 toolCallId,
973 status: "failed",
974 rawOutput: stepError,
975 });
976 }
977 return { stopReason: "end_turn" as const };
978 }),
979 })),
980 ),
981 withProcess: (_stop, task) => task,
982 defaultModel: Effect.undefined,
983 });
984 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
985 runtimeMode: "full-access",
986 interactionMode: "default",
987 cwd: workspace,
988 });
989 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
990 const session = yield* adapter.openSession({
991 threadId,
992 providerSessionId: ProviderSessionId.make("provider-session-antigravity-subagent-reads"),
993 modelSelection,
994 runtimePolicy,
995 });
996 const providerThread = yield* session.ensureThread({
997 threadId,
998 modelSelection,
999 runtimePolicy,
1000 });
1001 const now = yield* DateTime.now;
1002 yield* session.startTurn({
1003 appThread: {
1004 createdBy: "user",
1005 creationSource: "web",
1006 id: threadId,
1007 projectId: ProjectId.make("project-antigravity-subagent-reads"),
1008 title: "Antigravity subagent reads",
1009 providerInstanceId: instanceId,
1010 modelSelection,
1011 runtimeMode: "full-access",
1012 interactionMode: "default",
1013 branch: null,
1014 worktreePath: workspace,
1015 activeProviderThreadId: providerThread.id,
1016 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
1017 forkedFrom: null,
1018 createdAt: now,
1019 updatedAt: now,
1020 archivedAt: null,
1021 settledOverride: null,
1022 settledAt: null,
1023 lastVisitedAt: null,
1024 deletedAt: null,
1025 },
1026 threadId,
1027 runId: RunId.make("run-antigravity-subagent-reads"),
1028 runOrdinal: 1,
1029 providerTurnOrdinal: 1,
1030 attemptId: RunAttemptId.make("attempt-antigravity-subagent-reads"),
1031 rootNodeId: NodeId.make("node-antigravity-subagent-reads"),
1032 providerThread,
1033 message: {
1034 createdBy: "user",
1035 creationSource: "web",
1036 messageId: MessageId.make("message-antigravity-subagent-reads"),
1037 text: "review the codebase",
1038 attachments: [],
1039 },
1040 modelSelection,
1041 runtimePolicy,
1042 });
1043 const events = Array.from(
1044 yield* session.events.pipe(
1045 Stream.takeUntil((event) => event.type === "turn.terminal"),
1046 Stream.runCollect,
1047 ),
1048 );
1049 const read = (nativeId: string) => {
1050 const event = events.findLast(
1051 (event) =>
1052 event.type === "turn_item.updated" &&
1053 event.turnItem.nativeItemRef?.nativeId === nativeId,
1054 );
1055 return event?.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool"
1056 ? { status: event.turnItem.status, output: event.turnItem.output }
1057 : undefined;
1058 };
1059​
1060 const firstEvent = (nativeId: string, status: string) =>
1061 events.findIndex(
1062 (event) =>
1063 event.type === "turn_item.updated" &&
1064 event.turnItem.nativeItemRef?.nativeId === nativeId &&
1065 event.turnItem.status === status,
1066 );
1067 // T3 completes a served read itself instead of leaving it running until the flush.
1068 assert.isAbove(firstEvent("call_served", "completed"), -1);
1069 assert.isBelow(firstEvent("call_served", "completed"), firstEvent("call_native", "failed"));
1070 const failed = { status: "failed" as const, output: stepError };
1071 assert.deepEqual(read("call_served"), { status: "completed", output: undefined });
1072 assert.deepEqual(read("call_retried"), { status: "completed", output: undefined });
1073 assert.deepEqual(read("call_native"), failed);
1074 assert.deepEqual(read("call_missing"), failed);
1075 assert.deepEqual(read("call_denied"), failed);
1076 assert.deepEqual(read("call_unseen"), failed);
1077 }).pipe(Effect.provide(layerSession)),
1078 );
1079});
1080​
1081describe("AntigravityAdapterV2 runtime mode ceiling", () => {
1082 const modesSetFor = (pickedMode: string) =>
1083 Effect.gen(function* () {
1084 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1085 const path = yield* Path.Path;
1086 const crypto = yield* SecureRandom.SecureRandom;
1087 const mockAgentPath = yield* path.fromFileUrl(
1088 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
1089 );
1090 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
1091 const modesSet: Array<string> = [];
1092 const instanceId = ProviderInstanceId.make("antigravity-ceiling-test");
1093 const adapter = yield* makeAntigravityAdapterV2({
1094 instanceId,
1095 selfInvocation: yield* resolveSelfInvocation(),
1096 makeRuntime: (input) =>
1097 makeAntigravityAcpRuntime({
1098 ...input,
1099 childProcessSpawner,
1100 spawn: {
1101 command: process.execPath,
1102 args: [mockAgentPath],
1103 cwd: input.cwd,
1104 env: { T3_ACP_ANTIGRAVITY: "1" },
1105 },
1106 }).pipe(
1107 Effect.provideService(SecureRandom.SecureRandom, crypto),
1108 Effect.map((runtime): RuntimeService => ({
1109 ...runtime,
1110 setMode: (modeId) =>
1111 Effect.sync(() => {
1112 modesSet.push(modeId);
1113 }).pipe(Effect.andThen(runtime.setMode(modeId))),
1114 })),
1115 ),
1116 withProcess: (_stop, task) => task,
1117 defaultModel: Effect.undefined,
1118 });
1119 const threadId = ThreadId.make("thread-antigravity-ceiling");
1120 yield* adapter.openSession({
1121 threadId,
1122 providerSessionId: ProviderSessionId.make("provider-session-antigravity-ceiling"),
1123 modelSelection: {
1124 instanceId,
1125 model: "gemini-test-low",
1126 options: [{ id: "_t3/session-mode", value: pickedMode }],
1127 },
1128 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
1129 runtimeMode: "approval-required",
1130 interactionMode: "default",
1131 cwd: process.cwd(),
1132 maxRuntimeMode: "auto-accept-edits",
1133 }),
1134 });
1135 return modesSet;
1136 }).pipe(Effect.provide(layerSession), Effect.scoped);
1137​
1138 it.effect("does not apply a picked session mode above the policy ceiling", () =>
1139 Effect.gen(function* () {
1140 assert.deepEqual(yield* modesSetFor("yolo"), ["default"]);
1141 // A mode under the ceiling is still the user's to pick.
1142 assert.deepEqual(yield* modesSetFor("auto_edit"), ["default", "auto_edit"]);
1143 }),
1144 );
1145});
1146​
1147describe("AntigravityAdapterV2 session system prompt", () => {
1148 it.effect.each(["1.4.5", "1.4.1"])(
1149 "carries PR linking once in the %s session setup or once per turn",
1150 (agentVersion) =>
1151 Effect.gen(function* () {
1152 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1153 const fileSystem = yield* FileSystem.FileSystem;
1154 const path = yield* Path.Path;
1155 const crypto = yield* SecureRandom.SecureRandom;
1156 const mockAgentPath = yield* path.fromFileUrl(
1157 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
1158 );
1159 const logs = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-antigravity-log-" });
1160 const requestLog = path.join(logs, "requests.ndjson");
1161 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
1162 const promptReceived = yield* Deferred.make<Parameters<RuntimeService["prompt"]>[0]>();
1163 const instanceId = ProviderInstanceId.make("antigravity-system-prompt-test");
1164 const adapter = yield* makeAntigravityAdapterV2({
1165 instanceId,
1166 selfInvocation: yield* resolveSelfInvocation(),
1167 makeRuntime: (input) =>
1168 makeAntigravityAcpRuntime({
1169 ...input,
1170 childProcessSpawner,
1171 spawn: {
1172 command: process.execPath,
1173 args: [mockAgentPath],
1174 cwd: input.cwd,
1175 env: {
1176 T3_ACP_ANTIGRAVITY: "1",
1177 T3_ACP_AGENT_VERSION: agentVersion,
1178 T3_ACP_REQUEST_LOG_PATH: requestLog,
1179 T3_ACP_HANG_PROMPT_FOREVER: "1",
1180 },
1181 },
1182 }).pipe(
1183 Effect.provideService(SecureRandom.SecureRandom, crypto),
1184 Effect.map((runtime): RuntimeService => ({
1185 ...runtime,
1186 prompt: (request) =>
1187 Deferred.succeed(promptReceived, request).pipe(
1188 Effect.andThen(runtime.prompt(request)),
1189 ),
1190 })),
1191 ),
1192 withProcess: (_stop, task) => task,
1193 defaultModel: Effect.undefined,
1194 });
1195 const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ag-sp-" });
1196 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
1197 runtimeMode: "full-access",
1198 interactionMode: "default",
1199 cwd: workspace,
1200 });
1201 const threadId = ThreadId.make("thread-antigravity-system-prompt");
1202 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
1203 const session = yield* adapter.openSession({
1204 threadId,
1205 providerSessionId: ProviderSessionId.make("provider-session-antigravity-system-prompt"),
1206 modelSelection,
1207 runtimePolicy,
1208 });
1209 const providerThread = yield* session.ensureThread({
1210 threadId,
1211 modelSelection,
1212 runtimePolicy,
1213 });
1214 const now = yield* DateTime.now;
1215 yield* session
1216 .startTurn({
1217 appThread: {
1218 createdBy: "user",
1219 creationSource: "web",
1220 id: threadId,
1221 projectId: ProjectId.make("project-antigravity-system-prompt"),
1222 title: "Antigravity system prompt",
1223 providerInstanceId: instanceId,
1224 modelSelection,
1225 runtimeMode: "full-access",
1226 interactionMode: "default",
1227 branch: null,
1228 worktreePath: null,
1229 activeProviderThreadId: providerThread.id,
1230 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
1231 forkedFrom: null,
1232 createdAt: now,
1233 updatedAt: now,
1234 archivedAt: null,
1235 settledOverride: null,
1236 settledAt: null,
1237 lastVisitedAt: null,
1238 deletedAt: null,
1239 },
1240 threadId,
1241 runId: RunId.make("run-antigravity-system-prompt"),
1242 runOrdinal: 1,
1243 providerTurnOrdinal: 1,
1244 attemptId: RunAttemptId.make("attempt-antigravity-system-prompt"),
1245 rootNodeId: NodeId.make("node-antigravity-system-prompt"),
1246 providerThread,
1247 message: {
1248 createdBy: "user",
1249 creationSource: "web",
1250 messageId: MessageId.make("message-antigravity-system-prompt"),
1251 text: "open a PR",
1252 attachments: [],
1253 },
1254 modelSelection,
1255 runtimePolicy,
1256 })
1257 .pipe(Effect.forkScoped);
1258 const sent = JSON.stringify((yield* Deferred.await(promptReceived)).prompt);
1259 const sessionNew = (yield* fileSystem.readFileString(requestLog))
1260 .trim()
1261 .split("\n")
1262 .map((line) => JSON.parse(line) as { method?: string; params?: { _meta?: unknown } })
1263 .find((request) => request.method === "session/new");
1264 const append = JSON.stringify(sessionNew?.params?._meta ?? null);
1265 // Identity stays per turn either way.
1266 assert.include(sent, "through the Antigravity harness, as gemini-test-low");
1267 if (agentVersion === "1.4.5") {
1268 assert.include(append, "pull_request_linking");
1269 assert.notInclude(sent, "<pull_request_linking>");
1270 } else {
1271 assert.notInclude(append, "systemPrompt");
1272 assert.include(sent, "<pull_request_linking>");
1274 }).pipe(Effect.provide(layerSession)),
1275 );
1276});