Parcourir la source

fix(ui-models): contain the card's credential probe rejection

The review named this call site with the other two, and the previous pass
missed it: the editor card's mount-time `credentials.describe` had only a
fulfillment handler, so a transport failure reached the browser as an
unhandled rejection.

The probe is a placeholder hint ("already configured"), never a precondition
for editing, so it now renders without the hint rather than failing. Covered
by a test that fails without the handler.
Yichen Jiang il y a 2 mois
Parent
commit
4395268cc1

+ 11 - 4
packages/client/ui-models/src/client/ProviderEditor.tsx

@@ -141,10 +141,17 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
   useEffect(() => {
   useEffect(() => {
     let stale = false
     let stale = false
     setKeyState(undefined)
     setKeyState(undefined)
-    void api.credentials.describe({ refs: [keyRef] }).then((response) => {
-      if (stale || !response.result.ok) return
-      setKeyState(response.result.value.credentials[keyRef])
-    })
+    // The key state is a placeholder hint, not a precondition for editing:
+    // neither a business rejection nor a transport failure may reach the
+    // browser as an unhandled rejection, so the card simply renders without
+    // the "already configured" hint.
+    void api.credentials.describe({ refs: [keyRef] }).then(
+      (response) => {
+        if (stale || !response.result.ok) return
+        setKeyState(response.result.value.credentials[keyRef])
+      },
+      () => undefined,
+    )
     return () => { stale = true }
     return () => { stale = true }
   }, [api.credentials, keyRef])
   }, [api.credentials, keyRef])
 
 

+ 25 - 0
packages/client/ui-models/tests/components.spec.tsx

@@ -371,6 +371,31 @@ describe('ModelsSection', () => {
     expect(set).not.toHaveBeenCalled()
     expect(set).not.toHaveBeenCalled()
   })
   })
 
 
+  it('renders the card without the stored-key hint when the credential probe rejects', async () => {
+    // The probe is a placeholder hint, not a precondition: an escaping
+    // rejection would surface in the browser as an unhandled rejection.
+    const { face } = scriptedFace()
+    face.credentials.describe = vi.fn(() => Promise.reject(new Error('connection lost')))
+    const unhandled = vi.fn()
+    process.on('unhandledRejection', unhandled)
+    try {
+      const controller = new ModelsSettingsStore(face as unknown as WireFace)
+      await controller.load()
+      render(<ModelsSection
+        controller={controller}
+        useSnapshot={bindSnapshotSelector(controller.store)}
+        api={face as never}
+        t={t}
+      />)
+      const key = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
+      expect(key.placeholder).toBe(en.keyPlaceholder)
+      await new Promise(resolve => setTimeout(resolve, 10))
+      expect(unhandled).not.toHaveBeenCalled()
+    } finally {
+      process.off('unhandledRejection', unhandled)
+    }
+  })
+
   it('tells the user to reopen when another writer moved the namespace first', async () => {
   it('tells the user to reopen when another writer moved the namespace first', async () => {
     // The stale-draft overwrite: two tabs open the same card, the other saves,
     // The stale-draft overwrite: two tabs open the same card, the other saves,
     // and this one must be refused rather than replay its opening snapshot.
     // and this one must be refused rather than replay its opening snapshot.