1295 lines · 49.0 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import {  AntigravitySettings,  ApprovalRequestId,  ProviderInstanceId,  ThreadId,  type ProviderRuntimeEvent,} from "@t3tools/contracts";import * as Deferred from "effect/Deferred";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as Queue from "effect/Queue";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";import * as AcpErrors from "effect-acp/errors";import type * as AcpSchema from "effect-acp/schema"; import { ServerConfig } from "../../config.ts";import { ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE } from "../antigravityAuthSupport.ts";import type { AcpSessionRuntimeEvent } from "../acp/AcpSessionRuntime.ts";import { makeAntigravityAcpRuntime } from "../acp/AntigravityAcpSupport.ts";import {  mergeToolCallState,  parseSessionUpdateEvent,  type AcpToolCallState,} from "../acp/AcpRuntimeModel.ts";import { makeAntigravityAdapter, type AntigravityAdapterOptions } from "./AntigravityAdapter.ts"; const instanceId = ProviderInstanceId.make("antigravity-test");const threadId = ThreadId.make("antigravity-thread");const nativeSessionId = "b75db7e9-cd99-40e5-aa63-ac2b4674a6a9";const nativeDefault = "gemini-test-low";const nativeAlternative = "gemini-test-high";const decodeSettings = Schema.decodeSync(AntigravitySettings);const decodeRequestLog = Schema.decodeEffect(  Schema.Array(    Schema.fromJsonString(      Schema.Struct({ method: Schema.String, params: Schema.optional(Schema.Unknown) }),    ),  ),); interface NativePrompt {  readonly index: number;  readonly content: ReadonlyArray<AcpSchema.ContentBlock>;  readonly result: Deferred.Deferred<AcpSchema.PromptResponse, AcpErrors.AcpError>;} type Runtime = Effect.Success<ReturnType<AntigravityAdapterOptions["makeRuntime"]>>; function nativeToolUpdate(  update: Extract<    AcpSchema.SessionNotification["update"],    { sessionUpdate: "tool_call" | "tool_call_update" }  >,  previous?: AcpToolCallState,) {  const event = parseSessionUpdateEvent({ sessionId: nativeSessionId, update }).events.find(    (event) => event._tag === "ToolCallUpdated",  );  if (!event) throw new Error("Expected a native tool update");  return { ...event, toolCall: mergeToolCallState(previous, event.toolCall) };} const makeHarness = Effect.fn("makeAntigravityAdapterHarness")(function* (options?: {  readonly enabled?: boolean;  readonly holdCancel?: boolean;  readonly holdClose?: boolean;  readonly holdDispatch?: boolean;}) {  const runtimeEvents = yield* Queue.unbounded<AcpSessionRuntimeEvent>();  const canonicalEvents = yield* Queue.unbounded<ProviderRuntimeEvent>();  const prompts = yield* Queue.unbounded<NativePrompt>();  const cancellations = yield* Queue.unbounded<number>();  const cancelRelease = yield* Deferred.make<void>();  const closeStarted = yield* Deferred.make<void>();  const closeRelease = yield* Deferred.make<void>();  const dispatchStarted = yield* Deferred.make<void>();  const dispatchRelease = yield* Deferred.make<void>();  const seen: ProviderRuntimeEvent[] = [];  const calls: string[] = [];  const launches: Array<Parameters<AntigravityAdapterOptions["makeRuntime"]>[0]> = [];  const stops: Array<Effect.Effect<void>> = [];  const controls = { failModel: false, failAuth: false, authInvalidations: 0, closed: 0 };  let currentModel = nativeDefault;  let promptIndex = 0;  let active: NativePrompt | undefined;  const fileHandlers: {    read?: Parameters<Runtime["handleReadTextFile"]>[0];    write?: Parameters<Runtime["handleWriteTextFile"]>[0];  } = {};  let permissionHandler:    | ((        request: AcpSchema.RequestPermissionRequest,      ) => Effect.Effect<AcpSchema.RequestPermissionResponse, AcpErrors.AcpError>)    | undefined;   const configOptions = (): ReadonlyArray<AcpSchema.SessionConfigOption> => [    {      id: "model",      name: "Model",      type: "select",      category: "model",      currentValue: currentModel,      options: [        { value: nativeDefault, name: "Gemini test low" },        { value: nativeAlternative, name: "Gemini test high" },      ],    },  ];  const drainEvents = Effect.gen(function* () {    const acknowledge = yield* Deferred.make<void>();    yield* Queue.offer(runtimeEvents, { _tag: "EventStreamBarrier", acknowledge });    yield* Deferred.await(acknowledge);  });  const emitNative = (event: AcpSessionRuntimeEvent) =>    Queue.offer(runtimeEvents, event).pipe(Effect.asVoid);  const runtime: Effect.Success<ReturnType<AntigravityAdapterOptions["makeRuntime"]>> = {    handleRequestPermission: (handler) =>      Effect.sync(() => {        permissionHandler = handler;      }),    handleReadTextFile: (handler) =>      Effect.sync(() => {        fileHandlers.read = handler;      }),    handleWriteTextFile: (handler) =>      Effect.sync(() => {        fileHandlers.write = handler;      }),    start: () =>      Effect.gen(function* () {        if (controls.failAuth) {          return yield* new AcpErrors.AcpTransportError({            detail: ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE,            cause: undefined,          });        }        currentModel = nativeDefault;        calls.push("start");        yield* emitNative({          _tag: "AvailableCommandsUpdated",          availableCommands: [            { name: "plan", description: "Create a plan" },            { name: "logout", description: "Sign out" },          ],          rawPayload: {},        });        return {          sessionId: nativeSessionId,          initializeResult: {            protocolVersion: 1,            agentCapabilities: { sessionCapabilities: { resume: {} } },          },          sessionSetupResult: { sessionId: nativeSessionId, configOptions: configOptions() },          modelConfigId: "model",        };      }),    getConfigOptions: Effect.sync(configOptions),    setModel: (model) =>      Effect.gen(function* () {        calls.push(`model:${model}`);        if (controls.failModel) {          controls.failModel = false;          return yield* AcpErrors.AcpRequestError.invalidParams("Native model selection failed.");        }        currentModel = model;      }),    setMode: (mode) =>      Effect.sync(() => {        calls.push(`mode:${mode}`);        return {};      }),    getEvents: () => Stream.fromQueue(runtimeEvents),    drainEvents,    prompt: (payload, promptOptions) =>      Effect.gen(function* () {        yield* Deferred.succeed(dispatchStarted, undefined);        if (options?.holdDispatch) yield* Deferred.await(dispatchRelease);        const prompt: NativePrompt = {          index: ++promptIndex,          content: payload.prompt,          result: yield* Deferred.make<AcpSchema.PromptResponse, AcpErrors.AcpError>(),        };        active = prompt;        calls.push(`prompt:${prompt.index}`);        if (promptOptions?.dispatched) yield* Deferred.succeed(promptOptions.dispatched, undefined);        yield* Queue.offer(prompts, prompt);        return yield* Deferred.await(prompt.result).pipe(          Effect.ensuring(            Effect.sync(() => {              if (active === prompt) active = undefined;            }),          ),        );      }),    cancel: Effect.gen(function* () {      const prompt = active;      if (!prompt) return;      calls.push(`cancel:${prompt.index}`);      yield* Queue.offer(cancellations, prompt.index);      if (options?.holdCancel) yield* Deferred.await(cancelRelease);      yield* Deferred.succeed(prompt.result, { stopReason: "cancelled" });      yield* Deferred.await(prompt.result);      yield* drainEvents;      calls.push(`drained:${prompt.index}`);    }),  };  const commandUpdates: Array<ReadonlyArray<AcpSchema.AvailableCommand>> = [];  const adapter = yield* makeAntigravityAdapter(    decodeSettings({ enabled: options?.enabled ?? true }),    {      instanceId,      makeRuntime: (input) =>        Effect.gen(function* () {          launches.push(input);          yield* Effect.addFinalizer(() =>            Effect.gen(function* () {              yield* Deferred.succeed(closeStarted, undefined);              if (options?.holdClose) yield* Deferred.await(closeRelease);              controls.closed += 1;            }),          );          return runtime;        }),      withProcess: (stop, task) =>        Effect.suspend(() => {          stops.push(stop);          return task;        }),      onAvailableCommands: (commands) =>        Effect.sync(() => {          commandUpdates.push(commands);        }),      onAuthRequired: Effect.sync(() => {        controls.authInvalidations += 1;      }),    },  );  yield* adapter.streamEvents.pipe(    Stream.runForEach((event) =>      Effect.sync(() => {        seen.push(event);      }).pipe(Effect.andThen(Queue.offer(canonicalEvents, event))),    ),    Effect.forkScoped({ startImmediately: true }),  );  yield* Effect.addFinalizer(() =>    Effect.all([      Deferred.succeed(cancelRelease, undefined),      Deferred.succeed(closeRelease, undefined),      Deferred.succeed(dispatchRelease, undefined),    ]).pipe(Effect.asVoid),  );  const waitForEvent = Effect.fn("AntigravityAdapterTest.waitForEvent")(function* <    T extends ProviderRuntimeEvent,  >(predicate: (event: ProviderRuntimeEvent) => event is T) {    while (true) {      const event = yield* Queue.take(canonicalEvents);      if (predicate(event)) return event;    }  });  const invokePermission = (request: AcpSchema.RequestPermissionRequest) =>    Effect.suspend(() =>      permissionHandler        ? permissionHandler(request)        : Effect.die("Missing native permission handler"),    );  return {    fileHandlers,    adapter,    calls,    launches,    commandUpdates,    controls,    seen,    stops,    waitForEvent,    emitNative,    invokePermission,    closeStarted,    closeRelease,    cancelRelease,    dispatchStarted,    dispatchRelease,    nextPrompt: Queue.take(prompts),    nextCancellation: Queue.take(cancellations),    drainEvents,    hasActivePrompt: () => active !== undefined,  };}); const layer = ServerConfig.layerTest(process.cwd(), {  prefix: "t3-antigravity-adapter-test-",}).pipe(Layer.provideMerge(NodeServices.layer)); it.layer(layer)("AntigravityAdapter", (it) => {  it.effect(    "runs native auth, resume, models, commands, and streaming through the ACP transport",    () =>      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const crypto = yield* Crypto.Crypto;        const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;        const cwd = yield* fileSystem.makeTempDirectoryScoped({          prefix: "t3-antigravity-transport-",        });        const mockAgentPath = yield* path.fromFileUrl(          new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),        );        const requestLog = path.join(cwd, "requests.ndjson");        const commands: string[] = [];        const modelSelections: string[] = [];        const observed: ProviderRuntimeEvent[] = [];        const completed = yield* Deferred.make<void>();        const adapter = yield* makeAntigravityAdapter(decodeSettings({ enabled: true }), {          instanceId,          withProcess: (_stop, task) => task,          makeRuntime: (input) =>            makeAntigravityAcpRuntime({              ...input,              childProcessSpawner,              spawn: {                command: process.execPath,                args: [mockAgentPath],                cwd: input.cwd,                env: {                  ...process.env,                  T3_ACP_ANTIGRAVITY: "1",                  T3_ACP_REQUEST_LOG_PATH: requestLog,                },                extendEnv: false,              },            }).pipe(Effect.provideService(Crypto.Crypto, crypto)),          onAvailableCommands: (available) =>            Effect.sync(() => {              commands.push(...available.map((command) => command.name));            }),          onConfigOptionsUpdated: (configOptions) =>            Effect.sync(() => {              const model = configOptions.find((option) => option.category === "model");              if (model?.type === "select") modelSelections.push(model.currentValue);            }),        });        yield* adapter.streamEvents.pipe(          Stream.runForEach((event) =>            Effect.gen(function* () {              observed.push(event);              if (event.type === "turn.completed") yield* Deferred.succeed(completed, undefined);            }),          ),          Effect.forkScoped({ startImmediately: true }),        );        const original = yield* adapter.startSession({          threadId,          cwd,          runtimeMode: "auto-accept-edits",          modelSelection: { instanceId, model: nativeAlternative },        });        yield* adapter.stopSession(threadId);        const resumed = yield* adapter.startSession({          threadId,          cwd,          runtimeMode: "auto-accept-edits",          modelSelection: { instanceId, model: nativeAlternative },          resumeCursor: original.resumeCursor,        });        expect(resumed.model).toBe(nativeAlternative);        yield* adapter.sendTurn({ threadId, input: "Reply with one short line." });        yield* Deferred.await(completed);        expect(commands).toEqual(["plan", "logout", "plan", "logout"]);        expect(modelSelections.length).toBeGreaterThan(0);        expect(modelSelections.every((model) => model === nativeAlternative)).toBe(true);        expect(          observed            .filter((event) => event.type === "content.delta")            .map((event) => event.payload.delta)            .join(""),        ).toBe("hello from mock");        const lines = (yield* fileSystem.readFileString(requestLog)).trim().split("\n");        const requests = yield* decodeRequestLog(lines);        expect(          requests            .filter((request) => request.method === "authenticate")            .map((request) => request.params),        ).toEqual([{ methodId: "oauth-personal" }, { methodId: "oauth-personal" }]);        expect(requests.some((request) => request.method === "session/resume")).toBe(true);        expect(requests.some((request) => request.method === "session/load")).toBe(false);        expect(          requests            .filter((request) => request.method === "session/set_config_option")            .map((request) => request.params),        ).toContainEqual({ sessionId: "mock-session-1", configId: "mode", value: "auto_edit" });      }),  );   it.effect("reapplies the exact saved model and mode after a native resume", () =>    Effect.gen(function* () {      const h = yield* makeHarness();      const first = yield* h.adapter.startSession({        threadId,        cwd: process.cwd(),        runtimeMode: "auto-accept-edits",        modelSelection: { instanceId, model: nativeAlternative },      });      expect(first.model).toBe(nativeAlternative);      yield* h.adapter.stopSession(threadId);      const second = yield* h.adapter.startSession({        threadId,        cwd: "/tmp",        runtimeMode: "auto-accept-edits",        resumeCursor: first.resumeCursor,        modelSelection: { instanceId, model: nativeAlternative },      });      expect(second.model).toBe(nativeAlternative);      // The adapter resolves the cwd it was given through the host Path.      expect(second.cwd).toBe((yield* Path.Path).resolve("/tmp"));      expect(h.launches[1]?.resumeSessionId).toBe(nativeSessionId);      expect(h.calls).toEqual([        "start",        `model:${nativeAlternative}`,        "mode:auto_edit",        "start",        `model:${nativeAlternative}`,        "mode:auto_edit",      ]);      expect(h.commandUpdates.at(-1)?.map((command) => command.name)).toEqual(["plan", "logout"]);      expect(h.adapter.capabilities.supportsConversationRollback).toBe(false);      const rollback = yield* h.adapter.rollbackThread(threadId, 1).pipe(Effect.exit);      expect(Exit.isFailure(rollback)).toBe(true);    }),  );   it.effect("keeps thoughts, native command results, and replies on the active turn", () =>    Effect.gen(function* () {      const h = yield* makeHarness();      yield* h.adapter.startSession({        threadId,        cwd: process.cwd(),        runtimeMode: "approval-required",      });      const sending = yield* h.adapter        .sendTurn({ threadId, input: "Read the file" })        .pipe(Effect.forkChild);      const prompt = yield* h.nextPrompt;      yield* h.emitNative({ _tag: "ThoughtDelta", text: "I will read it.", rawPayload: {} });      yield* h.emitNative({        _tag: "ToolCallUpdated",        toolCall: {          toolCallId: "command-1",          kind: "execute",          status: "completed",          data: {            rawInput: { CommandLine: "cat probe.txt", Cwd: "/tmp" },            rawOutput: { combinedOutput: "after\n", exitCode: 0 },          },        },        rawPayload: {},      });      yield* h.emitNative({ _tag: "ContentDelta", text: "The file says after.", rawPayload: {} });      yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" });      const result = yield* Fiber.join(sending);      yield* h.waitForEvent((event) => event.type === "turn.completed");      const deltas = h.seen.filter((event) => event.type === "content.delta");      expect(deltas.map((event) => event.payload.streamKind)).toEqual([        "reasoning_text",        "assistant_text",      ]);      expect(deltas.every((event) => event.turnId === result.turnId)).toBe(true);      const tool = h.seen.find(        (event) =>          event.type === "item.completed" && event.payload.itemType === "command_execution",      );      expect(tool?.type === "item.completed" ? tool.payload.data : undefined).toMatchObject({        command: "cat probe.txt",        cwd: "/tmp",        item: { aggregatedOutput: "after\n", exitCode: 0 },      });    }),  );   it.effect("does not auto-approve a remaining native request in full access", () =>    Effect.gen(function* () {      const h = yield* makeHarness();      yield* h.adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });      const permission = yield* h        .invokePermission({          sessionId: nativeSessionId,          toolCall: { toolCallId: "write-1", kind: "edit", title: "Write probe.txt" },          options: [            { optionId: "native:allow", name: "Allow", kind: "allow_once" },            { optionId: "native:deny", name: "Deny", kind: "reject_once" },

Lines 1–500 of 1295.