Skip to content
Draft
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
125 changes: 125 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import { ProviderInstanceId } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import { HttpClient } from "effect/unstable/http";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import * as ClaudeAdapterV2 from "../../orchestration-v2/Adapters/ClaudeAdapterV2.ts";
import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import * as ResetCreditCoordinator from "../Layers/resetCreditCoordinator.ts";
import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import * as ModelManifest from "../ModelManifest.ts";
import { installFakeMise } from "../testUtils/fakeMise.ts";
import { ClaudeDriver } from "./ClaudeDriver.ts";

const testLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-claude-driver-maintenance-",
}).pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(IdAllocator.layer),
// Resolving maintenance never opens a Claude session.
Layer.provideMerge(Layer.mock(ClaudeAdapterV2.ClaudeAgentSdkQueryRunner)({})),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(ModelManifest.layerTest),
Layer.provideMerge(ResetCreditCoordinator.layerTest),
Layer.provideMerge(
Layer.mock(BackgroundPolicy.BackgroundPolicy)({
shouldRunScopeWork: () => Effect.succeed(false),
}),
),
Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
Layer.provideMerge(
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("Disabled Claude must not make an HTTP request")),
),
),
);

// These write `#!/bin/sh` stubs and symlinks, which Windows cannot resolve.
const windowsHost = HostProcessPlatform.defaultValue() === "win32";

it.layer(testLayer)("ClaudeDriver", (it) => {
it.effect.each([
{ layout: "native", miseOnPath: false, expected: "claude-update" },
{ layout: "mise wrapper", miseOnPath: false, expected: "manual" },
{ layout: "mise wrapper", miseOnPath: true, expected: "mise-upgrade" },
] as const)(
"resolves updates for a $layout at ~/.local/bin/claude (mise on PATH: $miseOnPath)",
({ layout, miseOnPath, expected }) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs
.makeTempDirectoryScoped({ prefix: "t3-claude-update-" })
.pipe(Effect.flatMap((directory) => fs.realPath(directory)));
const binaryPath = path.join(home, ".local", "bin", "claude");
yield* fs.makeDirectory(path.dirname(binaryPath), { recursive: true });
const fake = installFakeMise(path.join(home, "mise-bin", "mise"), {
which: { claude: path.join(home, ".local/share/mise/installs/claude/2.1.5/bin/claude") },
ls: {
claude: [
{
version: "2.1.5",
install_path: path.join(home, ".local/share/mise/installs/claude/2.1.5"),
active: true,
},
],
},
outdated: {},
});
if (layout === "native") {
const versionPath = path.join(home, ".local", "share", "claude", "versions", "2.1.0");
yield* fs.makeDirectory(path.dirname(versionPath), { recursive: true });
yield* fs.writeFileString(versionPath, "#!/bin/sh\n");
yield* fs.chmod(versionPath, 0o755);
yield* fs.symlink(versionPath, binaryPath);
} else {
const miseBin = path.join(home, ".local/share/mise/installs/claude/2.1.5/bin/claude");
yield* fs.makeDirectory(path.dirname(miseBin), { recursive: true });
yield* fs.writeFileString(miseBin, "#!/bin/sh\n");
yield* fs.writeFileString(
binaryPath,
'#!/bin/bash\nexport MISE_MINIMUM_RELEASE_AGE=0\nmise use -g --quiet "claude" || exit 1\nexec mise x "claude" -- "claude" "$@"\n',
);
yield* fs.chmod(binaryPath, 0o755);
}

const instance = yield* ClaudeDriver.create({
instanceId: ProviderInstanceId.make("claude-update"),
displayName: "Claude test",
enabled: false,
environment: [
{ name: "HOME", value: home, sensitive: false },
{
name: "PATH",
value: miseOnPath ? path.dirname(fake.misePath) : "",
sensitive: false,
},
],
config: { ...ClaudeDriver.defaultConfig(), binaryPath },
});

const update = (yield* instance.snapshot.resolveMaintenance()).update;
if (expected === "claude-update") {
expect(update).toMatchObject({ executable: binaryPath, args: ["update"] });
} else if (expected === "mise-upgrade") {
expect(update).toMatchObject({
executable: fake.misePath,
args: ["upgrade", "--no-prune", "claude"],
});
} else {
// `claude update` would only report that a package manager owns it.
expect(update).toBeNull();
}
}).pipe(Effect.scoped),
{ skip: windowsHost },
);
});
10 changes: 7 additions & 3 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,16 @@ const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
const CAPABILITIES_PROBE_TTL = Duration.minutes(5);

