Skip to content

Commit cd39ea8

Browse files
authored
Fix stale cursor-acp plugin loading and stabilize flaky cache tests
- respect opencode plugin array before initializing cursor-acp\n- disable plugin entrypoint when cursor-acp is removed from config\n- add plugin toggle helper + tests\n- stabilize flaky cache timing assertions in unit/integration tests\n- make tmp-path assertions cross-platform
1 parent a1d8c52 commit cd39ea8

6 files changed

Lines changed: 181 additions & 28 deletions

File tree

src/plugin-entry.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
11
/**
2-
* Minimal entrypoint for the OpenCode plugin loader.
3-
* Exports only the CursorPlugin to avoid non-plugin exports being
4-
* treated as plugins by OpenCode.
2+
* OpenCode-only entrypoint.
3+
*
4+
* When cursor-acp is removed from the `plugin` array in opencode.json,
5+
* this entrypoint turns into a no-op so users can disable the plugin
6+
* without deleting the symlink file.
57
*/
6-
import { CursorPlugin } from "./plugin.js";
8+
import type { Plugin } from "@opencode-ai/plugin";
9+
import { shouldEnableCursorPlugin } from "./plugin-toggle.js";
10+
import { createLogger } from "./utils/logger.js";
711

8-
export default CursorPlugin;
12+
const log = createLogger("plugin-entry");
13+
14+
const CursorPluginEntry: Plugin = async (input) => {
15+
const state = shouldEnableCursorPlugin();
16+
if (!state.enabled) {
17+
log.info("Plugin disabled in OpenCode config; skipping initialization", {
18+
configPath: state.configPath,
19+
reason: state.reason,
20+
});
21+
return {};
22+
}
23+
24+
const mod = await import("./plugin.js");
25+
return mod.CursorPlugin(input);
26+
};
27+
28+
export default CursorPluginEntry;

src/plugin-toggle.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { existsSync, readFileSync } from "fs";
2+
import { homedir } from "os";
3+
import { join, resolve } from "path";
4+
5+
const CURSOR_PROVIDER_ID = "cursor-acp";
6+
7+
type EnvLike = Record<string, string | undefined>;
8+
9+
export function resolveOpenCodeConfigPath(env: EnvLike = process.env): string {
10+
if (env.OPENCODE_CONFIG && env.OPENCODE_CONFIG.length > 0) {
11+
return resolve(env.OPENCODE_CONFIG);
12+
}
13+
14+
const configHome = env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0
15+
? env.XDG_CONFIG_HOME
16+
: join(homedir(), ".config");
17+
18+
return join(configHome, "opencode", "opencode.json");
19+
}
20+
21+
export function isCursorPluginEnabledInConfig(config: unknown): boolean {
22+
if (!config || typeof config !== "object") {
23+
return true;
24+
}
25+
26+
const configObject = config as { plugin?: unknown; provider?: unknown };
27+
28+
if (Array.isArray(configObject.plugin)) {
29+
return configObject.plugin.some((entry) => entry === CURSOR_PROVIDER_ID);
30+
}
31+
32+
return true;
33+
}
34+
35+
export function shouldEnableCursorPlugin(env: EnvLike = process.env): {
36+
enabled: boolean;
37+
configPath: string;
38+
reason: string;
39+
} {
40+
const configPath = resolveOpenCodeConfigPath(env);
41+
42+
if (!existsSync(configPath)) {
43+
return {
44+
enabled: true,
45+
configPath,
46+
reason: "config_missing",
47+
};
48+
}
49+
50+
try {
51+
const raw = readFileSync(configPath, "utf8");
52+
const parsed = JSON.parse(raw);
53+
const enabled = isCursorPluginEnabledInConfig(parsed);
54+
55+
return {
56+
enabled,
57+
configPath,
58+
reason: enabled ? "enabled_in_plugin_array_or_legacy" : "disabled_in_plugin_array",
59+
};
60+
} catch {
61+
return {
62+
enabled: true,
63+
configPath,
64+
reason: "config_unreadable_or_invalid",
65+
};
66+
}
67+
}

