mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 02:56:06 +00:00
* fix (Computer Use) Stabilize Codex Computer Use readiness Co-authored-by: Ben Badejo <188106718+bdjben@users.noreply.github.com> * fix (Computer Use) Stabilize Codex Computer Use readiness * fix (Computer Use) Stabilize Codex Computer Use readiness * fix (Computer Use) Stabilize Codex Computer Use readiness * fix(clawsweeper): address review for automerge-openclaw-openclaw-103331 (1) * fix (Computer Use) Stabilize Codex Computer Use readiness --------- Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: Ben Badejo <188106718+bdjben@users.noreply.github.com>
358 lines
13 KiB
TypeScript
358 lines
13 KiB
TypeScript
// Codex tests cover Computer Use shared plugin cache reconciliation.
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { ensureCodexComputerUseSharedPluginCache } from "./computer-use-cache.js";
|
|
import type { ResolvedCodexComputerUseConfig } from "./config.js";
|
|
import { useAutoCleanupTempDirTracker } from "./test-support.js";
|
|
|
|
describe("Codex Computer Use shared plugin cache", () => {
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
it("prefers the current ChatGPT.app bundled marketplace when both desktop app candidates exist", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const chatGptMarketplacePath = path.join(
|
|
root,
|
|
"Applications",
|
|
"ChatGPT.app",
|
|
"Contents",
|
|
"Resources",
|
|
"plugins",
|
|
"openai-bundled",
|
|
);
|
|
const legacyCodexMarketplacePath = path.join(
|
|
root,
|
|
"Applications",
|
|
"Codex.app",
|
|
"Contents",
|
|
"Resources",
|
|
"plugins",
|
|
"openai-bundled",
|
|
);
|
|
await writeBundledComputerUsePlugin(chatGptMarketplacePath, "2.0.0");
|
|
await writeBundledComputerUsePlugin(legacyCodexMarketplacePath, "1.0.0");
|
|
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome: path.join(root, "agent", "codex-home"),
|
|
bundledMarketplacePathCandidates: [chatGptMarketplacePath, legacyCodexMarketplacePath],
|
|
config: computerUseConfig(),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "shared",
|
|
version: "2.0.0",
|
|
targetPath: path.join(chatGptMarketplacePath, "plugins", "computer-use"),
|
|
});
|
|
});
|
|
|
|
it("falls back to the legacy Codex.app bundled marketplace when ChatGPT.app is absent", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const chatGptMarketplacePath = path.join(
|
|
root,
|
|
"Applications",
|
|
"ChatGPT.app",
|
|
"Contents",
|
|
"Resources",
|
|
"plugins",
|
|
"openai-bundled",
|
|
);
|
|
const legacyCodexMarketplacePath = path.join(
|
|
root,
|
|
"Applications",
|
|
"Codex.app",
|
|
"Contents",
|
|
"Resources",
|
|
"plugins",
|
|
"openai-bundled",
|
|
);
|
|
await writeBundledComputerUsePlugin(legacyCodexMarketplacePath, "1.0.0");
|
|
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome: path.join(root, "agent", "codex-home"),
|
|
bundledMarketplacePathCandidates: [chatGptMarketplacePath, legacyCodexMarketplacePath],
|
|
config: computerUseConfig(),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "shared",
|
|
version: "1.0.0",
|
|
targetPath: path.join(legacyCodexMarketplacePath, "plugins", "computer-use"),
|
|
});
|
|
});
|
|
|
|
it("copies the bundled plugin without removing versions used by live clients", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const bundledMarketplacePath = path.join(root, "Codex.app", "plugins", "openai-bundled");
|
|
const bundledPluginRoot = path.join(bundledMarketplacePath, "plugins", "computer-use");
|
|
await fs.mkdir(path.join(bundledPluginRoot, ".codex-plugin"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(bundledPluginRoot, ".codex-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "computer-use", version: "1.0.857" }),
|
|
);
|
|
const codexHome = path.join(root, "agent", "codex-home");
|
|
const activeCachePath = path.join(
|
|
codexHome,
|
|
"plugins",
|
|
"cache",
|
|
"openai-bundled",
|
|
"computer-use",
|
|
"1.0.857",
|
|
);
|
|
const priorCachePath = path.join(
|
|
codexHome,
|
|
"plugins",
|
|
"cache",
|
|
"openai-bundled",
|
|
"computer-use",
|
|
"1.0.799",
|
|
);
|
|
await fs.mkdir(priorCachePath, { recursive: true });
|
|
await fs.writeFile(path.join(priorCachePath, "live-client-marker"), "in use");
|
|
await fs.symlink(bundledPluginRoot, activeCachePath, "dir");
|
|
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome,
|
|
bundledMarketplacePath,
|
|
config: computerUseConfig(),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "shared",
|
|
changed: true,
|
|
version: "1.0.857",
|
|
removedStaleVersions: [],
|
|
});
|
|
await expect(
|
|
fs.readFile(path.join(priorCachePath, "live-client-marker"), "utf8"),
|
|
).resolves.toBe("in use");
|
|
const cacheEntries = await fs.readdir(path.dirname(activeCachePath), {
|
|
withFileTypes: true,
|
|
});
|
|
const activeCacheEntry = cacheEntries.find((entry) => entry.name === "1.0.857");
|
|
expect(activeCacheEntry?.isDirectory()).toBe(true);
|
|
expect(activeCacheEntry?.isSymbolicLink()).toBe(false);
|
|
expect((await fs.lstat(activeCachePath)).isDirectory()).toBe(true);
|
|
expect((await fs.lstat(activeCachePath)).isSymbolicLink()).toBe(false);
|
|
await expect(
|
|
fs.access(path.join(activeCachePath, ".codex-plugin", "plugin.json")),
|
|
).resolves.toBe(undefined);
|
|
await expect(fs.access(priorCachePath)).resolves.toBe(undefined);
|
|
});
|
|
|
|
it("leaves an up-to-date copied cache entry unchanged", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const bundledMarketplacePath = path.join(root, "Codex.app", "plugins", "openai-bundled");
|
|
const bundledPluginRoot = path.join(bundledMarketplacePath, "plugins", "computer-use");
|
|
await fs.mkdir(path.join(bundledPluginRoot, ".codex-plugin"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(bundledPluginRoot, ".codex-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "computer-use", version: "1.0.857" }),
|
|
);
|
|
const codexHome = path.join(root, "agent", "codex-home");
|
|
const activeCachePath = path.join(
|
|
codexHome,
|
|
"plugins",
|
|
"cache",
|
|
"openai-bundled",
|
|
"computer-use",
|
|
"1.0.857",
|
|
);
|
|
await fs.mkdir(path.dirname(activeCachePath), { recursive: true });
|
|
await fs.cp(bundledPluginRoot, activeCachePath, { recursive: true });
|
|
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome,
|
|
bundledMarketplacePath,
|
|
config: computerUseConfig(),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "shared",
|
|
changed: false,
|
|
version: "1.0.857",
|
|
removedStaleVersions: [],
|
|
});
|
|
expect((await fs.lstat(activeCachePath)).isDirectory()).toBe(true);
|
|
expect((await fs.lstat(activeCachePath)).isSymbolicLink()).toBe(false);
|
|
await expect(
|
|
fs.access(path.join(activeCachePath, ".codex-plugin", "plugin.json")),
|
|
).resolves.toBe(undefined);
|
|
});
|
|
|
|
it("refreshes a stale copied cache entry with the bundled version", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const bundledMarketplacePath = path.join(root, "Codex.app", "plugins", "openai-bundled");
|
|
const bundledPluginRoot = path.join(bundledMarketplacePath, "plugins", "computer-use");
|
|
await fs.mkdir(path.join(bundledPluginRoot, ".codex-plugin"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(bundledPluginRoot, ".codex-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "computer-use", version: "1.0.857" }),
|
|
);
|
|
const codexHome = path.join(root, "agent", "codex-home");
|
|
const activeCachePath = path.join(
|
|
codexHome,
|
|
"plugins",
|
|
"cache",
|
|
"openai-bundled",
|
|
"computer-use",
|
|
"1.0.857",
|
|
);
|
|
await fs.mkdir(path.join(activeCachePath, ".codex-plugin"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(activeCachePath, ".codex-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "computer-use", version: "1.0.799" }),
|
|
);
|
|
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome,
|
|
bundledMarketplacePath,
|
|
config: computerUseConfig(),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "shared",
|
|
changed: true,
|
|
version: "1.0.857",
|
|
removedStaleVersions: [],
|
|
});
|
|
await expect(
|
|
fs.readFile(path.join(activeCachePath, ".codex-plugin", "plugin.json"), "utf8"),
|
|
).resolves.toContain('"version":"1.0.857"');
|
|
});
|
|
|
|
it("leaves cache entries alone in independent mode", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome: path.join(root, "codex-home"),
|
|
bundledMarketplacePath: path.join(root, "missing"),
|
|
config: computerUseConfig({ pluginCacheMode: "independent" }),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "independent",
|
|
changed: false,
|
|
removedStaleVersions: [],
|
|
});
|
|
});
|
|
|
|
it("preserves an explicitly named marketplace cache", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const codexHome = path.join(root, "agent", "codex-home");
|
|
const cacheRoot = path.join(codexHome, "plugins", "cache", "desktop-tools", "computer-use");
|
|
await fs.mkdir(path.join(cacheRoot, "1.0.101"), { recursive: true });
|
|
await fs.mkdir(path.join(cacheRoot, "1.0.102"), { recursive: true });
|
|
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome,
|
|
bundledMarketplacePath: path.join(root, "missing-bundled-marketplace"),
|
|
config: computerUseConfig({ marketplaceName: "desktop-tools" }),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "explicit_marketplace",
|
|
changed: false,
|
|
removedStaleVersions: [],
|
|
});
|
|
await expect(fs.access(path.join(cacheRoot, "1.0.101"))).resolves.toBe(undefined);
|
|
await expect(fs.access(path.join(cacheRoot, "1.0.102"))).resolves.toBe(undefined);
|
|
});
|
|
|
|
it("preserves the default namespace when marketplacePath is explicit", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const codexHome = path.join(root, "agent", "codex-home");
|
|
const cacheRoot = path.join(codexHome, "plugins", "cache", "openai-bundled", "computer-use");
|
|
await fs.mkdir(path.join(cacheRoot, "1.0.101"), { recursive: true });
|
|
await fs.mkdir(path.join(cacheRoot, "1.0.102"), { recursive: true });
|
|
|
|
const result = await ensureCodexComputerUseSharedPluginCache({
|
|
codexHome,
|
|
bundledMarketplacePath: path.join(root, "missing-bundled-marketplace"),
|
|
config: computerUseConfig({
|
|
marketplacePath: path.join(
|
|
root,
|
|
"custom-marketplace",
|
|
".agents",
|
|
"plugins",
|
|
"marketplace.json",
|
|
),
|
|
}),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "explicit_marketplace",
|
|
changed: false,
|
|
removedStaleVersions: [],
|
|
});
|
|
await expect(fs.access(path.join(cacheRoot, "1.0.101"))).resolves.toBe(undefined);
|
|
await expect(fs.access(path.join(cacheRoot, "1.0.102"))).resolves.toBe(undefined);
|
|
});
|
|
|
|
it("preserves the active cache when replacement copying fails", async () => {
|
|
const root = tempDirs.make("openclaw-computer-use-cache-");
|
|
const codexHome = path.join(root, "agent", "codex-home");
|
|
const bundledMarketplacePath = path.join(root, "bundled-marketplace");
|
|
const cachePath = path.join(
|
|
codexHome,
|
|
"plugins",
|
|
"cache",
|
|
"openai-bundled",
|
|
"computer-use",
|
|
"2.0.0",
|
|
);
|
|
const manifestPath = path.join(cachePath, ".codex-plugin", "plugin.json");
|
|
const olderCachePath = path.join(path.dirname(cachePath), "1.0.0");
|
|
const olderManifestPath = path.join(olderCachePath, ".codex-plugin", "plugin.json");
|
|
await writeBundledComputerUsePlugin(bundledMarketplacePath, "2.0.0");
|
|
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
await fs.writeFile(manifestPath, JSON.stringify({ name: "computer-use", version: "old" }));
|
|
await fs.mkdir(path.dirname(olderManifestPath), { recursive: true });
|
|
await fs.writeFile(
|
|
olderManifestPath,
|
|
JSON.stringify({ name: "computer-use", version: "1.0.0" }),
|
|
);
|
|
vi.spyOn(fs, "cp").mockRejectedValueOnce(new Error("copy failed"));
|
|
|
|
await expect(
|
|
ensureCodexComputerUseSharedPluginCache({
|
|
codexHome,
|
|
bundledMarketplacePath,
|
|
config: computerUseConfig(),
|
|
}),
|
|
).rejects.toThrow("copy failed");
|
|
await expect(fs.readFile(manifestPath, "utf8")).resolves.toContain('"version":"old"');
|
|
await expect(fs.readFile(olderManifestPath, "utf8")).resolves.toContain('"version":"1.0.0"');
|
|
});
|
|
});
|
|
|
|
async function writeBundledComputerUsePlugin(
|
|
bundledMarketplacePath: string,
|
|
version: string,
|
|
): Promise<void> {
|
|
const bundledPluginRoot = path.join(bundledMarketplacePath, "plugins", "computer-use");
|
|
await fs.mkdir(path.join(bundledPluginRoot, ".codex-plugin"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(bundledPluginRoot, ".codex-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "computer-use", version }),
|
|
);
|
|
}
|
|
|
|
function computerUseConfig(
|
|
overrides: Partial<ResolvedCodexComputerUseConfig> = {},
|
|
): ResolvedCodexComputerUseConfig {
|
|
return {
|
|
enabled: true,
|
|
autoInstall: true,
|
|
marketplaceDiscoveryTimeoutMs: 60_000,
|
|
liveTestTimeoutMs: 60_000,
|
|
toolCallTimeoutMs: 60_000,
|
|
healthCheckEnabled: false,
|
|
healthCheckIntervalMinutes: 60,
|
|
pluginCacheMode: "shared",
|
|
strictReadiness: false,
|
|
autoRepair: false,
|
|
pluginName: "computer-use",
|
|
mcpServerName: "computer-use",
|
|
...overrides,
|
|
};
|
|
}
|