diff --git a/clients/tui/__tests__/GatedForm.test.tsx b/clients/tui/__tests__/GatedForm.test.tsx
new file mode 100644
index 0000000000..9cc5fc1311
--- /dev/null
+++ b/clients/tui/__tests__/GatedForm.test.tsx
@@ -0,0 +1,111 @@
+import React from "react";
+import { describe, it, expect, vi } from "vitest";
+import type { FormStructure } from "ink-form";
+import { render } from "./helpers/renderTui";
+import { GatedForm, initialFormValues } from "../src/components/GatedForm.js";
+
+// The real ink-form, not the double the modal suites use: what is under test
+// is that its own key handlers and focusables go away while the form is gated,
+// which the double (one Enter handler) cannot show (#2967).
+
+const tick = async () => {
+ for (let i = 0; i < 8; i++)
+ await new Promise((resolve) => setTimeout(resolve, 4));
+};
+const ESC = String.fromCharCode(27);
+const DOWN = `${ESC}[B`;
+
+const FORM: FormStructure = {
+ sections: [
+ {
+ title: "Args",
+ fields: [
+ { type: "string", name: "city", initialValue: "Oslo" },
+ { type: "string", name: "note" },
+ ],
+ },
+ ],
+};
+
+describe("initialFormValues", () => {
+ it("collects each field's initialValue across sections, skipping undefined", () => {
+ expect(
+ initialFormValues({
+ sections: [
+ FORM.sections[0]!,
+ {
+ title: "More",
+ fields: [{ type: "boolean", name: "flag", initialValue: false }],
+ },
+ ],
+ }),
+ ).toEqual({ city: "Oslo", flag: false });
+ });
+
+ it("keeps a field named __proto__ as an own value", () => {
+ const values = initialFormValues({
+ sections: [
+ {
+ title: "Args",
+ fields: [{ type: "string", name: "__proto__", initialValue: "x" }],
+ },
+ ],
+ });
+ expect(Object.prototype.hasOwnProperty.call(values, "__proto__")).toBe(
+ true,
+ );
+ expect(JSON.parse(JSON.stringify(values))).toEqual(
+ JSON.parse('{"__proto__":"x"}'),
+ );
+ });
+});
+
+describe("GatedForm", () => {
+ it("submits each field's default, as the uncontrolled form did", async () => {
+ const onSubmit = vi.fn();
+ const api = render(