Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
97 lines
4.5 KiB
TypeScript
97 lines
4.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
import TestRenderer, { act } from "react-test-renderer";
|
|
import ts from "typescript";
|
|
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
// Exercise the production lifecycle and asynchronous resolver together, without
|
|
// loading the unrelated provider forms or making real CLI/authentication calls.
|
|
const source = readFileSync(new URL("./SettingsAITab.tsx", import.meta.url), "utf8");
|
|
const lifecycleStart = source.indexOf(" const mountedRef = useRef(true);");
|
|
const lifecycleEnd = source.indexOf("\n const applyResolvedAgentPath", lifecycleStart);
|
|
const resolverStart = source.indexOf(" const resolveAgentPath = useCallback");
|
|
const resolverEnd = source.indexOf("\n useEffect(() => {", resolverStart);
|
|
assert.ok(lifecycleStart >= 0 && lifecycleEnd > lifecycleStart);
|
|
assert.ok(resolverStart >= 0 && resolverEnd > resolverStart);
|
|
const declarations = source.slice(lifecycleStart, lifecycleEnd) + source.slice(resolverStart, resolverEnd);
|
|
const setters = ["Codex", "Claude", "Copilot", "Cursor", "Codebuddy", "Opencode", "Grok"];
|
|
const bindings = ["useRef", "useEffect", "useCallback", "getBridge", "applyResolvedAgentPath", "cursorApiKeyEncrypted",
|
|
...setters.map(name => `setIsResolving${name}`)];
|
|
const runHooks = vm.runInNewContext(ts.transpile(`(function(ctx) {
|
|
const { ${bindings.join(",")} } = ctx;
|
|
${declarations}
|
|
return resolveAgentPath;
|
|
})`, { target: ts.ScriptTarget.ES2022 }), { console }) as
|
|
(ctx: Record<string, unknown>) => (key: string) => Promise<unknown>;
|
|
|
|
function deferred() {
|
|
let resolve!: (value: unknown) => void;
|
|
const promise = new Promise<unknown>(done => { resolve = done; });
|
|
return { promise, resolve };
|
|
}
|
|
|
|
async function mountResolver() {
|
|
const calls: ReturnType<typeof deferred>[] = [];
|
|
const applied: unknown[] = [];
|
|
const state: { busy: boolean; resolve?: (key: string) => Promise<unknown> } = { busy: false };
|
|
const bridge = { aiResolveCli: () => { const pending = deferred(); calls.push(pending); return pending.promise; } };
|
|
function Harness() {
|
|
const [busy, setBusy] = useState(false);
|
|
state.busy = busy;
|
|
state.resolve = runHooks({
|
|
useRef, useEffect, useCallback,
|
|
getBridge: () => bridge,
|
|
applyResolvedAgentPath: (_key: string, result: unknown) => applied.push(result),
|
|
cursorApiKeyEncrypted: "",
|
|
...Object.fromEntries(setters.map(name => [`setIsResolving${name}`, setBusy])),
|
|
});
|
|
return null;
|
|
}
|
|
let root!: TestRenderer.ReactTestRenderer;
|
|
await act(async () => { root = TestRenderer.create(React.createElement(React.StrictMode, null, React.createElement(Harness))); });
|
|
return { calls, applied, state, root };
|
|
}
|
|
|
|
test("AI detection accepts completed requests after StrictMode effect replay", async () => {
|
|
const h = await mountResolver();
|
|
try {
|
|
let request!: Promise<unknown>;
|
|
await act(async () => { request = h.state.resolve!("cursor"); });
|
|
assert.equal(h.state.busy, true);
|
|
const result = { installed: true, available: true, cliBinPath: "/fixture/cursor-agent" };
|
|
await act(async () => { h.calls[0].resolve(result); await request; });
|
|
assert.deepEqual(h.applied, [result]);
|
|
assert.equal(h.state.busy, false);
|
|
} finally { await act(async () => h.root.unmount()); }
|
|
});
|
|
|
|
test("AI detection still ignores completion after real unmount", async () => {
|
|
const h = await mountResolver();
|
|
let request!: Promise<unknown>;
|
|
await act(async () => { request = h.state.resolve!("cursor"); });
|
|
await act(async () => h.root.unmount());
|
|
h.calls[0].resolve({ installed: true });
|
|
await request;
|
|
assert.deepEqual(h.applied, []);
|
|
});
|
|
|
|
test("an older AI detection result cannot replace or finish the newer request", async () => {
|
|
const h = await mountResolver();
|
|
try {
|
|
let old!: Promise<unknown>;
|
|
let current!: Promise<unknown>;
|
|
await act(async () => { old = h.state.resolve!("cursor"); current = h.state.resolve!("cursor"); });
|
|
await act(async () => { h.calls[0].resolve({ installed: false }); await old; });
|
|
assert.deepEqual(h.applied, []);
|
|
assert.equal(h.state.busy, true);
|
|
const result = { installed: true };
|
|
await act(async () => { h.calls[1].resolve(result); await current; });
|
|
assert.deepEqual(h.applied, [result]);
|
|
assert.equal(h.state.busy, false);
|
|
} finally { await act(async () => h.root.unmount()); }
|
|
});
|