/**
* POSIX native launchers symlink into `~/.local/share/claude/`, so a plain
* `~/.local/bin/claude` is someone else's launcher (Omarchy's mise wrapper),
* where `claude update` reports a package manager and changes nothing.
* Windows copies `claude.exe` there instead of linking it.
*/
function isClaudeNativeCommandPath(commandPath: string): boolean {
const normalized = normalizeCommandPath(commandPath);
return (
normalized.endsWith("/.local/bin/claude") ||
normalized.endsWith("/.local/bin/claude.exe") ||
normalized.includes("/.local/share/claude/")
normalized.endsWith("/.local/bin/claude.exe") || normalized.includes("/.local/share/claude/")
);
}

Expand Down
95 changes: 53 additions & 42 deletions apps/server/src/provider/Drivers/CodexDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,12 @@ it.layer(testLayer)("CodexDriver", (it) => {

for (const fixture of [
{
name: "leaves mise npm-backend installations manual-only",
name: "leaves mise npm-backend installations manual-only without a mise to confirm them",
installSegments: ["mise", "installs", "npm-openai-codex", "0.110.0"],
npmOwned: false,
},
{
name: "leaves mise tool aliases backed by npm manual-only",
name: "leaves mise tool aliases backed by npm manual-only without a mise to confirm them",
installSegments: ["mise", "installs", "codex", "0.110.0"],
npmOwned: false,
},
Expand Down Expand Up @@ -373,7 +373,8 @@ it.layer(testLayer)("CodexDriver", (it) => {
instanceId: ProviderInstanceId.make("codex-installer"),
displayName: "Codex installer test",
enabled: false,
environment: [],
// No mise on PATH: these cases exercise the path rules alone.
environment: [{ name: "PATH", value: "", sensitive: false }],
config: {
...CodexDriver.defaultConfig(),
binaryPath,
Expand Down Expand Up @@ -405,42 +406,44 @@ it.layer(testLayer)("CodexDriver", (it) => {
}

for (const layout of ["direct", "wrapper"] as const) {
it.effect.skipIf(windowsHost)(`leaves a mise ${layout} installation manual-only`, () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs
.makeTempDirectoryScoped({ prefix: `t3-codex-mise-${layout}-` })
.pipe(Effect.flatMap((directory) => fs.realPath(directory)));
const binaryPath =
layout === "direct"
? NodePath.join(tempDir, "mise", "installs", "codex", "0.110.0", "codex")
: NodePath.join(tempDir, "omarchy", "bin", "codex");
yield* fs.makeDirectory(NodePath.dirname(binaryPath), { recursive: true });
yield* fs.writeFileString(
binaryPath,
layout === "direct"
? "#!/bin/sh\n"
: '#!/bin/sh\nmise use -g --quiet "codex" || exit 1\nexec mise x "codex" -- "codex" "$@"\n',
);
yield* fs.chmod(binaryPath, 0o755);

const instance = yield* CodexDriver.create({
instanceId: ProviderInstanceId.make(`codex-mise-${layout}`),
displayName: "Codex mise test",
enabled: false,
environment: [],
config: {
...CodexDriver.defaultConfig(),
it.effect.skipIf(windowsHost)(
`leaves a mise ${layout} installation manual-only without a mise to confirm it`,
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs
.makeTempDirectoryScoped({ prefix: `t3-codex-mise-${layout}-` })
.pipe(Effect.flatMap((directory) => fs.realPath(directory)));
const binaryPath =
layout === "direct"
? NodePath.join(tempDir, "mise", "installs", "codex", "0.110.0", "codex")
: NodePath.join(tempDir, "omarchy", "bin", "codex");
yield* fs.makeDirectory(NodePath.dirname(binaryPath), { recursive: true });
yield* fs.writeFileString(
binaryPath,
homePath: NodePath.join(tempDir, "codex-home"),
},
});
layout === "direct"
? "#!/bin/sh\n"
: '#!/bin/sh\nmise use -g --quiet "codex" || exit 1\nexec mise x "codex" -- "codex" "$@"\n',
);
yield* fs.chmod(binaryPath, 0o755);

expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn),
Effect.scoped,
),
const instance = yield* CodexDriver.create({
instanceId: ProviderInstanceId.make(`codex-mise-${layout}`),
displayName: "Codex mise test",
enabled: false,
environment: [{ name: "PATH", value: "", sensitive: false }],
config: {
...CodexDriver.defaultConfig(),
binaryPath,
homePath: NodePath.join(tempDir, "codex-home"),
},
});

expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn),
Effect.scoped,
),
);
}

Expand Down Expand Up @@ -517,19 +520,26 @@ it.layer(testLayer)("CodexDriver", (it) => {
NodePath.dirname(brewPath),
].join(NodePath.delimiter);
const probes: Array<ReadonlyArray<string>> = [];
const miseProbes: Array<ReadonlyArray<string>> = [];
const metadataSpawner = ChildProcessSpawner.make((command) => {
if (!ChildProcess.isStandardCommand(command) || command.command !== brewPath) {
if (
!ChildProcess.isStandardCommand(command) ||
(command.command !== brewPath && command.command !== misePath)
) {
return Effect.die("Provider resolution must not execute a provider or updater");
}
probes.push(command.args);
const stdout =
command.args[0] === "--prefix"
// This mise has no tool behind the shim, so it cannot own Codex.
const isMise = command.command === misePath;
(isMise ? miseProbes : probes).push(command.args);
const stdout = isMise
? ""
: command.args[0] === "--prefix"
? brewPrefix
: JSON.stringify({ formulae: [{ versions: { stable: "2026.9.1" } }] });
return Effect.succeed(
ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(1),
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(isMise ? 1 : 0)),
isRunning: Effect.succeed(false),
kill: () => Effect.void,
unref: Effect.succeed(Effect.void),
Expand Down Expand Up @@ -563,6 +573,7 @@ it.layer(testLayer)("CodexDriver", (it) => {
),
);
expect(probes).toEqual([]);
expect(miseProbes).toEqual(fixture.nodeFirst ? [] : [["which", fixture.commandName]]);
expect(latestVersion).toBe("0.153.4");
expect(
createProviderVersionAdvisory({
Expand Down
42 changes: 42 additions & 0 deletions apps/server/src/provider/Drivers/GrokDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import * as ServerConfig from "../../config.ts";
import * as ServerSettings from "../../serverSettings.ts";
import * as ProviderEventLoggers from "../Layers/ProviderEventLoggers.ts";
import { installFakeMise } from "../testUtils/fakeMise.ts";
import { GrokDriver } from "./GrokDriver.ts";

import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";
Expand Down Expand Up @@ -84,6 +85,47 @@ it.layer(testLayer)("GrokDriver", (it) => {
),
);

it.effect.skipIf(windowsHost)(
"upgrades a mise-managed Grok through mise, not `grok update`",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs
.makeTempDirectoryScoped({ prefix: "t3-grok-mise-" })
.pipe(Effect.flatMap((directory) => fs.realPath(directory)));
const toolDir = path.join(tempDir, "mise", "installs", "npm-xai-official-grok");
const installPath = path.join(toolDir, "1.0.0");
yield* fs.makeDirectory(path.join(installPath, "bin"), { recursive: true });
yield* fs.writeFileString(path.join(installPath, "bin", "grok"), "#!/bin/sh\n");
yield* fs.chmod(path.join(installPath, "bin", "grok"), 0o755);
yield* fs.symlink("1.0.0", path.join(toolDir, "latest"));
const binaryPath = path.join(toolDir, "latest", "bin", "grok");
const tool = "npm:@xai-official/grok";
const fake = installFakeMise(path.join(tempDir, "bin", "mise"), {
ls: {
[tool]: [{ version: "1.0.0", install_path: installPath, active: true }],
},
outdated: { [tool]: { latest: "1.1.0" } },
});

const instance = yield* GrokDriver.create({
instanceId: ProviderInstanceId.make("grok-mise"),
displayName: "Grok test",
enabled: false,
environment: [{ name: "PATH", value: path.dirname(fake.misePath), sensitive: false }],
config: { ...GrokDriver.defaultConfig(), binaryPath },
});

const capabilities = yield* instance.snapshot.resolveMaintenance();
expect(capabilities.update).toMatchObject({
executable: fake.misePath,
args: ["upgrade", "--no-prune", tool],
});
expect(capabilities.latestVersion).toBe("1.1.0");
}).pipe(Effect.scoped),
);

it.effect("stays manual-only when the configured executable does not exist", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
Loading
Loading