From 75f38071e415fc988a6963ac99ba7477c49f43d8 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 8 Sep 2026 08:17:49 +0100 Subject: [PATCH 1/4] fix(kits): restore extension select params Replace boolean declarations with labeled string selects so extension configuration values remain valid during kit migrations. --- kits/delete-user-data/README.md | 9 +- kits/delete-user-data/src/config.ts | 8 +- kits/delete-user-data/tests/config.test.ts | 41 +++++- kits/firestore-bigquery-export/README.md | 11 +- kits/firestore-bigquery-export/src/config.ts | 29 ++-- .../tests/config-parity.test.ts | 129 ++++++++++++++++++ kits/firestore-genai-chatbot/README.md | 17 +-- kits/firestore-genai-chatbot/src/config.ts | 15 +- .../tests/config-parity.test.ts | 111 +++++++++++++++ kits/firestore-send-email/README.md | 3 +- kits/firestore-send-email/src/config.ts | 8 +- .../tests/config-parity.test.ts | 85 ++++++++++++ kits/firestore-vector-search/src/config.ts | 11 +- .../tests/config-parity.test.ts | 102 ++++++++++++++ kits/speech-to-text/src/config.ts | 10 +- .../tests/config-parity.test.ts | 84 ++++++++++++ kits/storage-resize-images/src/config.ts | 23 ++-- .../tests/config.test.ts | 40 ++++++ 18 files changed, 647 insertions(+), 89 deletions(-) create mode 100644 kits/firestore-bigquery-export/tests/config-parity.test.ts create mode 100644 kits/firestore-genai-chatbot/tests/config-parity.test.ts create mode 100644 kits/firestore-send-email/tests/config-parity.test.ts create mode 100644 kits/firestore-vector-search/tests/config-parity.test.ts create mode 100644 kits/speech-to-text/tests/config-parity.test.ts diff --git a/kits/delete-user-data/README.md b/kits/delete-user-data/README.md index c920771b0e..733ae69452 100644 --- a/kits/delete-user-data/README.md +++ b/kits/delete-user-data/README.md @@ -102,7 +102,7 @@ overridden there. | `rtdbPaths` | `RTDB_PATHS` | no | (empty) | Comma-separated RTDB paths with `{UID}` | | `storageBucket` | `CLOUD_STORAGE_BUCKET` | no | default Storage bucket | Bucket to clear | | `storagePaths` | `STORAGE_PATHS` | no | (empty) | Comma-separated Storage paths with `{UID}` | -| `enableAutoDiscovery` | `ENABLE_AUTO_DISCOVERY` | no | `false` | Auto-discover user-linked docs | +| `enableAutoDiscovery` | `ENABLE_AUTO_DISCOVERY` | no | `no` | Auto-discover user-linked docs (`yes` or `no`) | | `searchDepth` | `AUTO_DISCOVERY_SEARCH_DEPTH` | no | `3` | Discovery depth | | `searchFields` | `AUTO_DISCOVERY_SEARCH_FIELDS` | no | `id,uid,userId` | Fields treated as user ids | | `searchFunction` | `SEARCH_FUNCTION` | no | (empty) | Optional custom search function | @@ -147,13 +147,6 @@ This kit is the extension repackaged as an npm package, but a few things behave differently. If you are moving from an installed extension instance, read this section before you deploy. -### Auto-discovery uses `true` / `false` - -`ENABLE_AUTO_DISCOVERY` is a boolean param, and only the literal string `true` -enables it. The extension used `yes` / `no`, so copying an old config across -leaves auto-discovery silently switched off. Change `yes` to `true` in your -`.env`. - ### The instance id comes from `firebase.json` The extension derived an instance id at install time and used it to name the diff --git a/kits/delete-user-data/src/config.ts b/kits/delete-user-data/src/config.ts index c32000dde2..d1e023bdb5 100644 --- a/kits/delete-user-data/src/config.ts +++ b/kits/delete-user-data/src/config.ts @@ -15,7 +15,6 @@ */ import { - defineBoolean, defineInt, defineString, type IntParam, @@ -116,12 +115,13 @@ const params = { }, }, }), - enableAutoDiscovery: defineBoolean("ENABLE_AUTO_DISCOVERY", { + enableAutoDiscovery: defineString("ENABLE_AUTO_DISCOVERY", { label: "Enable auto discovery", description: "Enable the extension to automatically discover Firestore collections and documents to delete.", - default: false, + default: "no", + input: select({ Yes: "yes", No: "no" }), }), searchDepth: defineInt("AUTO_DISCOVERY_SEARCH_DEPTH", { label: "Auto discovery search depth", @@ -197,7 +197,7 @@ export function configFromEnv(): DeleteUserDataConfig { storageBucket: optional(params.storageBucket.value()) ?? process.env.STORAGE_BUCKET, storagePaths: optional(params.storagePaths.value()), - enableAutoDiscovery: params.enableAutoDiscovery.value(), + enableAutoDiscovery: params.enableAutoDiscovery.value() === "yes", searchDepth: optionalInt(params.searchDepth), searchFields: params.searchFields.value(), searchFunction: optional(params.searchFunction.value()), diff --git a/kits/delete-user-data/tests/config.test.ts b/kits/delete-user-data/tests/config.test.ts index 5dbd6a1ff5..a41c7b72ab 100644 --- a/kits/delete-user-data/tests/config.test.ts +++ b/kits/delete-user-data/tests/config.test.ts @@ -33,6 +33,9 @@ class FakeStringParam extends FakeExpression { } value(): string { + if (process.env[this.name] !== undefined) { + return process.env[this.name]; + } if (this.defaultValue instanceof FakeStringParam) { return this.defaultValue.value(); } @@ -54,8 +57,13 @@ const defineInt = vi.fn((name: string, opts?: { default?: number }) => ({ value: () => opts?.default ?? 0, })); -const defineBoolean = vi.fn((_name: string, opts?: { default?: boolean }) => ({ - value: () => opts?.default ?? false, +const select = vi.fn((options: Record) => ({ + select: { + options: Object.entries(options).map(([label, value]) => ({ + label, + value, + })), + }, })); function cel(value: unknown): string { @@ -64,11 +72,10 @@ function cel(value: unknown): string { vi.mock("firebase-functions/params", () => ({ Expression: FakeExpression, - defineBoolean, defineInt, defineString, projectID: { value: () => "demo-test" }, - select: vi.fn((options: string[]) => ({ options })), + select, storageBucket: new FakeStringParam("STORAGE_BUCKET", "demo-test.appspot.com"), })); @@ -76,7 +83,7 @@ async function importConfig() { vi.resetModules(); defineString.mockClear(); defineInt.mockClear(); - defineBoolean.mockClear(); + select.mockClear(); vi.stubEnv("FIREBASE_KIT_INSTANCE_ID", "test-instance"); return import("../src/config"); @@ -112,6 +119,16 @@ describe("configFromEnv", () => { expect(config.searchDepth).toBeUndefined(); }); + test("parses the predecessor's yes/no values", async () => { + const { configFromEnv } = await importConfig(); + + vi.stubEnv("ENABLE_AUTO_DISCOVERY", "yes"); + expect(configFromEnv().enableAutoDiscovery).toBe(true); + + vi.stubEnv("ENABLE_AUTO_DISCOVERY", "no"); + expect(configFromEnv().enableAutoDiscovery).toBe(false); + }); + test("declares the params the extension exposes", async () => { await importConfig(); @@ -136,9 +153,19 @@ describe("configFromEnv", () => { "AUTO_DISCOVERY_SEARCH_DEPTH", expect.objectContaining({ default: 3 }), ]); - expect(defineBoolean.mock.calls).toContainEqual([ + expect(defineString.mock.calls).toContainEqual([ "ENABLE_AUTO_DISCOVERY", - expect.objectContaining({ default: false }), + expect.objectContaining({ + default: "no", + input: { + select: { + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + }, + }), ]); }); diff --git a/kits/firestore-bigquery-export/README.md b/kits/firestore-bigquery-export/README.md index 8ffb4e27d2..3b9d33664e 100644 --- a/kits/firestore-bigquery-export/README.md +++ b/kits/firestore-bigquery-export/README.md @@ -120,8 +120,8 @@ loads them at deploy time and prompts for any required values that are missing. | `timePartitioningFirestoreField` | `TIME_PARTITIONING_FIRESTORE_FIELD` | no | (empty) | Firestore field for partitioning | | `clustering` | `CLUSTERING` | no | (empty) | Clustering columns (max 4) | | `wildcardIds` | `WILDCARD_IDS` | no | `false` | Store path-param values as columns | -| `useNewSnapshotQuerySyntax` | `USE_NEW_SNAPSHOT_QUERY_SYNTAX` | no | `false` | Use newer snapshot query syntax | -| `excludeOldData` | `EXCLUDE_OLD_DATA` | no | `false` | Skip previous document state on updates | +| `useNewSnapshotQuerySyntax` | `USE_NEW_SNAPSHOT_QUERY_SYNTAX` | no | `no` | Use newer snapshot query syntax (`yes` or `no`) | +| `excludeOldData` | `EXCLUDE_OLD_DATA` | no | `no` | Skip previous document state on updates (`yes` or `no`) | | `viewType` | `VIEW_TYPE` | no | `view` | `view`, `materialized_incremental`, `materialized_non_incremental` | | `maxStaleness` | `MAX_STALENESS` | no | (empty) | Materialized view max staleness | | `refreshIntervalMinutes` | `REFRESH_INTERVAL_MINUTES` | no | (empty) | Materialized view refresh interval | @@ -227,13 +227,6 @@ This kit is the extension repackaged as an npm package, but a few things behave differently. If you are moving from an installed extension instance, read this section before you deploy. -### Boolean settings use `true` / `false` - -`WILDCARD_IDS`, `USE_NEW_SNAPSHOT_QUERY_SYNTAX` and `EXCLUDE_OLD_DATA` are -boolean params, and only the literal string `true` enables them. The extension -used `yes` / `no` for the last two, so copying an old config across leaves them -silently disabled. Change any `yes` to `true` in your `.env`. - ### Failed writes retry differently The extension pushed a failed BigQuery write onto a Cloud Tasks queue diff --git a/kits/firestore-bigquery-export/src/config.ts b/kits/firestore-bigquery-export/src/config.ts index 08e4b1d23a..81aa7b916c 100644 --- a/kits/firestore-bigquery-export/src/config.ts +++ b/kits/firestore-bigquery-export/src/config.ts @@ -21,12 +21,7 @@ import type { } from "@firebaseextensions/firestore-bigquery-change-tracker"; import { LogLevel } from "@firebaseextensions/firestore-bigquery-change-tracker"; import type { Expression } from "firebase-functions/params"; -import { - defineBoolean, - defineString, - projectID, - select, -} from "firebase-functions/params"; +import { defineString, projectID, select } from "firebase-functions/params"; import type { ExportConfig, ViewType } from "./export-config"; type TrackerLogLevel = "debug" | "info" | "warn" | "error" | "silent"; @@ -358,26 +353,29 @@ const params = { }, }, }), - wildcardIds: defineBoolean("WILDCARD_IDS", { + wildcardIds: defineString("WILDCARD_IDS", { label: "Enable Wildcard Column field with Parent Firestore Document IDs", description: "If enabled, creates a column containing a JSON object of all wildcard ids from a documents path.", - default: false, + default: "false", + input: select({ No: "false", Yes: "true" }), }), - useNewSnapshotQuerySyntax: defineBoolean("USE_NEW_SNAPSHOT_QUERY_SYNTAX", { + useNewSnapshotQuerySyntax: defineString("USE_NEW_SNAPSHOT_QUERY_SYNTAX", { label: "Use new query syntax for snapshots", description: "If enabled, snapshots will be generated with the new query syntax, which should be more performant, and avoid potential resource limitations.", - default: false, + default: "no", + input: select({ Yes: "yes", No: "no" }), }), - excludeOldData: defineBoolean("EXCLUDE_OLD_DATA", { + excludeOldData: defineString("EXCLUDE_OLD_DATA", { label: "Exclude old data payloads", description: "If enabled, table rows will never contain old data (document snapshot before the Firestore onDocumentUpdate event: `change.before.data()`). The reduction in data should be more performant, and avoid potential resource limitations.", - default: false, + default: "no", + input: select({ Yes: "yes", No: "no" }), }), viewType: defineString("VIEW_TYPE", { label: "View Type", @@ -626,9 +624,10 @@ export function configFromEnv(): ExportConfig { bqProjectId: optional(params.bigqueryProjectId.value()), projectId: projectID.value(), databaseId: optional(params.database.value()) || "(default)", - wildcardIds: params.wildcardIds.value(), - excludeOldData: params.excludeOldData.value(), - useNewSnapshotQuerySyntax: params.useNewSnapshotQuerySyntax.value(), + wildcardIds: params.wildcardIds.value() === "true", + excludeOldData: params.excludeOldData.value() === "yes", + useNewSnapshotQuerySyntax: + params.useNewSnapshotQuerySyntax.value() === "yes", viewType: (optional(params.viewType.value()) || "view") as ViewType, partitioning: buildPartitioningConfig({ timePartitioning: timePartitioning(tablePartitioning), diff --git a/kits/firestore-bigquery-export/tests/config-parity.test.ts b/kits/firestore-bigquery-export/tests/config-parity.test.ts new file mode 100644 index 0000000000..82beb7e6f2 --- /dev/null +++ b/kits/firestore-bigquery-export/tests/config-parity.test.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Compatibility requirement, not aspiration: the extension declared these + * settings as two-option selects, so a deployed `.env` carries the extension's + * literal option values. `config.test.ts` fakes `firebase-functions/params`, + * so these cases run against the real params to pin the accepted values. + */ + +import { declaredParams } from "firebase-functions/params"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { configFromEnv } from "../src/config"; + +function declaration(name: string) { + const param = declaredParams.find((candidate) => candidate.name === name); + if (!param || !("options" in param)) { + throw new Error(`Missing declaration for ${name}`); + } + const options = param.options as { default?: unknown; input?: unknown }; + return { + type: (param.constructor as unknown as { type: string }).type, + default: options.default, + input: options.input, + }; +} + +const KEYS = [ + "WILDCARD_IDS", + "USE_NEW_SNAPSHOT_QUERY_SYNTAX", + "EXCLUDE_OLD_DATA", +] as const; + +describe("select values inherited from the extension", () => { + const saved = new Map(); + + test("declares the predecessor's labeled string selects", () => { + expect(declaration("WILDCARD_IDS")).toEqual({ + type: "string", + default: "false", + input: { + select: { + options: [ + { label: "No", value: "false" }, + { label: "Yes", value: "true" }, + ], + }, + }, + }); + for (const name of ["USE_NEW_SNAPSHOT_QUERY_SYNTAX", "EXCLUDE_OLD_DATA"]) { + expect(declaration(name)).toEqual({ + type: "string", + default: "no", + input: { + select: { + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + }, + }); + } + }); + + beforeEach(() => { + for (const key of KEYS) { + saved.set(key, process.env[key]); + delete process.env[key]; + } + }); + + afterEach(() => { + for (const [key, value] of saved) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + saved.clear(); + }); + + // WILDCARD_IDS was a true/false select; the other two were yes/no. + test("reads WILDCARD_IDS as true/false", () => { + process.env.WILDCARD_IDS = "true"; + expect(configFromEnv().wildcardIds).toBe(true); + + process.env.WILDCARD_IDS = "false"; + expect(configFromEnv().wildcardIds).toBe(false); + }); + + test("reads USE_NEW_SNAPSHOT_QUERY_SYNTAX as yes/no", () => { + process.env.USE_NEW_SNAPSHOT_QUERY_SYNTAX = "yes"; + expect(configFromEnv().useNewSnapshotQuerySyntax).toBe(true); + + process.env.USE_NEW_SNAPSHOT_QUERY_SYNTAX = "no"; + expect(configFromEnv().useNewSnapshotQuerySyntax).toBe(false); + }); + + test("reads EXCLUDE_OLD_DATA as yes/no", () => { + process.env.EXCLUDE_OLD_DATA = "yes"; + expect(configFromEnv().excludeOldData).toBe(true); + + process.env.EXCLUDE_OLD_DATA = "no"; + expect(configFromEnv().excludeOldData).toBe(false); + }); + + test("treats an unset variable as off, as the extension did", () => { + const config = configFromEnv(); + + expect(config.wildcardIds).toBe(false); + expect(config.useNewSnapshotQuerySyntax).toBe(false); + expect(config.excludeOldData).toBe(false); + }); +}); diff --git a/kits/firestore-genai-chatbot/README.md b/kits/firestore-genai-chatbot/README.md index a6bdc1699c..c6e267f5d4 100644 --- a/kits/firestore-genai-chatbot/README.md +++ b/kits/firestore-genai-chatbot/README.md @@ -103,8 +103,8 @@ the CLI connects them to the function at deploy time. | `topK` | `TOP_K` | no | (empty) | Top-k | | `candidateCount` | `CANDIDATE_COUNT` | no | `1` | Candidate count | | `maxOutputTokens` | `MAX_OUTPUT_TOKENS` | no | (empty) | Max output tokens | -| `enableOverrides` | `ENABLE_DISCUSSION_OPTION_OVERRIDES` | no | `false` | Per-discussion option overrides | -| `enableGenkitMonitoring` | `ENABLE_GENKIT_MONITORING` | no | `false` | Enable Genkit monitoring | +| `enableOverrides` | `ENABLE_DISCUSSION_OPTION_OVERRIDES` | no | `no` | Per-discussion option overrides (`yes` or `no`) | +| `enableGenkitMonitoring` | `ENABLE_GENKIT_MONITORING` | no | `no` | Enable Genkit monitoring (`yes` or `no`) | | `harmHateSpeech` | `HARM_CATEGORY_HATE_SPEECH` | no | `HARM_BLOCK_THRESHOLD_UNSPECIFIED` | Harm threshold | | `harmDangerous` | `HARM_CATEGORY_DANGEROUS_CONTENT` | no | `HARM_BLOCK_THRESHOLD_UNSPECIFIED` | Harm threshold | | `harmHarassment` | `HARM_CATEGORY_HARASSMENT` | no | `HARM_BLOCK_THRESHOLD_UNSPECIFIED` | Harm threshold | @@ -141,16 +141,9 @@ to your own functions codebase. The generation logic, the Firestore trigger, the `status` state machine, the per-discussion overrides and the safety settings are all ported verbatim. Config keeps the same environment variable names, so a `.env` copied from your installed instance is close to a lift-and-shift, with -four exceptions below: the boolean toggles, the two region settings, and the API -key secret. - -### Change `yes` and `no` to `true` and `false` - -`ENABLE_DISCUSSION_OPTION_OVERRIDES` and `ENABLE_GENKIT_MONITORING` were -`yes`/`no` dropdowns. They are now booleans that count as enabled only for the -exact value `true`. A copied `.env` carrying `yes` deploys without complaint and -silently leaves the feature off, so per-discussion overrides stop being read and -Genkit monitoring stops reporting. +the exceptions below: the two region settings and the API key secret. +`ENABLE_DISCUSSION_OPTION_OVERRIDES` and `ENABLE_GENKIT_MONITORING` keep the +extension's `yes` / `no` values. ### Pick your Cloud Functions region, or you get us-central1 diff --git a/kits/firestore-genai-chatbot/src/config.ts b/kits/firestore-genai-chatbot/src/config.ts index 70cef15b05..af629c0f86 100644 --- a/kits/firestore-genai-chatbot/src/config.ts +++ b/kits/firestore-genai-chatbot/src/config.ts @@ -15,7 +15,6 @@ */ import { - defineBoolean, defineSecret, defineString, expr, @@ -243,19 +242,21 @@ const params = { default: "", input: POSITIVE_INT_VALIDATION, }), - enableOverrides: defineBoolean("ENABLE_DISCUSSION_OPTION_OVERRIDES", { + enableOverrides: defineString("ENABLE_DISCUSSION_OPTION_OVERRIDES", { label: "Enable per document overrides.", description: 'If set to "Yes", discussion parameters may be overwritten by fields in the discussion collection.', - default: false, + default: "no", + input: select({ Yes: "yes", No: "no" }), }), - enableGenkitMonitoring: defineBoolean("ENABLE_GENKIT_MONITORING", { + enableGenkitMonitoring: defineString("ENABLE_GENKIT_MONITORING", { label: "Enable Genkit Monitoring", description: 'If set to "Yes", enables Genkit Monitoring for collecting and viewing real-time telemetry data. This requires the Cloud Logging API, Cloud Trace API, and Cloud Monitoring API to be enabled, and appropriate IAM roles to be configured. See the documentation for more details.', - default: false, + default: "no", + input: select({ Yes: "yes", No: "no" }), }), harmHateSpeech: defineString("HARM_CATEGORY_HATE_SPEECH", { label: "Hate Speech Threshold", @@ -373,8 +374,8 @@ export function configFromEnv(): GenaiChatbotConfig { topK: num(params.topK.value()), candidateCount: num(params.candidateCount.value()), maxOutputTokens: num(params.maxOutputTokens.value()), - enableOverrides: params.enableOverrides.value(), - enableGenkitMonitoring: params.enableGenkitMonitoring.value(), + enableOverrides: params.enableOverrides.value() === "yes", + enableGenkitMonitoring: params.enableGenkitMonitoring.value() === "yes", safetySettings: buildSafetySettings(), secrets: [apiKeySecret], }; diff --git a/kits/firestore-genai-chatbot/tests/config-parity.test.ts b/kits/firestore-genai-chatbot/tests/config-parity.test.ts new file mode 100644 index 0000000000..1eb6a39b6f --- /dev/null +++ b/kits/firestore-genai-chatbot/tests/config-parity.test.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Compatibility requirement, not aspiration: the extension declared these + * toggles as `yes` / `no` selects, so a deployed `.env` copied from an + * installed instance carries those literal values. + */ + +import { declaredParams } from "firebase-functions/params"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { configFromEnv } from "../src/config"; + +function declaration(name: string) { + const param = declaredParams.find((candidate) => candidate.name === name); + if (!param || !("options" in param)) { + throw new Error(`Missing declaration for ${name}`); + } + const options = param.options as { default?: unknown; input?: unknown }; + return { + type: (param.constructor as unknown as { type: string }).type, + default: options.default, + input: options.input, + }; +} + +const KEYS = [ + "ENABLE_DISCUSSION_OPTION_OVERRIDES", + "ENABLE_GENKIT_MONITORING", + "FIREBASE_CONFIG", +] as const; + +describe("select values inherited from the extension", () => { + const saved = new Map(); + + test("declares the predecessor's labeled string selects", () => { + for (const name of [ + "ENABLE_DISCUSSION_OPTION_OVERRIDES", + "ENABLE_GENKIT_MONITORING", + ]) { + expect(declaration(name)).toEqual({ + type: "string", + default: "no", + input: { + select: { + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + }, + }); + } + }); + + beforeEach(() => { + for (const key of KEYS) { + saved.set(key, process.env[key]); + delete process.env[key]; + } + // The project id resolves out of the runtime-injected FIREBASE_CONFIG. + process.env.FIREBASE_CONFIG = JSON.stringify({ projectId: "demo-test" }); + }); + + afterEach(() => { + for (const [key, value] of saved) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + saved.clear(); + }); + + test("reads ENABLE_DISCUSSION_OPTION_OVERRIDES as yes/no", () => { + process.env.ENABLE_DISCUSSION_OPTION_OVERRIDES = "yes"; + expect(configFromEnv().enableOverrides).toBe(true); + + process.env.ENABLE_DISCUSSION_OPTION_OVERRIDES = "no"; + expect(configFromEnv().enableOverrides).toBe(false); + }); + + test("reads ENABLE_GENKIT_MONITORING as yes/no", () => { + process.env.ENABLE_GENKIT_MONITORING = "yes"; + expect(configFromEnv().enableGenkitMonitoring).toBe(true); + + process.env.ENABLE_GENKIT_MONITORING = "no"; + expect(configFromEnv().enableGenkitMonitoring).toBe(false); + }); + + test("treats an unset variable as off, as the extension did", () => { + const config = configFromEnv(); + + expect(config.enableOverrides).toBe(false); + expect(config.enableGenkitMonitoring).toBe(false); + }); +}); diff --git a/kits/firestore-send-email/README.md b/kits/firestore-send-email/README.md index 722146cd36..e897e6441f 100644 --- a/kits/firestore-send-email/README.md +++ b/kits/firestore-send-email/README.md @@ -214,8 +214,7 @@ a `TypeError` about reading `attachments` into `delivery.error`. It now writes path pattern so nested collections such as `users/{uid}/mail` keep working, and `MAIL_COLLECTION` still defaults to `mail`. - Every environment variable keeps its name, type and default, including - `OAUTH_SECURE`, which was a `true`/`false` dropdown and is now a boolean that - reads those same two values. + `OAUTH_SECURE`, which is still a `true`/`false` dropdown. - Document fields and their meanings are identical: `to`, `cc`, `bcc`, the `*Uids` variants, `message`, `template`, `sendGrid`, `headers`, `categories`, `from` and `replyTo`, along with the validation error messages written to diff --git a/kits/firestore-send-email/src/config.ts b/kits/firestore-send-email/src/config.ts index 9ed0e09a24..acc4361df0 100644 --- a/kits/firestore-send-email/src/config.ts +++ b/kits/firestore-send-email/src/config.ts @@ -15,7 +15,6 @@ */ import { - defineBoolean, defineInt, defineSecret, defineString, @@ -190,12 +189,13 @@ const params = { "The OAuth2 port number for the SMTP server (e.g., 465 for SMTPS, 587 for STARTTLS).", default: 465, }), - oauthSecure: defineBoolean("OAUTH_SECURE", { + oauthSecure: defineString("OAUTH_SECURE", { label: "Use secure OAuth2 connection?", description: "Set to true to enable a secure connection (TLS/SSL) when using OAuth2 authentication for the SMTP server.", - default: true, + default: "true", + input: select({ Yes: "true", No: "false" }), }), clientId: defineSecret("CLIENT_ID", { label: "OAuth2 Client ID", @@ -353,7 +353,7 @@ export function configFromEnv(): SendEmailConfig { tlsOptions: params.tlsOptions.value(), host: params.host.value(), oauthPort: params.oauthPort.value(), - oauthSecure: params.oauthSecure.value(), + oauthSecure: params.oauthSecure.value() === "true", user: params.user.value(), clientId: authType === AuthenticatonType.OAuth2 diff --git a/kits/firestore-send-email/tests/config-parity.test.ts b/kits/firestore-send-email/tests/config-parity.test.ts new file mode 100644 index 0000000000..8b22db2f6e --- /dev/null +++ b/kits/firestore-send-email/tests/config-parity.test.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Compatibility requirement, not aspiration: the extension declared + * `OAUTH_SECURE` as a `true` / `false` select and read it as + * `process.env.OAUTH_SECURE === "true"`, so an unset variable meant an + * insecure connection despite the declared default of `true`. + * `config.test.ts` fakes `firebase-functions/params`, so this runs against the + * real params. + */ + +import { declaredParams } from "firebase-functions/params"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { configFromEnv } from "../src/config"; + +function declaration(name: string) { + const param = declaredParams.find((candidate) => candidate.name === name); + if (!param || !("options" in param)) { + throw new Error(`Missing declaration for ${name}`); + } + const options = param.options as { default?: unknown; input?: unknown }; + return { + type: (param.constructor as unknown as { type: string }).type, + default: options.default, + input: options.input, + }; +} + +describe("OAUTH_SECURE values inherited from the extension", () => { + let saved: string | undefined; + + test("declares the predecessor's labeled string select", () => { + expect(declaration("OAUTH_SECURE")).toEqual({ + type: "string", + default: "true", + input: { + select: { + options: [ + { label: "Yes", value: "true" }, + { label: "No", value: "false" }, + ], + }, + }, + }); + }); + + beforeEach(() => { + saved = process.env.OAUTH_SECURE; + delete process.env.OAUTH_SECURE; + }); + + afterEach(() => { + if (saved === undefined) { + delete process.env.OAUTH_SECURE; + } else { + process.env.OAUTH_SECURE = saved; + } + }); + + test("reads OAUTH_SECURE as true/false", () => { + process.env.OAUTH_SECURE = "true"; + expect(configFromEnv().oauthSecure).toBe(true); + + process.env.OAUTH_SECURE = "false"; + expect(configFromEnv().oauthSecure).toBe(false); + }); + + test("treats an unset variable as insecure, as the extension did", () => { + expect(configFromEnv().oauthSecure).toBe(false); + }); +}); diff --git a/kits/firestore-vector-search/src/config.ts b/kits/firestore-vector-search/src/config.ts index 8b1075c863..234f349d93 100644 --- a/kits/firestore-vector-search/src/config.ts +++ b/kits/firestore-vector-search/src/config.ts @@ -15,7 +15,6 @@ */ import { - defineBoolean, defineInt, defineSecret, defineString, @@ -158,15 +157,17 @@ const params = { default: "status", input: { text: { example: "status" } }, }), - doBackfill: defineBoolean("DO_BACKFILL", { + doBackfill: defineString("DO_BACKFILL", { label: "Embed existing documents?", description: "Should existing documents in the Firestore collection be embedded as well?", + input: select({ Yes: "true", No: "false" }), }), - updateOnConfigure: defineBoolean("UPDATE_ON_CONFIGURE", { + updateOnConfigure: defineString("UPDATE_ON_CONFIGURE", { label: "Update existing embeddings?", description: "Should existing documents in the Firestore collection be updated with new embeddings on reconfiguring the extensions?", + input: select({ Yes: "true", No: "false" }), }), // These name the deployed function, not the fully-qualified queue: the Admin // SDK prefixes the name with `kit--` from @@ -219,8 +220,8 @@ export function configFromEnv(): VectorSearchConfig { inputFieldName: params.inputFieldName.value(), outputFieldName: params.outputFieldName.value(), statusFieldName: params.statusFieldName.value(), - doBackfill: params.doBackfill.value(), - updateOnConfigure: params.updateOnConfigure.value(), + doBackfill: params.doBackfill.value() === "true", + updateOnConfigure: params.updateOnConfigure.value() === "true", region: process.env.FUNCTION_REGION, projectId: projectID.value(), instanceId: params.instanceId.value(), diff --git a/kits/firestore-vector-search/tests/config-parity.test.ts b/kits/firestore-vector-search/tests/config-parity.test.ts new file mode 100644 index 0000000000..e42a8af188 --- /dev/null +++ b/kits/firestore-vector-search/tests/config-parity.test.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Compatibility requirement, not aspiration: the extension declared both + * backfill toggles as required `true` / `false` selects, so a deployed `.env` + * carries those literal values. + */ + +import { declaredParams } from "firebase-functions/params"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { configFromEnv } from "../src/config"; + +function declaration(name: string) { + const param = declaredParams.find((candidate) => candidate.name === name); + if (!param || !("options" in param)) { + throw new Error(`Missing declaration for ${name}`); + } + const options = param.options as { default?: unknown; input?: unknown }; + return { + type: (param.constructor as unknown as { type: string }).type, + default: options.default, + input: options.input, + }; +} + +const KEYS = ["DO_BACKFILL", "UPDATE_ON_CONFIGURE"] as const; + +describe("select values inherited from the extension", () => { + const saved = new Map(); + + test("declares the predecessor's required labeled string selects", () => { + for (const name of ["DO_BACKFILL", "UPDATE_ON_CONFIGURE"]) { + expect(declaration(name)).toEqual({ + type: "string", + default: undefined, + input: { + select: { + options: [ + { label: "Yes", value: "true" }, + { label: "No", value: "false" }, + ], + }, + }, + }); + } + }); + + beforeEach(() => { + for (const key of KEYS) { + saved.set(key, process.env[key]); + delete process.env[key]; + } + }); + + afterEach(() => { + for (const [key, value] of saved) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + saved.clear(); + }); + + test("reads DO_BACKFILL as true/false", () => { + process.env.DO_BACKFILL = "true"; + expect(configFromEnv().doBackfill).toBe(true); + + process.env.DO_BACKFILL = "false"; + expect(configFromEnv().doBackfill).toBe(false); + }); + + test("reads UPDATE_ON_CONFIGURE as true/false", () => { + process.env.UPDATE_ON_CONFIGURE = "true"; + expect(configFromEnv().updateOnConfigure).toBe(true); + + process.env.UPDATE_ON_CONFIGURE = "false"; + expect(configFromEnv().updateOnConfigure).toBe(false); + }); + + test("treats an unset variable as off, as the extension did", () => { + const config = configFromEnv(); + + expect(config.doBackfill).toBe(false); + expect(config.updateOnConfigure).toBe(false); + }); +}); diff --git a/kits/speech-to-text/src/config.ts b/kits/speech-to-text/src/config.ts index 258ace2580..d1c74152b9 100644 --- a/kits/speech-to-text/src/config.ts +++ b/kits/speech-to-text/src/config.ts @@ -15,7 +15,6 @@ */ import { BUCKET_PICKER, - defineBoolean, defineString, select, storageBucket, @@ -88,13 +87,13 @@ const params = { }, }, }), - enableAutomaticPunctuation: defineBoolean("ENABLE_AUTOMATIC_PUNCTUATION", { + enableAutomaticPunctuation: defineString("ENABLE_AUTOMATIC_PUNCTUATION", { label: "Enable automatic punctuation", description: "Should the transcription algorithm attempt to add punctuation to the transcription? For details, see [the documentation](https://cloud.google.com/speech-to-text/docs/automatic-punctuation)", - default: true, - input: select({ Enabled: true, Disabled: false }), + default: "true", + input: select({ Enabled: "true", Disabled: "false" }), }), }; @@ -115,7 +114,8 @@ export function configFromEnv(): SpeechToTextConfig { model: optional(params.model.value()), outputStoragePath: optional(params.outputStoragePath.value()), collectionPath: optional(params.collectionPath.value()), - enableAutomaticPunctuation: params.enableAutomaticPunctuation.value(), + enableAutomaticPunctuation: + params.enableAutomaticPunctuation.value() === "true", }; } diff --git a/kits/speech-to-text/tests/config-parity.test.ts b/kits/speech-to-text/tests/config-parity.test.ts new file mode 100644 index 0000000000..2842d12814 --- /dev/null +++ b/kits/speech-to-text/tests/config-parity.test.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Compatibility requirement, not aspiration: the extension declared + * `ENABLE_AUTOMATIC_PUNCTUATION` as an Enabled / Disabled select carrying the + * literal values `true` / `false`, and read it as + * `process.env.ENABLE_AUTOMATIC_PUNCTUATION === "true"`, so an unset variable + * meant disabled despite the declared default of `true`. + */ + +import { declaredParams } from "firebase-functions/params"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { configFromEnv } from "../src/config"; + +function declaration(name: string) { + const param = declaredParams.find((candidate) => candidate.name === name); + if (!param || !("options" in param)) { + throw new Error(`Missing declaration for ${name}`); + } + const options = param.options as { default?: unknown; input?: unknown }; + return { + type: (param.constructor as unknown as { type: string }).type, + default: options.default, + input: options.input, + }; +} + +describe("ENABLE_AUTOMATIC_PUNCTUATION values inherited from the extension", () => { + let saved: string | undefined; + + test("declares the predecessor's labeled string select", () => { + expect(declaration("ENABLE_AUTOMATIC_PUNCTUATION")).toEqual({ + type: "string", + default: "true", + input: { + select: { + options: [ + { label: "Enabled", value: "true" }, + { label: "Disabled", value: "false" }, + ], + }, + }, + }); + }); + + beforeEach(() => { + saved = process.env.ENABLE_AUTOMATIC_PUNCTUATION; + delete process.env.ENABLE_AUTOMATIC_PUNCTUATION; + }); + + afterEach(() => { + if (saved === undefined) { + delete process.env.ENABLE_AUTOMATIC_PUNCTUATION; + } else { + process.env.ENABLE_AUTOMATIC_PUNCTUATION = saved; + } + }); + + test("reads the Enabled/Disabled option values", () => { + process.env.ENABLE_AUTOMATIC_PUNCTUATION = "true"; + expect(configFromEnv().enableAutomaticPunctuation).toBe(true); + + process.env.ENABLE_AUTOMATIC_PUNCTUATION = "false"; + expect(configFromEnv().enableAutomaticPunctuation).toBe(false); + }); + + test("treats an unset variable as disabled, as the extension did", () => { + expect(configFromEnv().enableAutomaticPunctuation).toBe(false); + }); +}); diff --git a/kits/storage-resize-images/src/config.ts b/kits/storage-resize-images/src/config.ts index 9902641f79..11a60b4012 100644 --- a/kits/storage-resize-images/src/config.ts +++ b/kits/storage-resize-images/src/config.ts @@ -16,7 +16,6 @@ import { BUCKET_PICKER, - defineBoolean, defineInt, defineList, defineString, @@ -99,12 +98,13 @@ const params = { "Delete only on successful resize attempts": "on_success", }), }), - makePublic: defineBoolean("MAKE_PUBLIC", { + makePublic: defineString("MAKE_PUBLIC", { label: "Make resized images public", description: "Do you want to make the resized images public automatically? So you can access them by URL. For example: https://storage.googleapis.com/{bucket}/{path}", - default: false, + default: "false", + input: select({ Yes: "true", No: "false" }), }), resizedImagesPath: defineString("RESIZED_IMAGES_PATH", { label: "Cloud Storage path for resized images", @@ -197,12 +197,12 @@ const params = { }, }, }), - isAnimated: defineBoolean("IS_ANIMATED", { + isAnimated: defineString("IS_ANIMATED", { label: "GIF and WEBP animated option", description: "Keep animation of GIF and WEBP formats.", - default: true, - input: select({ True: true, "No (1st frame only)": false }), + default: "true", + input: select({ Yes: "true", "No (1st frame only)": "false" }), }), memory: defineInt("FUNCTION_MEMORY", { label: "Cloud Function memory", @@ -218,12 +218,13 @@ const params = { "8 GB": 8192, }), }), - regenerateToken: defineBoolean("REGENERATE_TOKEN", { + regenerateToken: defineString("REGENERATE_TOKEN", { label: "Assign new access token", description: "Should resized images have a new access token assigned to them, different from the original image?", - default: true, + default: "true", + input: select({ Yes: "true", No: "false" }), }), contentFilterLevel: defineString("CONTENT_FILTER_LEVEL", { label: "Content filter level", @@ -290,7 +291,7 @@ export function configFromEnv(): ResizeImagesConfig { bucket: params.bucket.value(), sizes: params.sizes.value(), deleteOriginal: params.deleteOriginal.value() as DeleteOriginalFile, - makePublic: params.makePublic.value(), + makePublic: params.makePublic.value() === "true", resizedImagesPath: optional(params.resizedImagesPath.value()), includePathList: optional(params.includePathList.value()), excludePathList: optional(params.excludePathList.value()), @@ -299,9 +300,9 @@ export function configFromEnv(): ResizeImagesConfig { imageTypes: params.imageTypes.value(), outputOptions: optional(params.outputOptions.value()), sharpOptions: params.sharpOptions.value(), - isAnimated: params.isAnimated.value(), + isAnimated: params.isAnimated.value() === "true", memory: params.memory.value(), - regenerateToken: params.regenerateToken.value(), + regenerateToken: params.regenerateToken.value() === "true", contentFilterLevel: params.contentFilterLevel.value() as ResizeImagesConfig["contentFilterLevel"], customFilterPrompt: optional(params.customFilterPrompt.value()), diff --git a/kits/storage-resize-images/tests/config.test.ts b/kits/storage-resize-images/tests/config.test.ts index 7ecaeef67d..9e0fa5c456 100644 --- a/kits/storage-resize-images/tests/config.test.ts +++ b/kits/storage-resize-images/tests/config.test.ts @@ -26,8 +26,22 @@ * same variable as a comma-separated string). */ +import { declaredParams } from "firebase-functions/params"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; +function declaration(name: string) { + const param = declaredParams.find((candidate) => candidate.name === name); + if (!param || !("options" in param)) { + throw new Error(`Missing declaration for ${name}`); + } + const options = param.options as { default?: unknown; input?: unknown }; + return { + type: (param.constructor as unknown as { type: string }).type, + default: options.default, + input: options.input, + }; +} + const ENV_KEYS = [ "IMG_BUCKET", "IMG_SIZES", @@ -90,6 +104,32 @@ describe("configFromEnv", () => { saved.clear(); }); + test("declares the predecessor's labeled string selects", async () => { + await import("../src/config"); + + for (const [name, defaultValue] of [ + ["MAKE_PUBLIC", "false"], + ["IS_ANIMATED", "true"], + ["REGENERATE_TOKEN", "true"], + ] as const) { + expect(declaration(name)).toEqual({ + type: "string", + default: defaultValue, + input: { + select: { + options: [ + { label: "Yes", value: "true" }, + { + label: name === "IS_ANIMATED" ? "No (1st frame only)" : "No", + value: "false", + }, + ], + }, + }, + }); + } + }); + test("reads the same environment variables as the extension", async () => { const { configFromEnv } = await import("../src/config"); const config = configFromEnv(); From 0c335c6671ca7ee95182b53c402c2e708164e1bf Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 8 Sep 2026 13:09:18 +0100 Subject: [PATCH 2/4] test(kits): fold the config-parity suites into the kits' config tests The `config-parity.test.ts` name added a third convention next to the existing `config.test.ts` / `config-runtime.test.ts` split, so the cases move into that split instead. Coverage is unchanged: every declaration, option label, option value, option order, parsed value, and unset-variable case is kept verbatim. - firestore-genai-chatbot, firestore-vector-search, speech-to-text had no config test at all, so the file becomes `tests/config.test.ts`. - firestore-bigquery-export and firestore-send-email already have a `tests/config.test.ts` that fakes `firebase-functions/params`, so these cases, which need the real params, become `tests/config-runtime.test.ts`, the name delete-user-data already uses for the same reason. --- .../tests/{config-parity.test.ts => config-runtime.test.ts} | 0 .../tests/{config-parity.test.ts => config.test.ts} | 0 .../tests/{config-parity.test.ts => config-runtime.test.ts} | 0 .../tests/{config-parity.test.ts => config.test.ts} | 0 .../tests/{config-parity.test.ts => config.test.ts} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename kits/firestore-bigquery-export/tests/{config-parity.test.ts => config-runtime.test.ts} (100%) rename kits/firestore-genai-chatbot/tests/{config-parity.test.ts => config.test.ts} (100%) rename kits/firestore-send-email/tests/{config-parity.test.ts => config-runtime.test.ts} (100%) rename kits/firestore-vector-search/tests/{config-parity.test.ts => config.test.ts} (100%) rename kits/speech-to-text/tests/{config-parity.test.ts => config.test.ts} (100%) diff --git a/kits/firestore-bigquery-export/tests/config-parity.test.ts b/kits/firestore-bigquery-export/tests/config-runtime.test.ts similarity index 100% rename from kits/firestore-bigquery-export/tests/config-parity.test.ts rename to kits/firestore-bigquery-export/tests/config-runtime.test.ts diff --git a/kits/firestore-genai-chatbot/tests/config-parity.test.ts b/kits/firestore-genai-chatbot/tests/config.test.ts similarity index 100% rename from kits/firestore-genai-chatbot/tests/config-parity.test.ts rename to kits/firestore-genai-chatbot/tests/config.test.ts diff --git a/kits/firestore-send-email/tests/config-parity.test.ts b/kits/firestore-send-email/tests/config-runtime.test.ts similarity index 100% rename from kits/firestore-send-email/tests/config-parity.test.ts rename to kits/firestore-send-email/tests/config-runtime.test.ts diff --git a/kits/firestore-vector-search/tests/config-parity.test.ts b/kits/firestore-vector-search/tests/config.test.ts similarity index 100% rename from kits/firestore-vector-search/tests/config-parity.test.ts rename to kits/firestore-vector-search/tests/config.test.ts diff --git a/kits/speech-to-text/tests/config-parity.test.ts b/kits/speech-to-text/tests/config.test.ts similarity index 100% rename from kits/speech-to-text/tests/config-parity.test.ts rename to kits/speech-to-text/tests/config.test.ts From 9385a86d2a5cc5cbc139675c1e61eb29a47ef5d3 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 8 Sep 2026 13:29:57 +0100 Subject: [PATCH 3/4] fix(kits): keep boolean params for the true/false selects The previous commit converted all ten extension selects to defineString with manual === "true" coercion. Only the three yes/no params need that: BooleanParam.runtimeValue() is `env === "true"`, so for the seven params whose extension values are already true/false the conversion changed no runtime value, and the declared default drives only the deploy-time prompt (an unset variable still reads false, matching the predecessor). select is generic, so a BooleanParam carries the extension's option labels directly. Reverts OAUTH_SECURE, DO_BACKFILL, UPDATE_ON_CONFIGURE, ENABLE_AUTOMATIC_PUNCTUATION, MAKE_PUBLIC, IS_ANIMATED and REGENERATE_TOKEN to defineBoolean with select, restoring the labels without giving up the typed param or hand-writing coercion. ENABLE_AUTO_DISCOVERY, ENABLE_DISCUSSION_OPTION_OVERRIDES and ENABLE_GENKIT_MONITORING stay defineString, matching #3145's scope. Tests now pin type: "boolean" with boolean-valued options; the runtime and unset-variable assertions are unchanged. Adds the per-kit CHANGELOG entries the branch was missing. --- kits/delete-user-data/CHANGELOG.md | 1 + kits/firestore-genai-chatbot/CHANGELOG.md | 1 + kits/firestore-send-email/CHANGELOG.md | 1 + kits/firestore-send-email/README.md | 3 ++- kits/firestore-send-email/src/config.ts | 9 ++++--- .../tests/config-runtime.test.ts | 15 ++++++----- kits/firestore-vector-search/CHANGELOG.md | 1 + kits/firestore-vector-search/src/config.ts | 13 +++++----- .../tests/config.test.ts | 8 +++--- kits/speech-to-text/CHANGELOG.md | 1 + kits/speech-to-text/src/config.ts | 10 ++++---- kits/speech-to-text/tests/config.test.ts | 15 ++++++----- kits/storage-resize-images/CHANGELOG.md | 1 + kits/storage-resize-images/src/config.ts | 25 ++++++++++--------- .../tests/config.test.ts | 14 +++++------ 15 files changed, 67 insertions(+), 51 deletions(-) diff --git a/kits/delete-user-data/CHANGELOG.md b/kits/delete-user-data/CHANGELOG.md index edf59238f5..6c0068152d 100644 --- a/kits/delete-user-data/CHANGELOG.md +++ b/kits/delete-user-data/CHANGELOG.md @@ -1,2 +1,3 @@ +- fix: `ENABLE_AUTO_DISCOVERY` accepts the extension's `yes` / `no` values again. The kit had declared it as a boolean param, which only counts the literal `true` as enabled, so a `.env` copied from an installed extension instance carried `yes` and silently left auto-discovery switched off. The param is now the extension's labeled `Yes` / `No` select and is read the same way the extension read it. - Initial release of kit, see README for differences between the legacy extension and this kit - The instance id now comes from `FIREBASE_KIT_INSTANCE_ID`, which the Firebase CLI (15.27.0 or later) provides to each kit instance; `INSTANCE_ID` is no longer a configuration parameter diff --git a/kits/firestore-genai-chatbot/CHANGELOG.md b/kits/firestore-genai-chatbot/CHANGELOG.md index 711eb60d36..cdb68889e6 100644 --- a/kits/firestore-genai-chatbot/CHANGELOG.md +++ b/kits/firestore-genai-chatbot/CHANGELOG.md @@ -1 +1,2 @@ +- fix: `ENABLE_DISCUSSION_OPTION_OVERRIDES` and `ENABLE_GENKIT_MONITORING` accept the extension's `yes` / `no` values again. The kit had declared them as boolean params, which only count the literal `true` as enabled, so a `.env` copied from an installed extension instance carried `yes` and silently left per-discussion overrides and Genkit monitoring off. Both are now the extension's labeled `Yes` / `No` selects and are read the same way the extension read them. - Initial release of kit, see README for differences between the legacy extension and this kit diff --git a/kits/firestore-send-email/CHANGELOG.md b/kits/firestore-send-email/CHANGELOG.md index 156348bc17..ec605dc946 100644 --- a/kits/firestore-send-email/CHANGELOG.md +++ b/kits/firestore-send-email/CHANGELOG.md @@ -1,3 +1,4 @@ +- fix: restore the extension's `Yes` / `No` option labels on the `OAUTH_SECURE` deploy-time prompt. The stored values are unchanged (`true`/`false`), so this is a label-only fix and no `.env` from an earlier deploy needs editing. - Initial release of kit, see README for differences between the legacy extension and this kit - SendGrid sends now work with `AUTH_TYPE=OAuth2`: the `SMTP_PASSWORD` secret is no longer dropped from the config under OAuth2, so the SendGrid transport receives its API key - SendGrid delivery no longer fails with `sgMail.setApiKey is not a function`: the transport imports `@sendgrid/mail` in a form that survives the compiled output diff --git a/kits/firestore-send-email/README.md b/kits/firestore-send-email/README.md index 653b4b1237..c4f5879469 100644 --- a/kits/firestore-send-email/README.md +++ b/kits/firestore-send-email/README.md @@ -217,7 +217,8 @@ a `TypeError` about reading `attachments` into `delivery.error`. It now writes path pattern so nested collections such as `users/{uid}/mail` keep working, and `MAIL_COLLECTION` still defaults to `mail`. - Every environment variable keeps its name, type and default, including - `OAUTH_SECURE`, which is still a `true`/`false` dropdown. + `OAUTH_SECURE`, which is still a `true`/`false` dropdown, and keeps the + extension's `Yes` / `No` option labels. - Document fields and their meanings are identical: `to`, `cc`, `bcc`, the `*Uids` variants, `message`, `template`, `sendGrid`, `headers`, `categories`, `from` and `replyTo`, along with the validation error messages written to diff --git a/kits/firestore-send-email/src/config.ts b/kits/firestore-send-email/src/config.ts index 41a8a0b69a..c94523b607 100644 --- a/kits/firestore-send-email/src/config.ts +++ b/kits/firestore-send-email/src/config.ts @@ -15,6 +15,7 @@ */ import { + defineBoolean, defineInt, defineSecret, defineString, @@ -189,13 +190,13 @@ const params = { "The OAuth2 port number for the SMTP server (e.g., 465 for SMTPS, 587 for STARTTLS).", default: 465, }), - oauthSecure: defineString("OAUTH_SECURE", { + oauthSecure: defineBoolean("OAUTH_SECURE", { label: "Use secure OAuth2 connection?", description: "Set to true to enable a secure connection (TLS/SSL) when using OAuth2 authentication for the SMTP server.", - default: "true", - input: select({ Yes: "true", No: "false" }), + default: true, + input: select({ Yes: true, No: false }), }), clientId: defineSecret("CLIENT_ID", { label: "OAuth2 Client ID", @@ -359,7 +360,7 @@ export function configFromEnv(): SendEmailConfig { tlsOptions: params.tlsOptions.value(), host: params.host.value(), oauthPort: params.oauthPort.value(), - oauthSecure: params.oauthSecure.value() === "true", + oauthSecure: params.oauthSecure.value(), user: params.user.value(), clientId: authType === AuthenticatonType.OAuth2 diff --git a/kits/firestore-send-email/tests/config-runtime.test.ts b/kits/firestore-send-email/tests/config-runtime.test.ts index 8b22db2f6e..d1a5180a42 100644 --- a/kits/firestore-send-email/tests/config-runtime.test.ts +++ b/kits/firestore-send-email/tests/config-runtime.test.ts @@ -18,7 +18,10 @@ * Compatibility requirement, not aspiration: the extension declared * `OAUTH_SECURE` as a `true` / `false` select and read it as * `process.env.OAUTH_SECURE === "true"`, so an unset variable meant an - * insecure connection despite the declared default of `true`. + * insecure connection despite the declared default of `true`. `BooleanParam` + * resolves identically (`runtimeValue()` is `env === "true"`, the default only + * drives the deploy-time prompt), so the kit keeps the boolean param and + * carries the extension's labels through a `select`. * `config.test.ts` fakes `firebase-functions/params`, so this runs against the * real params. */ @@ -43,15 +46,15 @@ function declaration(name: string) { describe("OAUTH_SECURE values inherited from the extension", () => { let saved: string | undefined; - test("declares the predecessor's labeled string select", () => { + test("declares the predecessor's labeled boolean select", () => { expect(declaration("OAUTH_SECURE")).toEqual({ - type: "string", - default: "true", + type: "boolean", + default: true, input: { select: { options: [ - { label: "Yes", value: "true" }, - { label: "No", value: "false" }, + { label: "Yes", value: true }, + { label: "No", value: false }, ], }, }, diff --git a/kits/firestore-vector-search/CHANGELOG.md b/kits/firestore-vector-search/CHANGELOG.md index bccb4a65f6..b4b803cbf3 100644 --- a/kits/firestore-vector-search/CHANGELOG.md +++ b/kits/firestore-vector-search/CHANGELOG.md @@ -1,3 +1,4 @@ +- fix: restore the extension's `Yes` / `No` option labels on the `DO_BACKFILL` and `UPDATE_ON_CONFIGURE` deploy-time prompts. The stored values are unchanged (`true`/`false`), so this is a label-only fix and no `.env` from an earlier deploy needs editing. - Fixed the backfill and update task dispatch failing with "Queue does not exist": the kit prefixed queue names with `kit--` itself, which the Admin SDK then prefixed again from `FIREBASE_KIT_INSTANCE_ID`. The four `*_QUEUE_NAME` settings now take the deployed function name without that prefix - OpenAI embeddings are back on the extension's model and size: `EMBEDDING_PROVIDER: openai` requests `text-embedding-ada-002` at its native 1536 dimensions with a batch size of 16, replacing `text-embedding-3-small` pinned at 512 with a batch size of 1. Vectors written by an earlier version of the kit are not comparable with the ones it writes now, so re-embed the collection after upgrading. The vector index the kit creates for OpenAI is still declared with 512 dimensions, exactly as the extension declared it, so it does not cover the 1536-dimension vectors and `findNearest` fails against it; create the 1536-dimension index yourself if you query an OpenAI-embedded collection. - Initial release of kit, see README for differences between the legacy extension and this kit diff --git a/kits/firestore-vector-search/src/config.ts b/kits/firestore-vector-search/src/config.ts index 234f349d93..d0ad747761 100644 --- a/kits/firestore-vector-search/src/config.ts +++ b/kits/firestore-vector-search/src/config.ts @@ -15,6 +15,7 @@ */ import { + defineBoolean, defineInt, defineSecret, defineString, @@ -157,17 +158,17 @@ const params = { default: "status", input: { text: { example: "status" } }, }), - doBackfill: defineString("DO_BACKFILL", { + doBackfill: defineBoolean("DO_BACKFILL", { label: "Embed existing documents?", description: "Should existing documents in the Firestore collection be embedded as well?", - input: select({ Yes: "true", No: "false" }), + input: select({ Yes: true, No: false }), }), - updateOnConfigure: defineString("UPDATE_ON_CONFIGURE", { + updateOnConfigure: defineBoolean("UPDATE_ON_CONFIGURE", { label: "Update existing embeddings?", description: "Should existing documents in the Firestore collection be updated with new embeddings on reconfiguring the extensions?", - input: select({ Yes: "true", No: "false" }), + input: select({ Yes: true, No: false }), }), // These name the deployed function, not the fully-qualified queue: the Admin // SDK prefixes the name with `kit--` from @@ -220,8 +221,8 @@ export function configFromEnv(): VectorSearchConfig { inputFieldName: params.inputFieldName.value(), outputFieldName: params.outputFieldName.value(), statusFieldName: params.statusFieldName.value(), - doBackfill: params.doBackfill.value() === "true", - updateOnConfigure: params.updateOnConfigure.value() === "true", + doBackfill: params.doBackfill.value(), + updateOnConfigure: params.updateOnConfigure.value(), region: process.env.FUNCTION_REGION, projectId: projectID.value(), instanceId: params.instanceId.value(), diff --git a/kits/firestore-vector-search/tests/config.test.ts b/kits/firestore-vector-search/tests/config.test.ts index e42a8af188..a8ba477836 100644 --- a/kits/firestore-vector-search/tests/config.test.ts +++ b/kits/firestore-vector-search/tests/config.test.ts @@ -42,16 +42,16 @@ const KEYS = ["DO_BACKFILL", "UPDATE_ON_CONFIGURE"] as const; describe("select values inherited from the extension", () => { const saved = new Map(); - test("declares the predecessor's required labeled string selects", () => { + test("declares the predecessor's required labeled boolean selects", () => { for (const name of ["DO_BACKFILL", "UPDATE_ON_CONFIGURE"]) { expect(declaration(name)).toEqual({ - type: "string", + type: "boolean", default: undefined, input: { select: { options: [ - { label: "Yes", value: "true" }, - { label: "No", value: "false" }, + { label: "Yes", value: true }, + { label: "No", value: false }, ], }, }, diff --git a/kits/speech-to-text/CHANGELOG.md b/kits/speech-to-text/CHANGELOG.md index c1a366237d..628c00529e 100644 --- a/kits/speech-to-text/CHANGELOG.md +++ b/kits/speech-to-text/CHANGELOG.md @@ -1,2 +1,3 @@ +- fix: restore the extension's `Enabled` / `Disabled` option labels on the `ENABLE_AUTOMATIC_PUNCTUATION` deploy-time prompt. The stored values are unchanged (`true`/`false`), so this is a label-only fix and no `.env` from an earlier deploy needs editing. - Initial release of kit, see README for differences between the legacy extension and this kit - The `.txt` transcription output no longer has `tmp/` stripped from its path, a remnant of the legacy extension's temp-file handling; it lands at `_transcription.txt` exactly ([#3026](https://github.com/firebase/extensions/issues/3026)) diff --git a/kits/speech-to-text/src/config.ts b/kits/speech-to-text/src/config.ts index d1c74152b9..258ace2580 100644 --- a/kits/speech-to-text/src/config.ts +++ b/kits/speech-to-text/src/config.ts @@ -15,6 +15,7 @@ */ import { BUCKET_PICKER, + defineBoolean, defineString, select, storageBucket, @@ -87,13 +88,13 @@ const params = { }, }, }), - enableAutomaticPunctuation: defineString("ENABLE_AUTOMATIC_PUNCTUATION", { + enableAutomaticPunctuation: defineBoolean("ENABLE_AUTOMATIC_PUNCTUATION", { label: "Enable automatic punctuation", description: "Should the transcription algorithm attempt to add punctuation to the transcription? For details, see [the documentation](https://cloud.google.com/speech-to-text/docs/automatic-punctuation)", - default: "true", - input: select({ Enabled: "true", Disabled: "false" }), + default: true, + input: select({ Enabled: true, Disabled: false }), }), }; @@ -114,8 +115,7 @@ export function configFromEnv(): SpeechToTextConfig { model: optional(params.model.value()), outputStoragePath: optional(params.outputStoragePath.value()), collectionPath: optional(params.collectionPath.value()), - enableAutomaticPunctuation: - params.enableAutomaticPunctuation.value() === "true", + enableAutomaticPunctuation: params.enableAutomaticPunctuation.value(), }; } diff --git a/kits/speech-to-text/tests/config.test.ts b/kits/speech-to-text/tests/config.test.ts index 2842d12814..394b1b94d3 100644 --- a/kits/speech-to-text/tests/config.test.ts +++ b/kits/speech-to-text/tests/config.test.ts @@ -19,7 +19,10 @@ * `ENABLE_AUTOMATIC_PUNCTUATION` as an Enabled / Disabled select carrying the * literal values `true` / `false`, and read it as * `process.env.ENABLE_AUTOMATIC_PUNCTUATION === "true"`, so an unset variable - * meant disabled despite the declared default of `true`. + * meant disabled despite the declared default of `true`. `BooleanParam` + * resolves identically (`runtimeValue()` is `env === "true"`, the default only + * drives the deploy-time prompt), so the kit keeps the boolean param and + * carries the extension's labels through a `select`. */ import { declaredParams } from "firebase-functions/params"; @@ -42,15 +45,15 @@ function declaration(name: string) { describe("ENABLE_AUTOMATIC_PUNCTUATION values inherited from the extension", () => { let saved: string | undefined; - test("declares the predecessor's labeled string select", () => { + test("declares the predecessor's labeled boolean select", () => { expect(declaration("ENABLE_AUTOMATIC_PUNCTUATION")).toEqual({ - type: "string", - default: "true", + type: "boolean", + default: true, input: { select: { options: [ - { label: "Enabled", value: "true" }, - { label: "Disabled", value: "false" }, + { label: "Enabled", value: true }, + { label: "Disabled", value: false }, ], }, }, diff --git a/kits/storage-resize-images/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index 6729d6545c..31a7c92cf5 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1,3 +1,4 @@ +- fix: restore the extension's option labels on the `MAKE_PUBLIC`, `IS_ANIMATED` and `REGENERATE_TOKEN` deploy-time prompts; `IS_ANIMATED` in particular was labelled "True" instead of "Yes" and lost "No (1st frame only)". The stored values are unchanged (`true`/`false`), so this is a label-only fix and no `.env` from an earlier deploy needs editing. - fix: the "Off (No filtering)" content-filter option now stores `OFF` instead of `False`, which the resolver rejected, so every storage event threw `Invalid HarmBlockThreshold: False`. A `.env` written by an earlier deploy keeps the stored value on upgrade: if it carries `CONTENT_FILTER_LEVEL=False`, edit it to `OFF` (or re-select the option). - fix: the "original" image-type option now stores `false` instead of `False`. The resize path treated `False` as a target format, so each resized file was uploaded as `_.False` with no content type and reported as a success. A stored `IMAGE_TYPE=["False"]` likewise needs editing to `["false"]`. - fix: restore the `us-central1` content-filter fallback. `checkImageContent` threw `FUNCTION_REGION is required for Vertex AI filtering.` when no region was available; the extension fell back to `us-central1`. The Vertex AI call now uses the function's region when known and `us-central1` otherwise, matching the extension. Normal CLI deploys were unaffected (the Firebase CLI sets `FUNCTION_REGION` on deployed functions); the throw was reachable for library consumers, emulator runs, and hand-rolled environments. diff --git a/kits/storage-resize-images/src/config.ts b/kits/storage-resize-images/src/config.ts index 3a91842dda..0e753da04f 100644 --- a/kits/storage-resize-images/src/config.ts +++ b/kits/storage-resize-images/src/config.ts @@ -16,6 +16,7 @@ import { BUCKET_PICKER, + defineBoolean, defineInt, defineList, defineString, @@ -82,13 +83,13 @@ const params = { "Delete only on successful resize attempts": "on_success", }), }), - makePublic: defineString("MAKE_PUBLIC", { + makePublic: defineBoolean("MAKE_PUBLIC", { label: "Make resized images public", description: "Do you want to make the resized images public automatically? So you can access them by URL. For example: https://storage.googleapis.com/{bucket}/{path}", - default: "false", - input: select({ Yes: "true", No: "false" }), + default: false, + input: select({ Yes: true, No: false }), }), resizedImagesPath: defineString("RESIZED_IMAGES_PATH", { label: "Cloud Storage path for resized images", @@ -181,12 +182,12 @@ const params = { }, }, }), - isAnimated: defineString("IS_ANIMATED", { + isAnimated: defineBoolean("IS_ANIMATED", { label: "GIF and WEBP animated option", description: "Keep animation of GIF and WEBP formats.", - default: "true", - input: select({ Yes: "true", "No (1st frame only)": "false" }), + default: true, + input: select({ Yes: true, "No (1st frame only)": false }), }), memory: defineInt("FUNCTION_MEMORY", { label: "Cloud Function memory", @@ -202,13 +203,13 @@ const params = { "8 GB": 8192, }), }), - regenerateToken: defineString("REGENERATE_TOKEN", { + regenerateToken: defineBoolean("REGENERATE_TOKEN", { label: "Assign new access token", description: "Should resized images have a new access token assigned to them, different from the original image?", - default: "true", - input: select({ Yes: "true", No: "false" }), + default: true, + input: select({ Yes: true, No: false }), }), contentFilterLevel: defineString("CONTENT_FILTER_LEVEL", { label: "Content filter level", @@ -275,7 +276,7 @@ export function configFromEnv(): ResizeImagesConfig { bucket: params.bucket.value(), sizes: params.sizes.value(), deleteOriginal: params.deleteOriginal.value() as DeleteOriginalFile, - makePublic: params.makePublic.value() === "true", + makePublic: params.makePublic.value(), resizedImagesPath: optional(params.resizedImagesPath.value()), includePathList: optional(params.includePathList.value()), excludePathList: optional(params.excludePathList.value()), @@ -284,9 +285,9 @@ export function configFromEnv(): ResizeImagesConfig { imageTypes: params.imageTypes.value(), outputOptions: optional(params.outputOptions.value()), sharpOptions: params.sharpOptions.value(), - isAnimated: params.isAnimated.value() === "true", + isAnimated: params.isAnimated.value(), memory: params.memory.value(), - regenerateToken: params.regenerateToken.value() === "true", + regenerateToken: params.regenerateToken.value(), contentFilterLevel: params.contentFilterLevel.value() as ResizeImagesConfig["contentFilterLevel"], customFilterPrompt: optional(params.customFilterPrompt.value()), diff --git a/kits/storage-resize-images/tests/config.test.ts b/kits/storage-resize-images/tests/config.test.ts index 8882b3d915..e0ca786ffb 100644 --- a/kits/storage-resize-images/tests/config.test.ts +++ b/kits/storage-resize-images/tests/config.test.ts @@ -112,24 +112,24 @@ describe("configFromEnv", () => { saved.clear(); }); - test("declares the predecessor's labeled string selects", async () => { + test("declares the predecessor's labeled boolean selects", async () => { await import("../src/config"); for (const [name, defaultValue] of [ - ["MAKE_PUBLIC", "false"], - ["IS_ANIMATED", "true"], - ["REGENERATE_TOKEN", "true"], + ["MAKE_PUBLIC", false], + ["IS_ANIMATED", true], + ["REGENERATE_TOKEN", true], ] as const) { expect(declaration(name)).toEqual({ - type: "string", + type: "boolean", default: defaultValue, input: { select: { options: [ - { label: "Yes", value: "true" }, + { label: "Yes", value: true }, { label: name === "IS_ANIMATED" ? "No (1st frame only)" : "No", - value: "false", + value: false, }, ], }, From e8a954a02c97578766f1e35ea00388f4064e071a Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Tue, 8 Sep 2026 16:00:26 +0100 Subject: [PATCH 4/4] fix(storage-resize-images): preselect the extension default for MAKE_PUBLIC The CLI's select prompt passes the declared `default` straight to inquirer while stringifying every option value (firebase-tools `promptSelect`), so a non-string default matches no option and the first option is highlighted. `MAKE_PUBLIC` is the only select in this kit whose extension default is not the first option: the prompt highlighted `Yes`, so accepting it stored `MAKE_PUBLIC=true` and made every resized image public, where the extension stored `false`. Declare the param as a string with `default: "false"` and option values `"true"` / `"false"`, and coerce with `=== "true"` as the extension did. The stored values and the runtime result are unchanged, so no existing `.env` needs editing. `IS_ANIMATED` and `REGENERATE_TOKEN` keep `defineBoolean`: their default is `true` and `Yes` is their first option, so the highlighted option already matches the extension. --- kits/storage-resize-images/CHANGELOG.md | 1 + kits/storage-resize-images/src/config.ts | 18 +++++++--- .../tests/config.test.ts | 36 ++++++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/kits/storage-resize-images/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index 31a7c92cf5..7bd0a45a08 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1,3 +1,4 @@ +- fix: `MAKE_PUBLIC` now preselects "No" at the prompt, as the extension did. The prompt highlighted "Yes", so accepting the default stored `MAKE_PUBLIC=true` and made every resized image public; the extension's default is `false`. Stored values are unchanged (`true`/`false`), so no `.env` from an earlier deploy needs editing, but check any deploy that accepted the highlighted option. - fix: restore the extension's option labels on the `MAKE_PUBLIC`, `IS_ANIMATED` and `REGENERATE_TOKEN` deploy-time prompts; `IS_ANIMATED` in particular was labelled "True" instead of "Yes" and lost "No (1st frame only)". The stored values are unchanged (`true`/`false`), so this is a label-only fix and no `.env` from an earlier deploy needs editing. - fix: the "Off (No filtering)" content-filter option now stores `OFF` instead of `False`, which the resolver rejected, so every storage event threw `Invalid HarmBlockThreshold: False`. A `.env` written by an earlier deploy keeps the stored value on upgrade: if it carries `CONTENT_FILTER_LEVEL=False`, edit it to `OFF` (or re-select the option). - fix: the "original" image-type option now stores `false` instead of `False`. The resize path treated `False` as a target format, so each resized file was uploaded as `_.False` with no content type and reported as a success. A stored `IMAGE_TYPE=["False"]` likewise needs editing to `["false"]`. diff --git a/kits/storage-resize-images/src/config.ts b/kits/storage-resize-images/src/config.ts index bde64a8bcf..43af13a5d6 100644 --- a/kits/storage-resize-images/src/config.ts +++ b/kits/storage-resize-images/src/config.ts @@ -83,13 +83,22 @@ const params = { "Delete only on successful resize attempts": "on_success", }), }), - makePublic: defineBoolean("MAKE_PUBLIC", { + // A string param, unlike the sibling `IS_ANIMATED` / `REGENERATE_TOKEN` + // booleans: the CLI's select prompt compares its `default` against + // `option.value.toString()` (firebase-tools `promptSelect`), so a non-string + // default never matches an option and the first option is highlighted + // instead. The extension's default is `false` ("No"), but a `defineBoolean` + // here left "Yes" preselected, so pressing Enter stored `MAKE_PUBLIC=true` + // and made every resized image public. Declaring the default as the string + // `"false"` preselects "No" as the extension did. Stored values stay + // `true`/`false`, so no existing `.env` needs editing. + makePublic: defineString("MAKE_PUBLIC", { label: "Make resized images public", description: "Do you want to make the resized images public automatically? So you can access them by URL. For example: https://storage.googleapis.com/{bucket}/{path}", - default: false, - input: select({ Yes: true, No: false }), + default: "false", + input: select({ Yes: "true", No: "false" }), }), resizedImagesPath: defineString("RESIZED_IMAGES_PATH", { label: "Cloud Storage path for resized images", @@ -296,7 +305,8 @@ export function configFromEnv(): ResizeImagesConfig { bucket: params.bucket.value(), sizes: params.sizes.value(), deleteOriginal: params.deleteOriginal.value() as DeleteOriginalFile, - makePublic: params.makePublic.value(), + // Matches the extension's `process.env.MAKE_PUBLIC === "true"`. + makePublic: params.makePublic.value() === "true", resizedImagesPath: optional(params.resizedImagesPath.value()), includePathList: optional(params.includePathList.value()), excludePathList: optional(params.excludePathList.value()), diff --git a/kits/storage-resize-images/tests/config.test.ts b/kits/storage-resize-images/tests/config.test.ts index 55a9cc8101..60d281dc1e 100644 --- a/kits/storage-resize-images/tests/config.test.ts +++ b/kits/storage-resize-images/tests/config.test.ts @@ -116,7 +116,6 @@ describe("configFromEnv", () => { await import("../src/config"); for (const [name, defaultValue] of [ - ["MAKE_PUBLIC", false], ["IS_ANIMATED", true], ["REGENERATE_TOKEN", true], ] as const) { @@ -138,6 +137,37 @@ describe("configFromEnv", () => { } }); + // MAKE_PUBLIC is the one select in this kit whose extension default is not + // the first option, so it is the one that exposes the CLI's non-string + // default handling: `promptSelect` passes the declared default straight to + // inquirer while stringifying every option value, so a boolean `false` + // default matched nothing and "Yes" was preselected. A deploy that accepted + // the prompt therefore stored MAKE_PUBLIC=true and published every resized + // image, where the extension stored `false`. + test("declares MAKE_PUBLIC so the CLI preselects the extension default", async () => { + await import("../src/config"); + const declared = declaration("MAKE_PUBLIC"); + + expect(declared.type).toBe("string"); + expect(declared.default).toBe("false"); + expect(declared.input).toEqual({ + select: { + options: [ + { label: "Yes", value: "true" }, + { label: "No", value: "false" }, + ], + }, + }); + + // The comparison the CLI actually makes: `default` against + // `option.value.toString()`. + const options = (declared.input as SelectInput).select.options; + const preselected = options.filter( + (option) => String(option.value) === declared.default + ); + expect(preselected).toEqual([{ label: "No", value: "false" }]); + }); + test("reads the same environment variables as the extension", async () => { const { configFromEnv } = await import("../src/config"); const config = configFromEnv(); @@ -232,6 +262,7 @@ describe("configFromEnv", () => { // undefined so the resolver can apply its default.) delete process.env.IS_ANIMATED; delete process.env.REGENERATE_TOKEN; + delete process.env.MAKE_PUBLIC; delete process.env.FUNCTION_MEMORY; delete process.env.SHARP_OPTIONS; @@ -240,6 +271,9 @@ describe("configFromEnv", () => { expect(config.isAnimated).toBe(false); expect(config.regenerateToken).toBe(false); + // The extension read `process.env.MAKE_PUBLIC === "true"`, so an unset + // variable was `false` there too. + expect(config.makePublic).toBe(false); expect(config.memory).toBeUndefined(); expect(config.sharpOptions).toBe(""); });