diff --git a/AGENTS.md b/AGENTS.md index bc578fd..003440f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,7 @@ src/ mcp/register.ts Native MCP server definition provider (VS Code 1.100+ API) status/details.ts Status presentation: buildStatusDetails, preferredStatusAction status/statusBar.ts Status bar item: create, refresh, dispose + status/refreshAfterInputChange.ts Clear inflight status then refresh bar and MCP workspace/readiness.ts Workspace readiness: environment detection, folder selection test/ unit/ Unit tests (node:test, dependency-injected, no VS Code API) @@ -55,6 +56,7 @@ test/ managedLifecycle.test.ts Managed install with real file I/O (22 tests) mcpConfig.test.ts MCP config with real temp directories (12 tests) mcpRegister.test.ts Native MCP definition helper for binary path (2 tests) + statusRefresh.test.ts Status and MCP refresh order after input change (1 test) outputChannel.test.ts Output channel logging wrapper (22 tests) patchloomCli.test.ts Patchloom CLI integration with real binary + managed install e2e MCP (46 tests incl. e2e) propertyBased.test.ts Property-based tests with fast-check (13 tests) diff --git a/src/extension.ts b/src/extension.ts index fc50891..16079cb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,8 @@ import { checkForUpdates } from "./commands/autoUpdate.js"; import { clearPatchloomStatusInflight } from "./binary/patchloom.js"; import { setManagedInstallRoot } from "./install/managed.js"; import { createPatchloomLog, getPatchloomLog, setPatchloomLog } from "./logging/outputChannel.js"; -import { registerMcpServerProviderWithBinary } from "./mcp/register.js"; +import { refreshMcpServerBinary, registerMcpServerProviderWithBinary } from "./mcp/register.js"; +import { refreshAfterPatchloomInputChange } from "./status/refreshAfterInputChange.js"; import { disposeStatusBar, refreshStatusBar } from "./status/statusBar.js"; export function activate(context: vscode.ExtensionContext): void { @@ -38,17 +39,26 @@ export function activate(context: vscode.ExtensionContext): void { new vscode.Disposable(disposeStatusBar), vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration("patchloom")) { - clearPatchloomStatusInflight(); - void refreshStatusBar(); + void refreshAfterPatchloomInputChange( + clearPatchloomStatusInflight, + refreshStatusBar, + refreshMcpServerBinary + ); } }), vscode.workspace.onDidChangeWorkspaceFolders(() => { - clearPatchloomStatusInflight(); - void refreshStatusBar(); + void refreshAfterPatchloomInputChange( + clearPatchloomStatusInflight, + refreshStatusBar, + refreshMcpServerBinary + ); }), vscode.workspace.onDidGrantWorkspaceTrust(() => { - clearPatchloomStatusInflight(); - void refreshStatusBar(); + void refreshAfterPatchloomInputChange( + clearPatchloomStatusInflight, + refreshStatusBar, + refreshMcpServerBinary + ); }) ); diff --git a/src/status/refreshAfterInputChange.ts b/src/status/refreshAfterInputChange.ts new file mode 100644 index 0000000..4420109 --- /dev/null +++ b/src/status/refreshAfterInputChange.ts @@ -0,0 +1,13 @@ +/** + * Drop a shared status probe and refresh surfaces that cache the CLI path. + * Callers pass the concrete refresh functions so unit tests can assert order. + */ +export async function refreshAfterPatchloomInputChange( + clearInflight: () => void, + refreshStatus: () => Promise, + refreshMcp: () => Promise +): Promise { + clearInflight(); + await refreshStatus(); + await refreshMcp(); +} diff --git a/test/unit/binary.test.ts b/test/unit/binary.test.ts index 1f5957d..21ca083 100644 --- a/test/unit/binary.test.ts +++ b/test/unit/binary.test.ts @@ -1141,15 +1141,22 @@ test("clearPatchloomStatusInflight starts a new probe while one is pending", asy clearPatchloomStatusInflight(); let calls = 0; let releaseFirst: (() => void) | undefined; + let releaseSecond: (() => void) | undefined; const firstGate = new Promise((resolve) => { releaseFirst = resolve; }); + const secondGate = new Promise((resolve) => { + releaseSecond = resolve; + }); const resolve = async () => { calls += 1; const n = calls; if (n === 1) { await firstGate; } + if (n === 2) { + await secondGate; + } return { ready: true, source: "path" as const, @@ -1162,10 +1169,14 @@ test("clearPatchloomStatusInflight starts a new probe while one is pending", asy clearPatchloomStatusInflight(); const second = resolvePatchloomStatusWithSharedInflight(resolve); releaseFirst?.(); - const [a, b] = await Promise.all([first, second]); + const third = resolvePatchloomStatusWithSharedInflight(resolve); + assert.equal(calls, 2); + releaseSecond?.(); + const [a, b, c] = await Promise.all([first, second, third]); assert.equal(calls, 2); assert.equal(a.message, "call-1"); assert.equal(b.message, "call-2"); + assert.equal(c.message, "call-2"); clearPatchloomStatusInflight(); }); diff --git a/test/unit/statusRefresh.test.ts b/test/unit/statusRefresh.test.ts new file mode 100644 index 0000000..a00ea7b --- /dev/null +++ b/test/unit/statusRefresh.test.ts @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { refreshAfterPatchloomInputChange } from "../../src/status/refreshAfterInputChange.js"; + +test("refreshAfterPatchloomInputChange clears inflight before status and MCP refresh", async () => { + const order: string[] = []; + await refreshAfterPatchloomInputChange( + () => { + order.push("clear"); + }, + async () => { + order.push("status"); + }, + async () => { + order.push("mcp"); + } + ); + assert.deepEqual(order, ["clear", "status", "mcp"]); +});