From 26480931faa6c4ad0330378efe22cba523de80f6 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 3 Sep 2026 15:18:59 +0900 Subject: [PATCH 1/4] fix(gui): hide unavailable key rotation controls --- .../components/apikeys-workspace/ApiKeysWorkspace.tsx | 5 +++-- gui/tests/apikeys-actions.test.tsx | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx index dba0e23591b..76a2835d9f8 100644 --- a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx +++ b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx @@ -140,6 +140,7 @@ export default function ApiKeysWorkspace({ const selectedRotationId = selected ? (rotationSecret?.id === selected.id ? rotationSecret.rotationId : selected.pendingRotation?.id) : undefined; + const rotationEnabled = Boolean(onRotationStart || onRotationCommit || onRotationAbort); const mutationPending = deleting || renamePending || rotationPending; const runRotation = async (operation: "start" | "commit" | "abort") => { @@ -356,7 +357,7 @@ export default function ApiKeysWorkspace({ -
+ {rotationEnabled &&

{t("api.rotation.title")}

{selectedRotationId ? ( <> @@ -394,7 +395,7 @@ export default function ApiKeysWorkspace({ )} {rotationFailed &&

{t("api.rotation.failed")}

} -
+
}

{t("api.attribution.title")}

{/* Branch on the DATASET field, not on `usage`: a key with zero diff --git a/gui/tests/apikeys-actions.test.tsx b/gui/tests/apikeys-actions.test.tsx index ae3bffeceff..f7db49c10f7 100644 --- a/gui/tests/apikeys-actions.test.tsx +++ b/gui/tests/apikeys-actions.test.tsx @@ -284,6 +284,16 @@ test("rotation start, one-time secret, commit, and abort stay explicit", async ( expect(calls).toContain("abort:k1:rotation-1"); }); +test("rotation controls stay hidden when the runtime supplies no rotation handlers", async () => { + const container = await mount({}); + await openKey(container); + + expect(container.textContent).not.toContain("Key rotation"); + expect(container.textContent).not.toContain("Start rotation"); + expect(container.textContent).not.toContain("Commit rotation"); + expect(container.textContent).not.toContain("Abort rotation"); +}); + test("a protocol result belongs to its own chip", async () => { const container = await mount({ filteredModels: [{ id: "gpt-5.4", displayName: "gpt-5.4", provider: "openai", native: true }], From dd078abae49c2403804521d5216831ad06691175 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:37:57 +0000 Subject: [PATCH 2/4] fix(gui): gate API key rotation actions by their wired handlers Co-Authored-By: Epinephrine --- .../apikeys-workspace/ApiKeysWorkspace.tsx | 24 ++++++--- gui/tests/apikeys-actions.test.tsx | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx index 138abe8d839..4a42ab587e9 100644 --- a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx +++ b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx @@ -147,7 +147,13 @@ export default function ApiKeysWorkspace({ const selectedRotationId = selected ? (rotationSecret?.id === selected.id ? rotationSecret.rotationId : selected.pendingRotation?.id) : undefined; - const rotationEnabled = Boolean(onRotationStart || onRotationCommit || onRotationAbort); + // Each handler is independently optional, so "enabled" holds only when the + // key's current state has an action the caller wired: start for an idle key, + // commit/abort for a pending one. Rendering the rest offers operations that + // can only fail locally. + const rotationEnabled = selectedRotationId + ? Boolean(onRotationCommit || onRotationAbort) + : Boolean(onRotationStart); const mutationPending = deleting || renamePending || rotationPending; const runRotation = async (operation: "start" | "commit" | "abort") => { @@ -387,12 +393,16 @@ export default function ApiKeysWorkspace({
)}
- - + {onRotationCommit && ( + + )} + {onRotationAbort && ( + + )}
) : ( diff --git a/gui/tests/apikeys-actions.test.tsx b/gui/tests/apikeys-actions.test.tsx index 9711a270104..83ce62ecc98 100644 --- a/gui/tests/apikeys-actions.test.tsx +++ b/gui/tests/apikeys-actions.test.tsx @@ -294,6 +294,56 @@ test("rotation controls stay hidden when the runtime supplies no rotation handle expect(container.textContent).not.toContain("Abort rotation"); }); +test("an idle key offers rotation only when its start handler is wired", async () => { + // Commit/abort without start: the only action an idle key can take has no + // handler, so the whole section hides — a visible Start could only fail. + const container = await mount({ + onRotationCommit: async () => true, + onRotationAbort: async () => true, + }); + await openKey(container); + + expect(container.textContent).not.toContain("Key rotation"); + expect(container.textContent).not.toContain("Start rotation"); +}); + +test("a pending key renders only the rotation actions that have handlers", async () => { + const pendingKey = { + id: "k1", + name: "alpha", + prefix: "ocx_data_aaaaaaaa...", + createdAt: "2026-01-01T00:00:00.000Z", + pendingRotation: { + id: "rotation-1", + createdAt: "2026-08-28T00:00:00.000Z", + expiresAt: "2026-08-28T00:10:00.000Z", + }, + usage: { requests7d: 0, totalRequests: 0 }, + }; + + // Commit without abort: Commit renders, Abort does not. + const container = await mount({ + keys: [pendingKey], + onRotationCommit: async () => true, + }); + await openKey(container); + expect(container.textContent).toContain("Commit rotation"); + expect(container.textContent).not.toContain("Abort rotation"); + + await act(async () => { active?.unmount(); active = null; }); + + // Start without commit/abort: no action applies to a pending key, so the + // section hides rather than offering a Start that cannot help it. + const startOnly = await mount({ + keys: [pendingKey], + onRotationStart: async () => true, + }); + await openKey(startOnly); + expect(startOnly.textContent).not.toContain("Key rotation"); + expect(startOnly.textContent).not.toContain("Commit rotation"); + expect(startOnly.textContent).not.toContain("Abort rotation"); +}); + test("a protocol result belongs to its own chip", async () => { const container = await mount({ filteredModels: [{ id: "gpt-5.5", displayName: "gpt-5.5", provider: "openai", native: true }], From bed96ddfe3de31e61fa6ff62c9cb2415e6879157 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:29:20 +0000 Subject: [PATCH 3/4] fix(gui): keep the issued rotation secret visible for partial integrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A start-only caller's successful rotation flips selectedRotationId truthy, which switched the section guard to commit/abort and unmounted the reveal — stranding the one-time secret. Treat a matching rotationSecret as its own reason to render, and wire Copy/Close to their optional callbacks instead of always drawing them. Adds a rerender test for the start->secret flow and coverage for the secret controls' independent gating. Co-Authored-By: Epinephrine --- .../apikeys-workspace/ApiKeysWorkspace.tsx | 48 +++++++------ gui/tests/apikeys-actions.test.tsx | 71 +++++++++++++++++++ 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx index 4a42ab587e9..21198082162 100644 --- a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx +++ b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx @@ -144,16 +144,18 @@ export default function ApiKeysWorkspace({ const [rotationFailed, setRotationFailed] = useState(false); const selected = selectedId ? (keys.find(k => k.id === selectedId) ?? null) : null; + const selectedHasRotationSecret = Boolean(selected && rotationSecret?.id === selected.id); const selectedRotationId = selected ? (rotationSecret?.id === selected.id ? rotationSecret.rotationId : selected.pendingRotation?.id) : undefined; // Each handler is independently optional, so "enabled" holds only when the // key's current state has an action the caller wired: start for an idle key, - // commit/abort for a pending one. Rendering the rest offers operations that - // can only fail locally. - const rotationEnabled = selectedRotationId + // commit/abort for a pending one. A revealed one-time secret counts on its + // own — hiding the section then would strand the only copy. Rendering the + // rest offers operations that can only fail locally. + const rotationEnabled = selectedHasRotationSecret || (selectedRotationId ? Boolean(onRotationCommit || onRotationAbort) - : Boolean(onRotationStart); + : Boolean(onRotationStart)); const mutationPending = deleting || renamePending || rotationPending; const runRotation = async (operation: "start" | "commit" | "abort") => { @@ -385,25 +387,31 @@ export default function ApiKeysWorkspace({

{t("api.rotation.secretOnce")}

{rotationSecret.key} - - + {onCopyRotationSecret && ( + + )} + {onDismissRotationSecret && ( + + )} )} -
- {onRotationCommit && ( - - )} - {onRotationAbort && ( - - )} -
+ {(onRotationCommit || onRotationAbort) && ( +
+ {onRotationCommit && ( + + )} + {onRotationAbort && ( + + )} +
+ )} ) : ( <> diff --git a/gui/tests/apikeys-actions.test.tsx b/gui/tests/apikeys-actions.test.tsx index 83ce62ecc98..0b5bd5fc0b6 100644 --- a/gui/tests/apikeys-actions.test.tsx +++ b/gui/tests/apikeys-actions.test.tsx @@ -27,6 +27,7 @@ const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_AC let previousGlobals: Record<(typeof globals)[number], unknown>; let testWindow: Window; let active: Root | null = null; +let rerender: (props: Partial) => Promise = async () => {}; beforeEach(() => { previousGlobals = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previousGlobals; @@ -44,6 +45,7 @@ afterEach(async () => { if (active) { const root = active; active = null; + rerender = async () => {}; await act(async () => { root.unmount(); }); } testWindow.close(); @@ -105,6 +107,13 @@ async function mount(props: Partial): Promise { + await act(async () => { + root.render(); + }); + }; await act(async () => { root.render(); }); return container; } @@ -307,6 +316,68 @@ test("an idle key offers rotation only when its start handler is wired", async ( expect(container.textContent).not.toContain("Start rotation"); }); +test("a start-only integration keeps the issued secret on screen", async () => { + const calls: string[] = []; + const container = await mount({ + onRotationStart: async id => { calls.push(`start:${id}`); return true; }, + }); + await openKey(container); + await act(async () => { button(container, "Start rotation").click(); await Promise.resolve(); }); + expect(calls).toEqual(["start:k1"]); + + // The hub's answer carries the one-time secret. Without finish handlers the + // pending-state guard alone would hide the section — stranding the only copy. + await rerender({ + rotationSecret: { id: "k1", key: "ocx_data_shown_once", rotationId: "rotation-1" }, + }); + expect(container.textContent).toContain("Key rotation"); + expect(container.textContent).toContain("ocx_data_shown_once"); + expect(container.textContent).not.toContain("Commit rotation"); + expect(container.textContent).not.toContain("Abort rotation"); +}); + +test("the secret's own controls are wired separately from the lifecycle actions", async () => { + const pendingKey = { + id: "k1", + name: "alpha", + prefix: "ocx_data_aaaaaaaa...", + createdAt: "2026-01-01T00:00:00.000Z", + pendingRotation: { + id: "rotation-1", + createdAt: "2026-08-28T00:00:00.000Z", + expiresAt: "2026-08-28T00:10:00.000Z", + }, + usage: { requests7d: 0, totalRequests: 0 }, + }; + const rotationSecret = { id: "k1", key: "ocx_data_shown_once", rotationId: "rotation-1" }; + + // A finish handler keeps the section up, but Copy and Close still check + // their own callbacks — an unwired reveal box is read-only. + const container = await mount({ + keys: [pendingKey], + rotationSecret, + onRotationCommit: async () => true, + }); + await openKey(container); + const reveal = container.querySelector(".api-key-reveal")!; + expect(reveal.textContent).toContain("ocx_data_shown_once"); + expect([...reveal.querySelectorAll("button")]).toHaveLength(0); + + await act(async () => { active?.unmount(); active = null; }); + + const wired = await mount({ + keys: [pendingKey], + rotationSecret, + onRotationCommit: async () => true, + onCopyRotationSecret: () => {}, + onDismissRotationSecret: () => {}, + }); + await openKey(wired); + const wiredReveal = wired.querySelector(".api-key-reveal")!; + const labels = [...wiredReveal.querySelectorAll("button")].map(b => b.textContent?.trim()); + expect(labels).toEqual(["Copy", "Close"]); +}); + test("a pending key renders only the rotation actions that have handlers", async () => { const pendingKey = { id: "k1", From e61d87a7eb622284e349f6b259075c8ad8df1c01 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 06:30:27 +0000 Subject: [PATCH 4/4] ci: retrigger after macos 2/2 runner stall Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>