Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 86 additions & 26 deletions apps/server/src/provider/Layers/GrokAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ const mockAgentCommand = process.execPath;

async function makeMockGrokWrapper(extraEnv?: Record<string, string>) {
const dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-acp-mock-"));
if (NodePath.sep === "\\") {
const wrapperPath = NodePath.join(dir, "fake-grok.cmd");
const envAssignments = Object.entries(extraEnv ?? {})
.map(([key, value]) => `set "${key}=${value}"`)
.join("\r\n");
const script = `@echo off\r\n${envAssignments}\r\n${JSON.stringify(mockAgentCommand)} ${JSON.stringify(mockAgentPath)} %*\r\n`;
await NodeFSP.writeFile(wrapperPath, script, "utf8");
return wrapperPath;
}

const wrapperPath = NodePath.join(dir, "fake-grok.sh");
const envExports = Object.entries(extraEnv ?? {})
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
Expand Down Expand Up @@ -189,33 +199,35 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
);

it.effect("closes the ACP child process when a session stops", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-stop-session-close");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-adapter-exit-log-")),
);
const exitLogPath = NodePath.join(tempDir, "exit.log");

const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
T3_ACP_EXIT_LOG_PATH: exitLogPath,
NodePath.sep === "\\"
? Effect.void
: Effect.gen(function* () {
const threadId = ThreadId.make("grok-stop-session-close");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-adapter-exit-log-")),
);
const exitLogPath = NodePath.join(tempDir, "exit.log");

const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
T3_ACP_EXIT_LOG_PATH: exitLogPath,
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-build" },
});

yield* adapter.stopSession(threadId);

const exitLog = yield* waitForFileContent(exitLogPath);
assert.include(exitLog, "SIGTERM");
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-build" },
});

yield* adapter.stopSession(threadId);

const exitLog = yield* waitForFileContent(exitLogPath);
assert.include(exitLog, "SIGTERM");
}),
);

it.effect("reports a Grok session running only while the prompt is in flight", () =>
Expand Down Expand Up @@ -943,6 +955,54 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
}),
);

it.effect("scopes assistant item ids to the T3 turn across resumed Grok runtimes", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-resume-item-id-scope");
const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper());

const runPrompt = (resume: boolean) =>
Effect.gen(function* () {
const adapter = yield* makeTestAdapter(wrapperPath);
const contentDelta =
yield* Deferred.make<Extract<ProviderRuntimeEvent, { type: "content.delta" }>>();
const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) =>
event.type === "content.delta"
? Deferred.succeed(contentDelta, event).pipe(Effect.ignore)
: Effect.void,
).pipe(Effect.forkChild);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-build" },
...(resume
? { resumeCursor: { schemaVersion: 1 as const, sessionId: "mock-session-1" } }
: {}),
});
const turn = yield* adapter.sendTurn({
threadId,
input: resume ? "after restart" : "before restart",
attachments: [],
});
const delta = yield* Deferred.await(contentDelta);

yield* Fiber.interrupt(eventsFiber);
yield* adapter.stopSession(threadId);
return { itemId: delta.itemId, turnId: turn.turnId };
});

const beforeRestart = yield* runPrompt(false);
const afterRestart = yield* runPrompt(true);

assert.notEqual(beforeRestart.turnId, afterRestart.turnId);
assert.isDefined(beforeRestart.itemId);
assert.isDefined(afterRestart.itemId);
assert.notEqual(beforeRestart.itemId, afterRestart.itemId);
}),
);

it.effect("ignores replayed session/load updates when resuming a Grok session", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-load-replay-filter");
Expand Down
13 changes: 10 additions & 3 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ const resolveNotificationTurnId = (ctx: GrokSessionContext): TurnId | undefined

const resolveCallbackTurnId = (ctx: GrokSessionContext): TurnId | undefined => ctx.activeTurnId;

const scopeAssistantItemIdToTurn = (turnId: TurnId, itemId: string): string =>
`${itemId}:turn:${turnId}`;

const resolveSessionCallbackTurnId = (
sessions: ReadonlyMap<ThreadId, GrokSessionContext>,
threadId: ThreadId,
Expand Down Expand Up @@ -816,7 +819,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
provider: PROVIDER,
threadId: ctx.threadId,
turnId: notificationTurnId,
itemId: event.itemId,
itemId: scopeAssistantItemIdToTurn(notificationTurnId, event.itemId),
lifecycle: "item.started",
}),
);
Expand All @@ -828,7 +831,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
provider: PROVIDER,
threadId: ctx.threadId,
turnId: notificationTurnId,
itemId: event.itemId,
itemId: scopeAssistantItemIdToTurn(notificationTurnId, event.itemId),
lifecycle: "item.completed",
}),
);
Expand Down Expand Up @@ -862,7 +865,11 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
provider: PROVIDER,
threadId: ctx.threadId,
turnId: notificationTurnId,
...(event.itemId ? { itemId: event.itemId } : {}),
...(event.itemId
? {
itemId: scopeAssistantItemIdToTurn(notificationTurnId, event.itemId),
}
: {}),
text: event.text,
rawPayload: event.rawPayload,
}),
Expand Down
22 changes: 15 additions & 7 deletions apps/server/src/provider/Layers/GrokProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { GrokSettings } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";

