diff --git a/ide/src/connectionEditorWebview.ts b/ide/src/connectionEditorWebview.ts index d83cdca26c..235d9c9393 100644 --- a/ide/src/connectionEditorWebview.ts +++ b/ide/src/connectionEditorWebview.ts @@ -296,9 +296,12 @@ export function connectionEditorScript(token: string, p: ConnectionEditorScriptI // One entry per message its hosts post: at least connectionEditor.ts and configEditors.ts use // this page. Both post "error"; only connectionEditor.ts answers with "fields". A field is a // FieldDescriptor from connectionForm.ts: its declared fields with their types. The two - // unknown-typed values (value, defaultValue) and the carried-through envDefault and - // preserveValue are not typed there either. choices is required and may be null; envKey, cast - // and a group's description may be absent and are never null. + // unknown-typed values (value, defaultValue, envDefault, preserveValue) are not typed there + // either, and their presence is not checked: all four are optional there, and FieldDescriptor + // says why value and defaultValue can be absent, so a presence check would discard ordinary + // "fields" messages. This page does not read envDefault or preserveValue at all. choices is + // required and may be null; envKey, cast and a group's description may be absent and are never + // null. // control and type are closed sets: the lists FieldControl and ParamType are derived from. const FIELD_CONTROLS = ${embedJson(FIELD_CONTROLS)}; const PARAM_TYPES = ${embedJson(PARAM_TYPES)}; diff --git a/ide/src/connectionForm.ts b/ide/src/connectionForm.ts index 613ec667a8..a2da84edd4 100644 --- a/ide/src/connectionForm.ts +++ b/ide/src/connectionForm.ts @@ -152,10 +152,19 @@ export interface FieldDescriptor { choices: unknown[] | null; /** The default, as display text for a hint. "" when the setting has no default. */ placeholder: string; - /** The default, raw (a checkbox needs its unset position). RENDER-ONLY — never post it. */ - defaultValue: unknown; - /** The record's literal value, or undefined when the record does not set this key. */ - value: unknown; + /** + * The default, raw (a checkbox needs its unset position). RENDER-ONLY — never post it. Optional + * because it can be undefined (at least for a key the schema does not describe, and for a schema + * param that carries no `default`), and posting the descriptor to the webview serializes it as + * JSON, which drops an undefined member: the page can receive a field with no `defaultValue`. + */ + defaultValue?: unknown; + /** + * The record's literal value. Undefined at least when the record does not set this key, when the + * key is a secret, and when it holds an env reference -- so undefined does not mean "unset". Optional + * for the same reason as `defaultValue`: an undefined value is absent from what the webview receives. + */ + value?: unknown; isEnvRef: boolean; /** The referenced environment key, when `isEnvRef`. */ envKey?: string; diff --git a/ide/src/test/suite/webview-receivers.test.ts b/ide/src/test/suite/webview-receivers.test.ts index 2baa00927d..7e4b5b4895 100644 --- a/ide/src/test/suite/webview-receivers.test.ts +++ b/ide/src/test/suite/webview-receivers.test.ts @@ -376,12 +376,22 @@ const SCHEMA: ConnectionSchema = { const FIELDS_OK = { command: "fields", groups: buildForm(SCHEMA, "mllp", "outbound", { - host: { env: "PEER_HOST", cast: "str" }, + host: { env: "PEER_HOST", cast: "str", default: "localhost" }, port: 6661, + // An inline secret literal, so one field carries preserveValue. + tls_key_password: "x", legacy_key: "kept", }), }; +/** FIELDS_OK with no field carrying `value` or `defaultValue`: both are optional (FieldDescriptor). */ +const FIELDS_BARE = variant(FIELDS_OK, (c) => { + for (const f of c.groups.flatMap((g: Payload) => g.fields)) { + delete f.value; + delete f.defaultValue; + } +}); + const connection: Receiver = { panel: "connection editor", key: "command", @@ -407,7 +417,7 @@ const connection: Receiver = {
${SENTINEL}
`, ), - wellFormed: { error: [ERROR_OK], fields: [FIELDS_OK] }, + wellFormed: { error: [ERROR_OK], fields: [FIELDS_OK, FIELDS_BARE] }, malformed: { error: badMessages(ERROR_OK), fields: [ @@ -814,6 +824,11 @@ suite("webview receivers discard a malformed payload and render a well-formed on const fields = FIELDS_OK.groups.flatMap((g) => g.fields); assert.ok(fields.some((f) => typeof f.envKey === "string" && typeof f.cast === "string")); assert.ok(fields.some((f) => !("envKey" in f) && !("cast" in f))); + const bare = FIELDS_BARE.groups.flatMap((g: Payload) => g.fields); + assert.ok(fields.some((f) => f.value !== undefined) && fields.some((f) => f.defaultValue !== undefined)); + assert.ok(fields.some((f) => "envDefault" in f) && fields.some((f) => !("envDefault" in f))); + assert.ok(fields.some((f) => "preserveValue" in f) && fields.some((f) => !("preserveValue" in f))); + assert.ok(bare.length > 0 && bare.every((f: Payload) => !("value" in f) && !("defaultValue" in f))); assert.ok(FIELDS_OK.groups.some((g) => typeof g.description === "string"), "no group has a description"); assert.ok(FIELDS_OK.groups.some((g) => !("description" in g))); assert.ok(fields.some((f) => f.choices === null) && fields.some((f) => Array.isArray(f.choices))); diff --git a/messagefoundry/__main__.py b/messagefoundry/__main__.py index 9e9ba60159..d77a5db8bb 100644 --- a/messagefoundry/__main__.py +++ b/messagefoundry/__main__.py @@ -206,9 +206,10 @@ def _refuse_toolkit_command(command: str, argv: Sequence[str]) -> int: ``_emit_error`` writes JSON to stdout or one line to stderr, never both, and its own return value is 1, so it is not returned. ``argv_wants_json`` is the one pre-parse JSON rule both commands use. - The line names no install command. The toolkit distribution is not yet on PyPI, and - tests/test_install_instruction_provenance.py refuses shipped text that tells a user to install - an unclaimed name. + The line names no install command. It was written while the toolkit name was unclaimed on PyPI, + when tests/test_install_instruction_provenance.py refused shipped text telling a user to + install it by name. ADR 0201 slice 3 recorded the name as claimed; the docs that give a + bare-name install arrive with slice 4. """ _emit_error( f"`{command}` is not a messagefoundry command. It is an authoring command: run it as " diff --git a/tests/test_install_instruction_provenance.py b/tests/test_install_instruction_provenance.py index 1f9074cf24..636e11cc62 100644 --- a/tests/test_install_instruction_provenance.py +++ b/tests/test_install_instruction_provenance.py @@ -41,27 +41,36 @@ #: Distributions this repository builds that are **not yet published to any index**. Until a name is #: claimed, no shipped text may tell a user to install it by bare name. #: -#: **`messagefoundry-toolkit` is here from ADR 0201 slice 2 until its first upload claims the name.** -#: The other three distributions this repo builds are registered on PyPI (`messagefoundry`, -#: `messagefoundry-harness`, `messagefoundry-webconsole` -- the last claimed 2026-07-29 by the first -#: `webconsole-v*` release, closing the ASVS 15.2.4 exposure where README.md instructed an index -#: install of an unclaimed name). ADR 0201 slice 3 moves the toolkit to `_PUBLISHED_DISTRIBUTIONS` -#: after a probe of the first tag's JSON API URL, with the harness as a control, shows the upload -#: landed. An empty set is the goal state, not a disabled guard. +#: Every distribution this repo builds is registered on PyPI, so the set is empty. An empty set +#: is the goal state, not a disabled guard. `messagefoundry-webconsole` was claimed 2026-07-29 by the +#: first `webconsole-v*` release, closing the ASVS 15.2.4 exposure where README.md instructed an index +#: install of an unclaimed name. #: -#: This matches an install VERB only, so it cannot see text that names the toolkit without one. ADR -#: 0201 section 1 does not rely on it to close the window: the release job uploads the toolkit before -#: the engine, so no engine naming the toolkit reaches PyPI while the name is unclaimed. +#: This matches an install VERB only, so it cannot see text that names a listed distribution without +#: one, such as a `Requires-Dist` line or prose saying a package depends on it. #: #: The set exists for the NEXT distribution: add its name here the moment `packaging//` lands and #: before any doc references it, then remove it once the first release claims the name. #: `test_a_new_distribution_must_be_classified` fails if a packaged distribution is neither listed here #: nor demonstrably published, so this cannot silently rot back to vacuous. -_UNPUBLISHED_DISTRIBUTIONS: frozenset[str] = frozenset({"messagefoundry-toolkit"}) +_UNPUBLISHED_DISTRIBUTIONS: frozenset[str] = frozenset() #: Distribution names registered on PyPI and published by this repo's release workflow. Listing one #: here asserts the name is CLAIMED -- the property that makes a bare-name install instruction safe. -_PUBLISHED_DISTRIBUTIONS = frozenset({"messagefoundry-harness", "messagefoundry-webconsole"}) +#: +#: **`messagefoundry-toolkit` moved here in ADR 0201 slice 3**, from `_UNPUBLISHED_DISTRIBUTIONS`, +#: where slice 2 put it. The probe, read 2026-10-27: PyPI's JSON API returned 200 for +#: `/pypi/messagefoundry-toolkit/json`, `/pypi/messagefoundry-toolkit/0.5.0/json` and +#: `/pypi/messagefoundry-toolkit/0.5.1/json`, and for the harness control +#: `/pypi/messagefoundry-harness/0.5.1/json`; it returned 404 for the controls +#: `/pypi/messagefoundry-toolkit/0.0.0.dev999/json` and an unregistered project name. The project +#: lists releases 0.5.0, 0.5.1 and 0.5.2, and each wheel's provenance names publisher +#: `MEFORORG/MessageFoundry`, workflow `release.yml`. So every release on the project is this repo's +#: upload. That is a reading on that date, not something this test re-checks: no test here reaches +#: the network. +_PUBLISHED_DISTRIBUTIONS = frozenset( + {"messagefoundry-harness", "messagefoundry-toolkit", "messagefoundry-webconsole"} +) #: Files whose install commands are shipped to, or executed by, someone other than a maintainer. #: CI workflows and internal handoffs are excluded deliberately: they install from the source tree by