tests/competitive/edge.test.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,18 @@ describe("Competitive Edge Analysis", () => {
113113

114114
it("should have faster model discovery with caching", async () => {
115115
const service = new ModelDiscoveryService({ cacheTTL: 60000 });
116+
let queryCalls = 0;
117+
const originalQuery = (service as any).queryCursorAgent.bind(service);
118+
(service as any).queryCursorAgent = async () => {
119+
queryCalls += 1;
120+
return originalQuery();
121+
};
116122

117-
// First discovery
118-
const start1 = Date.now();
119-
await service.discover();
120-
const time1 = Date.now() - start1;
121-
122-
// Second discovery (cached)
123-
const start2 = Date.now();
124-
await service.discover();
125-
const time2 = Date.now() - start2;
123+
const models1 = await service.discover();
124+
const models2 = await service.discover();
126125

127-
// Cached should be significantly faster
128-
expect(time2).toBeLessThan(time1);
126+
expect(models2).toEqual(models1);
127+
expect(queryCalls).toBe(1);
129128
});
130129

131130
it("should handle concurrent tool executions efficiently", async () => {

tests/integration/comprehensive.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -223,17 +223,18 @@ describe("Comprehensive End-to-End Integration", () => {
223223

224224
it("should cache model discovery", async () => {
225225
const service = new ModelDiscoveryService({ cacheTTL: 5000 });
226+
let queryCalls = 0;
227+
const originalQuery = (service as any).queryCursorAgent.bind(service);
228+
(service as any).queryCursorAgent = async () => {
229+
queryCalls += 1;
230+
return originalQuery();
231+
};
226232

227-
const startTime1 = Date.now();
228-
await service.discover();
229-
const endTime1 = Date.now();
230-
231-
const startTime2 = Date.now();
232-
await service.discover(); // Should be cached
233-
const endTime2 = Date.now();
233+
const models1 = await service.discover();
234+
const models2 = await service.discover();
234235

235-
// Second call should be much faster (cached)
236-
expect(endTime2 - startTime2).toBeLessThan(endTime1 - startTime1);
236+
expect(models2).toEqual(models1);
237+
expect(queryCalls).toBe(1);
237238
});
238239
});
239240

@@ -328,4 +329,4 @@ describe("Comprehensive End-to-End Integration", () => {
328329
}
329330
});
330331
});
331-
});
332+
});

tests/tools/defaults.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,17 @@ describe("Default Tools", () => {
7575
const registry = new ToolRegistry();
7676
registerDefaultTools(registry);
7777
const executor = new LocalExecutor(registry);
78+
const fs = await import("fs");
79+
const os = await import("os");
80+
const workdir = os.tmpdir();
7881

7982
const result = await executeWithChain([executor], "bash", {
8083
cmd: "pwd",
81-
workdir: "/tmp",
84+
workdir,
8285
});
8386

8487
expect(result.status).toBe("success");
85-
expect(result.output?.trim()).toBe("/tmp");
88+
expect(fs.realpathSync(result.output?.trim() ?? "")).toBe(fs.realpathSync(workdir));
8689
});
8790

8891
it("should execute read tool", async () => {

tests/unit/plugin-toggle.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/// <reference types="bun-types/test-globals" />
2+
3+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
4+
import { join } from "path";
5+
import { tmpdir } from "os";
6+
import {
7+
isCursorPluginEnabledInConfig,
8+
resolveOpenCodeConfigPath,
9+
shouldEnableCursorPlugin,
10+
} from "../../src/plugin-toggle";
11+
12+
describe("plugin toggle", () => {
13+
it("enables plugin when plugin array includes cursor-acp", () => {
14+
expect(isCursorPluginEnabledInConfig({ plugin: ["cursor-acp"] })).toBe(true);
15+
});
16+
17+
it("disables plugin when plugin array excludes cursor-acp", () => {
18+
expect(isCursorPluginEnabledInConfig({ plugin: ["other-plugin"] })).toBe(false);
19+
});
20+
21+
it("keeps legacy behavior when plugin array is missing", () => {
22+
expect(isCursorPluginEnabledInConfig({ provider: { "cursor-acp": {} } })).toBe(true);
23+
expect(isCursorPluginEnabledInConfig({ provider: {} })).toBe(true);
24+
});
25+
26+
it("resolves config from OPENCODE_CONFIG first", () => {
27+
const customConfig = join(tmpdir(), "custom-opencode.json");
28+
const xdgHome = join(tmpdir(), "xdg");
29+
const path = resolveOpenCodeConfigPath({
30+
OPENCODE_CONFIG: customConfig,
31+
XDG_CONFIG_HOME: xdgHome,
32+
});
33+
expect(path).toBe(customConfig);
34+
});
35+
36+
it("disables when config file exists and plugin array excludes cursor-acp", () => {
37+
const dir = mkdtempSync(join(tmpdir(), "cursor-toggle-"));
38+
const configPath = join(dir, "opencode.json");
39+
40+
try {
41+
writeFileSync(configPath, JSON.stringify({ plugin: ["other-plugin"] }));
42+
const state = shouldEnableCursorPlugin({ OPENCODE_CONFIG: configPath });
43+
expect(state.enabled).toBe(false);
44+
expect(state.reason).toBe("disabled_in_plugin_array");
45+
} finally {
46+
rmSync(dir, { recursive: true, force: true });
47+
}
48+
});
49+
50+
it("stays enabled when config is invalid JSON", () => {
51+
const dir = mkdtempSync(join(tmpdir(), "cursor-toggle-"));
52+
const configPath = join(dir, "opencode.json");
53+
54+
try {
55+
writeFileSync(configPath, "{not-json");
56+
const state = shouldEnableCursorPlugin({ OPENCODE_CONFIG: configPath });
57+
expect(state.enabled).toBe(true);
58+
expect(state.reason).toBe("config_unreadable_or_invalid");
59+
} finally {
60+
rmSync(dir, { recursive: true, force: true });
61+
}
62+
});
63+
});

0 commit comments

Comments
 (0)