import { buildInitialGrokProviderSnapshot, checkGrokProviderStatus } from "./GrokProvider.ts";

Expand All @@ -30,6 +31,7 @@ describe("buildInitialGrokProviderSnapshot", () => {
expect(snapshot.installed).toBe(true);
expect(snapshot.status).toBe("warning");
expect(snapshot.version).toBeNull();
expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-4.5"]);
expect(snapshot.message).toContain("Checking Grok");
expect(snapshot.requiresNewThreadForModelChange).toBe(true);
}),
Expand Down Expand Up @@ -57,15 +59,18 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => {
const secretStderr = "broken grok install: secret-token-value";
const snapshot = yield* Effect.scoped(
Effect.gen(function* () {
const hostPlatform = yield* HostProcessPlatform;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-grok-version-" });
const grokPath = path.join(dir, "grok");
const grokPath = path.join(dir, hostPlatform === "win32" ? "grok.cmd" : "grok");
yield* fs.writeFileString(
grokPath,
["#!/bin/sh", `printf "%s\\n" "${secretStderr}" >&2`, "exit 2", ""].join("\n"),
hostPlatform === "win32"
? ["@echo off", `echo ${secretStderr} 1>&2`, "exit /b 2", ""].join("\r\n")
: ["#!/bin/sh", `printf "%s\\n" "${secretStderr}" >&2`, "exit 2", ""].join("\n"),
);
yield* fs.chmod(grokPath, 0o755);
if (hostPlatform !== "win32") yield* fs.chmod(grokPath, 0o755);

return yield* checkGrokProviderStatus(
decodeGrokSettings({ enabled: true, binaryPath: grokPath }),
Expand All @@ -85,15 +90,18 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => {
Effect.gen(function* () {
const snapshot = yield* Effect.scoped(
Effect.gen(function* () {
const hostPlatform = yield* HostProcessPlatform;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-grok-success-" });
const grokPath = path.join(dir, "grok");
const grokPath = path.join(dir, hostPlatform === "win32" ? "grok.cmd" : "grok");
yield* fs.writeFileString(
grokPath,
["#!/bin/sh", 'printf "grok-cli 0.0.99\\n"', "exit 0", ""].join("\n"),
hostPlatform === "win32"
? ["@echo off", "echo grok-cli 0.0.99", "exit /b 0", ""].join("\r\n")
: ["#!/bin/sh", 'printf "grok-cli 0.0.99\\n"', "exit 0", ""].join("\n"),
);
yield* fs.chmod(grokPath, 0o755);
if (hostPlatform !== "win32") yield* fs.chmod(grokPath, 0o755);

return yield* checkGrokProviderStatus(
decodeGrokSettings({ enabled: true, binaryPath: grokPath }),
Expand All @@ -103,7 +111,7 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => {

expect(snapshot.status).toBe("error");
expect(snapshot.installed).toBe(true);
expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-build"]);
expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-4.5"]);
expect(snapshot.message).toContain("ACP startup failed");
}),
);
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/provider/Layers/GrokProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ const GROK_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;

const GROK_BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{
slug: "grok-build",
name: "Grok Build",
slug: "grok-4.5",
name: "Grok 4.5",
isCustom: false,
capabilities: EMPTY_CAPABILITIES,
},
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/provider/acp/AcpSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export interface AcpSessionRuntimeOptions {
readonly name: string;
readonly version: string;
};
readonly authMethodId: string;
readonly authMethodId:
| string
| ((initializeResult: EffectAcpSchema.InitializeResponse) => string);
readonly mcpServers?: ReadonlyArray<EffectAcpSchema.McpServer>;
readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect<void, never>;
readonly protocolLogging?: {
Expand Down Expand Up @@ -529,8 +531,12 @@ export const make = (
acp.agent.initialize(initializePayload),
);

const authMethodId =
typeof options.authMethodId === "function"
? options.authMethodId(initializeResult)
: options.authMethodId;
const authenticatePayload = {
methodId: options.authMethodId,
methodId: authMethodId,
} satisfies EffectAcpSchema.AuthenticateRequest;

yield* runLoggedRequest(
Expand Down
27 changes: 19 additions & 8 deletions apps/server/src/provider/acp/GrokAcpCliProbe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const makeProbeRuntime = Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
return yield* makeGrokAcpRuntime({
grokSettings: { binaryPath: "grok" },
environment: process.env,
environment: { ...process.env, XAI_API_KEY: "" },
childProcessSpawner,
cwd: process.cwd(),
clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
Expand Down Expand Up @@ -53,17 +53,28 @@ describe.runIf(process.env.T3_GROK_ACP_PROBE === "1")("Grok ACP CLI probe", () =
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("session/set_model accepts a no-op switch to the current model", () =>
it.effect("session/set_model selects Grok 4.5 when advertised", () =>
Effect.gen(function* () {
const runtime = yield* makeProbeRuntime;
const started = yield* runtime.start();
const currentModelId = started.sessionSetupResult.models?.currentModelId?.trim();
expect(currentModelId).toBeDefined();
if (!currentModelId) return;
const models = started.sessionSetupResult.models;
const grok45 = models?.availableModels.find((model) => model.modelId === "grok-4.5");
expect(grok45).toBeDefined();

// No-op switch — selecting the model the session already runs on must
// succeed against every Grok build that implements `session/set_model`.
yield* runtime.setSessionModel(currentModelId);
yield* runtime.setSessionModel("grok-4.5");
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("runs a real Grok 4.5 prompt through cached SuperGrok OAuth", () =>
Effect.gen(function* () {
const runtime = yield* makeProbeRuntime;
yield* runtime.start();
yield* runtime.setSessionModel("grok-4.5");

const result = yield* runtime.prompt({
prompt: [{ type: "text", text: "Reply with exactly: T3_GROK_45_OAUTH_OK" }],
});
expect(result.stopReason).toBe("end_turn");
}).pipe(Effect.timeout("90 seconds"), Effect.scoped, Effect.provide(NodeServices.layer)),
);
});
25 changes: 23 additions & 2 deletions apps/server/src/provider/acp/GrokAcpSupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,37 @@ import {
applyGrokAcpModelSelection,
buildGrokAcpSpawnInput,
resolveGrokAcpBaseModelId,
resolveGrokAuthMethodId,
} from "./GrokAcpSupport.ts";

describe("resolveGrokAcpBaseModelId", () => {
it("normalizes empty and custom Grok model ids", () => {
expect(resolveGrokAcpBaseModelId(undefined)).toBe("grok-build");
expect(resolveGrokAcpBaseModelId(" ")).toBe("grok-build");
expect(resolveGrokAcpBaseModelId(undefined)).toBe("grok-4.5");
expect(resolveGrokAcpBaseModelId(" ")).toBe("grok-4.5");
expect(resolveGrokAcpBaseModelId(" grok-test-custom-model ")).toBe("grok-test-custom-model");
});
});

describe("resolveGrokAuthMethodId", () => {
it("uses cached OAuth when no API key is configured", () => {
expect(resolveGrokAuthMethodId(undefined)).toBe("cached_token");
expect(resolveGrokAuthMethodId({ XAI_API_KEY: " " })).toBe("cached_token");
});

it("uses an API key only when the CLI advertises that method", () => {
expect(resolveGrokAuthMethodId({ XAI_API_KEY: "secret" })).toBe("xai.api_key");
expect(resolveGrokAuthMethodId({ XAI_API_KEY: "secret" }, ["cached_token", "grok.com"])).toBe(
"cached_token",
);
expect(resolveGrokAuthMethodId({ XAI_API_KEY: "secret" }, ["grok.com", "cached_token"])).toBe(
"cached_token",
);
expect(
resolveGrokAuthMethodId({ XAI_API_KEY: "secret" }, ["xai.api_key", "cached_token"]),
).toBe("xai.api_key");
});
});

describe("buildGrokAcpSpawnInput", () => {
it("passes the T3 Code referrer through Grok OAuth env", () => {
const spawn = buildGrokAcpSpawnInput({ binaryPath: "/usr/local/bin/grok" }, "/tmp/project", {
Expand Down
Loading
Loading