Skip to content

Commit f3a5890

Browse files
authored
🤖 fix: provider settings subscription race condition (#1066)
The `onConfigChanged` subscription handler was dropping notifications when no listener was actively waiting for them. This caused provider settings updates to not propagate reliably until window reload. ## Root Cause The async generator pattern in the subscription was: 1. Registering a callback with providerService 2. Entering a wait state (promise pending) 3. Callback fires and resolves the promise 4. Yield to subscriber **Bug**: If the callback fired _before_ step 2 (before entering wait state), the notification was silently dropped because `resolveNext` was null. ## Fix Added a `pendingNotification` flag that queues notifications arriving before the listener enters its waiting state. When the iterator loops, it checks this flag first and yields immediately if a notification is pending. _Generated with `mux`_
1 parent 48ca952 commit f3a5890

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

src/node/orpc/router.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,21 +94,33 @@ export const router = (authToken?: string) => {
9494
.output(schemas.providers.onConfigChanged.output)
9595
.handler(async function* ({ context }) {
9696
let resolveNext: (() => void) | null = null;
97+
let pendingNotification = false;
9798
let ended = false;
9899

99100
const push = () => {
100101
if (ended) return;
101102
if (resolveNext) {
103+
// Listener is waiting - wake it up
102104
const resolve = resolveNext;
103105
resolveNext = null;
104106
resolve();
107+
} else {
108+
// No listener waiting yet - queue the notification
109+
pendingNotification = true;
105110
}
106111
};
107112

108113
const unsubscribe = context.providerService.onConfigChanged(push);
109114

110115
try {
111116
while (!ended) {
117+
// If notification arrived before we started waiting, yield immediately
118+
if (pendingNotification) {
119+
pendingNotification = false;
120+
yield undefined;
121+
continue;
122+
}
123+
// Wait for next notification
112124
await new Promise<void>((resolve) => {
113125
resolveNext = resolve;
114126
});

tests/e2e/scenarios/settings.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,56 @@ test.describe("Settings Modal", () => {
117117
await expect(page.getByPlaceholder(/model-id/i)).toBeVisible();
118118
await expect(page.getByRole("button", { name: /^Add$/i })).toBeVisible();
119119
});
120+
121+
test("provider settings updates propagate without reload", async ({ ui, page }) => {
122+
await ui.projects.openFirstWorkspace();
123+
await ui.settings.open();
124+
await ui.settings.selectSection("Providers");
125+
126+
// Expand OpenAI provider - use the specific button with OpenAI icon
127+
const openaiButton = page
128+
.getByRole("button", { name: /OpenAI/i })
129+
.filter({ has: page.getByText("OpenAI icon") });
130+
await expect(openaiButton).toBeVisible();
131+
await openaiButton.click();
132+
133+
// Wait for the provider section to expand - API Key label should be visible
134+
await expect(page.getByText("API Key", { exact: true })).toBeVisible();
135+
136+
// Verify API key is not set initially (shows "Not set")
137+
await expect(page.getByText("Not set").first()).toBeVisible();
138+
139+
// Click "Set" to enter edit mode - it's a text link, not a button role
140+
const setLink = page.getByText("Set", { exact: true }).first();
141+
await setLink.click();
142+
143+
// The password input should appear with autofocus
144+
const apiKeyInput = page.locator('input[type="password"]');
145+
await expect(apiKeyInput).toBeVisible();
146+
await apiKeyInput.fill("sk-test-key-12345");
147+
148+
// Save by pressing Enter (the input has onKeyDown handler for Enter)
149+
await page.keyboard.press("Enter");
150+
151+
// Verify the field now shows as set (masked value)
152+
await expect(page.getByText("••••••••")).toBeVisible();
153+
154+
// Close settings
155+
await ui.settings.close();
156+
157+
// Re-open settings and verify the change persisted without reload
158+
await ui.settings.open();
159+
await ui.settings.selectSection("Providers");
160+
161+
// Expand OpenAI again
162+
await openaiButton.click();
163+
await expect(page.getByText("API Key", { exact: true })).toBeVisible();
164+
165+
// The API key should still show as set
166+
await expect(page.getByText("••••••••")).toBeVisible();
167+
168+
// The provider should show as configured (green indicator dot)
169+
const configuredIndicator = openaiButton.locator(".bg-green-500");
170+
await expect(configuredIndicator).toBeVisible();
171+
});
120172
});

0 commit comments

Comments
 (0)