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/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-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-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.test.ts b/kits/firestore-genai-chatbot/tests/config.test.ts new file mode 100644 index 0000000000..1eb6a39b6f --- /dev/null +++ b/kits/firestore-genai-chatbot/tests/config.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/CHANGELOG.md b/kits/firestore-send-email/CHANGELOG.md index b75c77a037..8085e080d3 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. - chore: run on firebase-functions ^7.3.3-rc.0, the same release candidate as the other kits - fix: map `DATABASE_REGION` to a valid Cloud Run region before using it as the function region. Firestore multi-region locations (`nam5`, `nam7`) now deploy the function to `us-central1` and `eur3` to `europe-west1` instead of failing the deploy; regional locations pass through unchanged. The value is matched case-insensitively. With the parameter unset or empty the function declares no region and the Firebase CLI resolves one at deploy time. - Initial release of kit, see README for differences between the legacy extension and this kit diff --git a/kits/firestore-send-email/README.md b/kits/firestore-send-email/README.md index c473548f80..0615b4b26c 100644 --- a/kits/firestore-send-email/README.md +++ b/kits/firestore-send-email/README.md @@ -243,8 +243,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 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, 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 883d167522..ed80adb223 100644 --- a/kits/firestore-send-email/src/config.ts +++ b/kits/firestore-send-email/src/config.ts @@ -197,6 +197,7 @@ const params = { "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 }), }), clientId: defineSecret("CLIENT_ID", { label: "OAuth2 Client ID", diff --git a/kits/firestore-send-email/tests/config-runtime.test.ts b/kits/firestore-send-email/tests/config-runtime.test.ts new file mode 100644 index 0000000000..d1a5180a42 --- /dev/null +++ b/kits/firestore-send-email/tests/config-runtime.test.ts @@ -0,0 +1,88 @@ +/** + * 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`. `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. + */ + +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 boolean select", () => { + expect(declaration("OAUTH_SECURE")).toEqual({ + type: "boolean", + 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/CHANGELOG.md b/kits/firestore-vector-search/CHANGELOG.md index df84dee82b..d2fc89842a 100644 --- a/kits/firestore-vector-search/CHANGELOG.md +++ b/kits/firestore-vector-search/CHANGELOG.md @@ -1,5 +1,6 @@ - chore: run on firebase-functions ^7.3.3-rc.0, the same release candidate as the other kits - 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 +- 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 673e26a700..4613e19920 100644 --- a/kits/firestore-vector-search/src/config.ts +++ b/kits/firestore-vector-search/src/config.ts @@ -180,11 +180,13 @@ const params = { 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", { 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 diff --git a/kits/firestore-vector-search/tests/config.test.ts b/kits/firestore-vector-search/tests/config.test.ts index 50061dcd1b..07bc1dfe4b 100644 --- a/kits/firestore-vector-search/tests/config.test.ts +++ b/kits/firestore-vector-search/tests/config.test.ts @@ -62,3 +62,71 @@ describe("instance id", () => { ); }); }); + +/** + * Compatibility requirement, not aspiration: the extension declared both + * backfill toggles as required `true` / `false` selects, so a deployed `.env` + * carries those literal values. + */ +describe("select values inherited from the extension", () => { + const KEYS = ["DO_BACKFILL", "UPDATE_ON_CONFIGURE"] as const; + + 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, + }; + } + + beforeEach(() => { + for (const key of KEYS) { + vi.stubEnv(key, undefined); + } + }); + + test("declares the predecessor's required labeled boolean selects", () => { + for (const name of KEYS) { + expect(declaration(name)).toEqual({ + type: "boolean", + default: undefined, + input: { + select: { + options: [ + { label: "Yes", value: true }, + { label: "No", value: false }, + ], + }, + }, + }); + } + }); + + test("reads DO_BACKFILL as true/false", () => { + vi.stubEnv("DO_BACKFILL", "true"); + expect(configFromEnv().doBackfill).toBe(true); + + vi.stubEnv("DO_BACKFILL", "false"); + expect(configFromEnv().doBackfill).toBe(false); + }); + + test("reads UPDATE_ON_CONFIGURE as true/false", () => { + vi.stubEnv("UPDATE_ON_CONFIGURE", "true"); + expect(configFromEnv().updateOnConfigure).toBe(true); + + vi.stubEnv("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/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/tests/config.test.ts b/kits/speech-to-text/tests/config.test.ts new file mode 100644 index 0000000000..394b1b94d3 --- /dev/null +++ b/kits/speech-to-text/tests/config.test.ts @@ -0,0 +1,87 @@ +/** + * 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`. `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"; +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 boolean select", () => { + expect(declaration("ENABLE_AUTOMATIC_PUNCTUATION")).toEqual({ + type: "boolean", + 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/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index 6729d6545c..7bd0a45a08 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1,3 +1,5 @@ +- 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"]`. - 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 d5549249f5..43af13a5d6 100644 --- a/kits/storage-resize-images/src/config.ts +++ b/kits/storage-resize-images/src/config.ts @@ -83,12 +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, + default: "false", + input: select({ Yes: "true", No: "false" }), }), resizedImagesPath: defineString("RESIZED_IMAGES_PATH", { label: "Cloud Storage path for resized images", @@ -186,7 +196,7 @@ const params = { description: "Keep animation of GIF and WEBP formats.", default: true, - input: select({ True: true, "No (1st frame only)": false }), + input: select({ Yes: true, "No (1st frame only)": false }), }), memory: defineInt("FUNCTION_MEMORY", { label: "Cloud Function memory", @@ -208,6 +218,7 @@ const params = { "Should resized images have a new access token assigned to them, different from the original image?", default: true, + input: select({ Yes: true, No: false }), }), contentFilterLevel: defineString("CONTENT_FILTER_LEVEL", { label: "Content filter level", @@ -294,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 133918c997..60d281dc1e 100644 --- a/kits/storage-resize-images/tests/config.test.ts +++ b/kits/storage-resize-images/tests/config.test.ts @@ -26,6 +26,7 @@ * same variable as a comma-separated string). */ +import { declaredParams } from "firebase-functions/params"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import type { @@ -36,6 +37,19 @@ import type { } from "firebase-functions/params"; import type { ContentFilterLevel } from "../src/export-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 ENV_KEYS = [ "IMG_BUCKET", "IMG_SIZES", @@ -98,6 +112,62 @@ describe("configFromEnv", () => { saved.clear(); }); + test("declares the predecessor's labeled boolean selects", async () => { + await import("../src/config"); + + for (const [name, defaultValue] of [ + ["IS_ANIMATED", true], + ["REGENERATE_TOKEN", true], + ] as const) { + expect(declaration(name)).toEqual({ + type: "boolean", + default: defaultValue, + input: { + select: { + options: [ + { label: "Yes", value: true }, + { + label: name === "IS_ANIMATED" ? "No (1st frame only)" : "No", + value: false, + }, + ], + }, + }, + }); + } + }); + + // 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(); @@ -192,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; @@ -200,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(""); });