Skip to content

Commit 29ed6ad

Browse files
author
Paul
committed
test(antigravity): cover mixed catalogs without thinking options
Port the missing fixture and eleven regression cases from 487dc28edafaf42feee1c02b27e51539fba22e95. Keep ACP141's broader production detector, which already handles grouped families without thinking_level and models-only selection; applying GPTOSS's production hunk would duplicate those fixes and lose edge-case hardening. Retain both lanes' catalog tests and fixture imports where additive test patches overlap. The third-party ACP advertisement is explicitly synthetic; no static selectable model or client identity override is added.
1 parent 99da6e0 commit 29ed6ad

5 files changed

Lines changed: 402 additions & 0 deletions

File tree

‎apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
antigravity140Session,
3939
decodeAntigravity140Request,
4040
} from "../../provider/acp/antigravity140.fixture.ts";
41+
import { antigravityThirdPartyAgentSource } from "../../provider/acp/antigravityThirdParty.fixture.ts";
4142
import { resolveAttachmentPath } from "../../attachmentStore.ts";
4243
import { normalizeAntigravitySessionUpdate } from "../../provider/acp/AntigravityProtocol.ts";
4344
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
@@ -439,6 +440,139 @@ describe("AntigravityAdapterV2 ACP 1.4 models", () => {
439440
);
440441
}).pipe(Effect.provide(layerSession), Effect.scoped),
441442
);
443+
it.effect.each(["gpt-oss-120b-medium", "gemini-3.8-flash-low"])(
444+
"preserves saved %s in fresh and resumed chat from a GPT-OSS default",
445+
(requestedModel) =>
446+
Effect.gen(function* () {
447+
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
448+
const fileSystem = yield* FileSystem.FileSystem;
449+
const path = yield* Path.Path;
450+
const crypto = yield* Crypto.Crypto;
451+
const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3 ag models ü-" });
452+
const agentPath = path.join(workspace, "agent.mjs");
453+
const logPath = path.join(workspace, "requests.ndjson");
454+
yield* fileSystem.writeFileString(agentPath, antigravityThirdPartyAgentSource);
455+
const instanceId = ProviderInstanceId.make("antigravity-model-test");
456+
const adapter = yield* makeAntigravityAdapterV2({
457+
instanceId,
458+
selfInvocation: yield* resolveSelfInvocation(),
459+
makeRuntime: (input) =>
460+
makeAntigravityAcpRuntime({
461+
...input,
462+
childProcessSpawner,
463+
spawn: {
464+
command: process.execPath,
465+
args: [agentPath],
466+
cwd: workspace,
467+
env: { T3_ACP_REQUEST_LOG_PATH: logPath },
468+
},
469+
}).pipe(Effect.provideService(Crypto.Crypto, crypto)),
470+
withProcess: (_stop, task) => task,
471+
defaultModel: Effect.succeed("gemini-3.8-flash-high"),
472+
});
473+
const threadId = ThreadId.make("thread-antigravity-models");
474+
const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
475+
runtimeMode: "full-access",
476+
interactionMode: "default",
477+
cwd: workspace,
478+
});
479+
const savedModel = { instanceId, model: requestedModel };
480+
const open = {
481+
threadId,
482+
providerSessionId: ProviderSessionId.make("provider-session-antigravity-models"),
483+
modelSelection: savedModel,
484+
runtimePolicy,
485+
};
486+
const freshScope = yield* Scope.fork(yield* Effect.scope);
487+
let session = yield* adapter
488+
.openSession(open)
489+
.pipe(Effect.provideService(Scope.Scope, freshScope));
490+
const providerThread = yield* session.ensureThread(open);
491+
const now = yield* DateTime.now;
492+
const turn = Effect.fnUntraced(function* (
493+
ordinal: number,
494+
modelSelection: typeof savedModel & { options?: Array<{ id: string; value: string }> },
495+
) {
496+
const completed = yield* session.events.pipe(
497+
Stream.takeUntil(
498+
(event) =>
499+
event.type === "provider_turn.updated" && event.providerTurn.status === "completed",
500+
),
501+
Stream.runCollect,
502+
Effect.forkChild,
503+
);
504+
yield* session.startTurn({
505+
appThread: {
506+
createdBy: "user",
507+
creationSource: "web",
508+
id: threadId,
509+
projectId: ProjectId.make("project-antigravity-models"),
510+
title: "Antigravity models",
511+
providerInstanceId: instanceId,
512+
modelSelection,
513+
runtimeMode: "full-access",
514+
interactionMode: "default",
515+
branch: null,
516+
worktreePath: null,
517+
activeProviderThreadId: providerThread.id,
518+
lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
519+
forkedFrom: null,
520+
createdAt: now,
521+
updatedAt: now,
522+
archivedAt: null,
523+
settledOverride: null,
524+
settledAt: null,
525+
lastVisitedAt: null,
526+
deletedAt: null,
527+
},
528+
threadId,
529+
runId: RunId.make(`run-model-${ordinal}`),
530+
runOrdinal: ordinal,
531+
providerTurnOrdinal: ordinal,
532+
attemptId: RunAttemptId.make(`attempt-model-${ordinal}`),
533+
rootNodeId: NodeId.make(`node-model-${ordinal}`),
534+
providerThread,
535+
message: {
536+
createdBy: "user",
537+
creationSource: "web",
538+
messageId: MessageId.make(`message-model-${ordinal}`),
539+
text: "Report the selected model",
540+
attachments: [],
541+
},
542+
modelSelection,
543+
runtimePolicy,
544+
});
545+
const events = yield* Fiber.join(completed);
546+
return events
547+
.filter(
548+
(event) => event.type === "message.updated" && event.message.role === "assistant",
549+
)
550+
.map((event) => (event.type === "message.updated" ? event.message.text : undefined))
551+
.at(-1);
552+
});
553+
assert.equal(yield* turn(1, savedModel), requestedModel);
554+
yield* Scope.close(freshScope, Exit.void);
555+
assert.isNotNull(providerThread.nativeThreadRef?.nativeId);
556+
session = yield* adapter.openSession({
557+
...open,
558+
initialNativeThreadId: providerThread.nativeThreadRef!.nativeId!,
559+
});
560+
yield* session.resumeThread({ providerThread });
561+
assert.equal(yield* turn(2, savedModel), requestedModel);
562+
const requests = yield* Effect.forEach(
563+
(yield* fileSystem.readFileString(logPath)).trim().split("\n"),
564+
(line) => decodeAntigravity140Request(line),
565+
);
566+
assert.equal(requests.filter((request) => request.method === "session/new").length, 1);
567+
assert.equal(requests.filter((request) => request.method === "session/resume").length, 1);
568+
assert.deepEqual(
569+
requests
570+
.filter((request) => request.method === "session/set_model")
571+
.map((request) => request.params?.modelId),
572+
[requestedModel, requestedModel],
573+
);
574+
}).pipe(Effect.provide(layerSession), Effect.scoped),
575+
);
442576
});
443577

