From fbf1da74522e650c24a12727899831e750e96037 Mon Sep 17 00:00:00 2001 From: NotExpectedYet Date: Sat, 26 Sep 2026 07:43:31 +0100 Subject: [PATCH] fix: register the settings card instead of throwing while building it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card never appeared, and nothing said why: the plugin's configuration section was missing from its card in the Plugins panel while a sibling plugin's showed, on a deployment where the Host demonstrably served both namespaces (`settings/describe` over the app's own RPC answered `dsh-recall fields=32 secrets=[embeddingApiKey set]`) and the browser was demonstrably serving this bundle. The cause was construction order. `SettingsFormModel.bind()` is not a subscription setter — it builds its snapshot store from one immediate `project()` call — so the projection runs inside the constructor, and the projection reads the describe mirror to report whether the API key is set. `this.describe` was assigned *after* `bind`, so that first projection read `undefined.getSnapshot()`: TypeError: Cannot read properties of undefined (reading 'getSnapshot') at RecallSettingsController.secretIsSet (client/client.js:428) at RecallSettingsController.projection (client/client.js:413) It was thrown from inside the `ctx.inject(['configForms'], …)` callback while the card was being registered, so the registration was abandoned: no ledger entry, no section on the card, and no error in the UI — the one failure mode this file's own header set out to prevent. Everything the projection reads is now assigned before anything projects. The sibling plugin was never affected because it reads the same face lazily from a field it assigns first. The test stub's `bind()` only *stored* the projection, which is why the suite was green through all of this. It now projects once, as the framework does — and with the old ordering every mounting test fails with that TypeError, so this cannot regress silently again. --- client/client.js | 12 ++++++++++-- test/client.test.js | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/client/client.js b/client/client.js index 32e0a2c..5a4f39d 100644 --- a/client/client.js +++ b/client/client.js @@ -385,6 +385,16 @@ window.__ModuleLoader__.load({ */ constructor(ctx) { this.ctx = ctx + // Every field the projection reads must exist BEFORE anything projects. + // + // `SettingsFormModel.bind()` is not a subscription setter: it builds the + // snapshot store from one immediate `project()` call, so the projection + // runs once, right here, inside this constructor. Assigning `describe` + // after `bind` made that first projection read `undefined.getSnapshot()` + // — a TypeError thrown while the inject callback was registering the + // card, which aborted the registration and left the plugin with no + // settings section at all and nothing in the UI to say so. + this.describe = ctx.configForms.describe() this.form = ctx.configForms.get(SETTINGS_ENTRY) this.model = new SettingsFormModel( this.form, @@ -399,8 +409,6 @@ window.__ModuleLoader__.load({ })), ) this.store = this.model.bind(() => this.projection()) - /** The describe mirror that knows whether a secret is configured. */ - this.describe = ctx.configForms.describe() } /** Everything the page renders, rebuilt whenever the form or a draft moves. */ diff --git a/test/client.test.js b/test/client.test.js index 63ea60d..55b3f01 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -109,6 +109,11 @@ function makePrimitives(record) { bind(project) { this.project = project + // Faithful to the framework: `bind` is not a subscription setter, it + // builds the snapshot store from ONE immediate projection. A stub that + // only stored the projection would let a controller read a field it has + // not assigned yet — which is exactly the bug this reproduces. + project() return { getSnapshot: () => project(), subscribe: () => () => {} } }