444578
describe("AntigravityAdapterV2 client file system", () => {

‎apps/server/src/provider/AntigravityProvider.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
makeAntigravityProvider,
3030
} from "./AntigravityProvider.ts";
3131
import { antigravity140Session } from "./acp/antigravity140.fixture.ts";
32+
import { antigravityThirdPartySession } from "./acp/antigravityThirdParty.fixture.ts";
3233

3334
import { antigravity141Session } from "./acp/antigravity141.fixture.ts";
3435

@@ -336,6 +337,34 @@ it.layer(layerTest)("Antigravity provider snapshots", (it) => {
336337
),
337338
);
338339

340+
it.effect.each(["gpt-oss-120b-medium", "claude-sonnet-5-5-low"])(
341+
"keeps every concrete family when %s omits thinking_level",
342+
(currentModelId) =>
343+
Effect.scoped(
344+
Effect.gen(function* () {
345+
const harness = yield* makeHarness();
346+
yield* harness.initialize;
347+
const setup = {
348+
...antigravityThirdPartySession,
349+
models: { ...antigravityThirdPartySession.models, currentModelId },
350+
configOptions: antigravityThirdPartySession.configOptions.map((option) => ({
351+
...option,
352+
currentValue: currentModelId,
353+
})),
354+
};
355+
yield* harness.provider.onSessionStarted({ ...started, sessionSetupResult: setup });
356+
const snapshot = yield* harness.provider.snapshot.getSnapshot;
357+
expect(snapshot.models.map((model) => [model.slug, model.name])).toEqual(
358+
setup.models.availableModels.map((model) => [model.modelId, model.name]),
359+
);
360+
expect(snapshot.models.find((model) => model.isDefault)?.slug).toBe(currentModelId);
361+
yield* harness.provider.onConfigOptionsUpdated(antigravity140Session.configOptions);
362+
yield* harness.provider.onConfigOptionsUpdated(setup.configOptions);
363+
expect((yield* harness.provider.snapshot.getSnapshot).models).toEqual(snapshot.models);
364+
}),
365+
),
366+
);
367+
339368
it.effect("does not restore removed concrete models after a session refresh", () =>
340369
Effect.scoped(
341370
Effect.gen(function* () {

‎apps/server/src/provider/acp/AntigravityAcpSupport.test.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from "./AntigravityAcpSupport.ts";
2323

2424
import { antigravity140Session } from "./antigravity140.fixture.ts";
25+
import { antigravityThirdPartySession } from "./antigravityThirdParty.fixture.ts";
2526

2627
const modelConfig = {
2728
id: "model",
@@ -54,6 +55,74 @@ function makeModelRuntime(
5455
}
5556

5657
describe("applyAntigravityAcpModelSelection", () => {
58+
it.effect("selects GPT-OSS when only session models are advertised", () =>
59+
Effect.gen(function* () {
60+
const selections: string[] = [];
61+
const model = "gpt-oss-120b-medium";
62+
expect(
63+
yield* applyAntigravityAcpModelSelection({
64+
runtime: {
65+
getConfigOptions: Effect.succeed([]),
66+
setModel: () => Effect.die("No model config option was advertised"),
67+
setSessionModel: (value) =>
68+
Effect.sync(() => {
69+
selections.push(value);
70+
return {};
71+
}),
72+
},
73+
models: antigravityThirdPartySession.models,
74+
model,
75+
mapError: (cause) => cause,
76+
}),
77+
).toBe(model);
78+
expect(selections).toEqual([model]);
79+
}),
80+
);
81+
82+
it.effect.each(["gpt-oss-120b-medium", "claude-sonnet-5-5-low", "gemini-3.8-flash-low"])(
83+
"selects concrete %s when the current GPT-OSS model omits thinking_level",
84+
(model) =>
85+
Effect.gen(function* () {
86+
const selections: string[] = [];
87+
const selected = yield* applyAntigravityAcpModelSelection({
88+
runtime: {
89+
getConfigOptions: Effect.succeed(antigravityThirdPartySession.configOptions),
90+
setModel: () => Effect.die("Grouped model options are not concrete IDs"),
91+
setSessionModel: (value) =>
92+
Effect.sync(() => {
93+
selections.push(value);
94+
return {};
95+
}),
96+
},
97+
models: antigravityThirdPartySession.models,
98+
model,
99+
mapError: (cause) => cause,
100+
});
101+
expect(selected).toBe(model);
102+
expect(selections).toEqual([model]);
103+
}),
104+
);
105+
106+
it.effect("selects GPT-OSS from legacy flat and grouped model options", () =>
107+
Effect.gen(function* () {
108+
const option = { value: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)" };
109+
for (const options of [
110+
[option],
111+
[{ groupId: "openai", name: "OpenAI", options: [option] }],
112+
]) {
113+
const { runtime, selections } = makeModelRuntime([{ ...modelConfig, options }]);
114+
expect(
115+
yield* applyAntigravityAcpModelSelection({
116+
runtime,
117+
model: option.value,
118+
mapError: (cause) => cause,
119+
}),
120+
).toBe(option.value);
121+
expect(selections).toEqual([option.value]);
122+
}
123+
}),
124+
);
125+
57126
it.effect("restores the saved model instead of the cold-resume default", () =>
58127
Effect.gen(function* () {
59128
const { runtime, selections } = makeModelRuntime();
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type * as AcpSchema from "effect-acp/compat";
2+
3+
// IDs/names/order captured from fetchAvailableModels on 2026-10-26. This is a
4+
// synthetic ACP advertisement: 1.4.0 filters Claude/GPT-OSS for T3's identity.
5+
// Its unmodified config_options.py groups only Gemini; selecting GPT-OSS omits
6+
// thinking_level. Mode options/descriptions are omitted; session ID is synthetic.
7+
export const antigravityThirdPartySession = {
8+
sessionId: "047c62f6-607b-44db-bfbe-f83b67e9e8b1",
9+
models: {
10+
currentModelId: "gpt-oss-120b-medium",
11+
availableModels: [
12+
{ modelId: "gemini-4-argon-medium", name: "Gemini 4 Argon (Medium)" },
13+
{ modelId: "gemini-4-argon-high", name: "Gemini 4 Argon (High)" },
14+
{ modelId: "gemini-4-argon-low", name: "Gemini 4 Argon (Low)" },
15+
{ modelId: "gemini-3.8-flash-high", name: "Gemini 3.8 Flash (High)" },
16+
{ modelId: "gemini-3.8-flash-medium", name: "Gemini 3.8 Flash (Medium)" },
17+
{ modelId: "gemini-3.8-flash-low", name: "Gemini 3.8 Flash (Low)" },
18+
{ modelId: "claude-opus-5-5-low", name: "Claude Opus 5.5 (Low)" },
19+
{ modelId: "claude-opus-5-5-medium", name: "Claude Opus 5.5 (Medium)" },
20+
{ modelId: "claude-opus-5-5-high", name: "Claude Opus 5.5 (High)" },
21+
{ modelId: "claude-sonnet-5-5-low", name: "Claude Sonnet 5.5 (Low)" },
22+
{ modelId: "claude-sonnet-5-5-medium", name: "Claude Sonnet 5.5 (Medium)" },
23+
{ modelId: "claude-sonnet-5-5-high", name: "Claude Sonnet 5.5 (High)" },
24+
{ modelId: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)" },
25+
],
26+
},
27+
configOptions: [
28+
{
29+
id: "model",
30+
name: "Model",
31+
category: "model",
32+
type: "select",
33+
currentValue: "gpt-oss-120b-medium",
34+
options: [
35+
{ value: "gemini-4-argon", name: "Gemini 4 Argon" },
36+
{ value: "gemini-3.8-flash", name: "Gemini 3.8 Flash" },
37+
{ value: "claude-opus-5-5-low", name: "Claude Opus 5.5 (Low)" },
38+
{ value: "claude-opus-5-5-medium", name: "Claude Opus 5.5 (Medium)" },
39+
{ value: "claude-opus-5-5-high", name: "Claude Opus 5.5 (High)" },
40+
{ value: "claude-sonnet-5-5-low", name: "Claude Sonnet 5.5 (Low)" },
41+
{ value: "claude-sonnet-5-5-medium", name: "Claude Sonnet 5.5 (Medium)" },
42+
{ value: "claude-sonnet-5-5-high", name: "Claude Sonnet 5.5 (High)" },
43+
{ value: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)" },
44+
],
45+
},
46+
],
47+
} satisfies AcpSchema.NewSessionResponse;
48+
49+
// Protocol peer, not a live Google capture. Cold resume deliberately returns
50+
// GPT-OSS so a saved Gemini selection must survive the absence of thinking_level.
51+
export const antigravityThirdPartyAgentSource = `
52+
import { appendFileSync } from "node:fs";
53+
import { createInterface } from "node:readline";
54+
55+
const setup = ${JSON.stringify(antigravityThirdPartySession)};
56+
let sessionId = setup.sessionId;
57+
let modelId = setup.models.currentModelId;
58+
const write = (message) => process.stdout.write(JSON.stringify(message) + "\\r\\n");
59+
const configOptions = () => {
60+
const gemini = modelId.startsWith("gemini-");
61+
const family = modelId.startsWith("gemini-3.8-flash-") ? "gemini-3.8-flash" : "gemini-4-argon";
62+
return [
63+
{ ...setup.configOptions[0], currentValue: gemini ? family : modelId },
64+
...(gemini ? [{
65+
id: "thinking_level", name: "Thinking Level", category: "thought_level", type: "select",
66+
currentValue: modelId.split("-").at(-1),
67+
options: ["low", "medium", "high"]
68+
.map((value) => ({ value, name: value[0].toUpperCase() + value.slice(1) })),
69+
}] : []),
70+
];
71+
};
72+
for await (const line of createInterface({ input: process.stdin })) {
73+
const request = JSON.parse(line);
74+
appendFileSync(process.env.T3_ACP_REQUEST_LOG_PATH, line + "\\n");
75+
if (request.id === undefined) continue;
76+
let result = {};
77+
switch (request.method) {
78+
case "initialize":
79+
result = { protocolVersion: 1, agentInfo: { name: "antigravity-acp", version: "1.4.0" },
80+
agentCapabilities: { sessionCapabilities: { resume: {} } } };
81+
break;
82+
case "authenticate":
83+
case "session/set_mode":
84+
break;
85+
case "session/new":
86+
case "session/resume":
87+
sessionId = request.params.sessionId ?? sessionId;
88+
result = { ...setup, sessionId, configOptions: configOptions() };
89+
break;
90+
case "session/set_model":
91+
if (!setup.models.availableModels.some((model) => model.modelId === request.params.modelId)) {
92+
write({ jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "Unknown model" } });
93+
continue;
94+
}
95+
modelId = request.params.modelId;
96+
write({ jsonrpc: "2.0", method: "session/update", params: { sessionId,
97+
update: { sessionUpdate: "config_option_update", configOptions: configOptions() } } });
98+
break;
99+
case "session/set_config_option":
100+
if (request.params.configId !== "mode") {
101+
write({ jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "Expected concrete model" } });
102+
continue;
103+
}
104+
result = { configOptions: configOptions() };
105+
break;
106+
case "session/prompt":
107+
write({ jsonrpc: "2.0", method: "session/update", params: { sessionId,
108+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text",
109+
text: process.env.T3_ACP_TITLE === "1" ? JSON.stringify({ title: modelId }) : modelId } } } });
110+
result = { stopReason: "end_turn" };
111+
break;
112+
default:
113+
write({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } });
114+
continue;
115+
}
116+
write({ jsonrpc: "2.0", id: request.id, result });
117+
}
118+
`;

0 commit comments

Comments
 (0)