From bf4d55effc9893814378bce08d22b140759fe743 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 2 Jul 2026 09:56:51 +0100 Subject: [PATCH 1/2] fix(firestore-semantic-search): stop native tfjs addon breaking tests in CI Unit test suites failed to run in CI with 'tfjs_binding.node can not be found'. The cause is a require-time dependency: several suites transitively import use_embeddings.ts, which calls require('@tensorflow/tfjs-node'). That package loads a platform-specific native addon at import time, but CI installs dependencies with 'npm ci --ignore-scripts', so tfjs-node's postinstall never downloads the addon. Whether a run passed depended on stale cached node_modules, making it flaky and CI-only. Mock @tensorflow/tfjs-node and the Universal Sentence Encoder via jest moduleNameMapper so unit tests never load the native binary. The only tests that exercise real embeddings are already skipped (they require live model access), so no coverage is lost. Verified against the emulator with the native addon removed: all suites now run and pass instead of failing to load. --- .../universal-sentence-encoder.js | 37 +++++++++++++++++++ .../__mocks__/@tensorflow/tfjs-node.js | 22 +++++++++++ .../functions/jest.config.js | 9 +++++ 3 files changed, 68 insertions(+) create mode 100644 firestore-semantic-search/functions/__mocks__/@tensorflow-models/universal-sentence-encoder.js create mode 100644 firestore-semantic-search/functions/__mocks__/@tensorflow/tfjs-node.js diff --git a/firestore-semantic-search/functions/__mocks__/@tensorflow-models/universal-sentence-encoder.js b/firestore-semantic-search/functions/__mocks__/@tensorflow-models/universal-sentence-encoder.js new file mode 100644 index 000000000..e9447344a --- /dev/null +++ b/firestore-semantic-search/functions/__mocks__/@tensorflow-models/universal-sentence-encoder.js @@ -0,0 +1,37 @@ +/** + * Copyright 2023 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. + */ + +// Stub for the Universal Sentence Encoder model. It transitively depends on the +// native @tensorflow/tfjs-node addon, so it is mocked for unit tests. load() +// returns a model whose embed() yields a deterministic zero vector; tests that +// assert on real embedding values are integration tests and remain skipped. +const DIMENSIONS = 512; + +async function load() { + return { + embed: async input => { + const rows = Array.isArray(input) ? input.length : 1; + const vectors = Array.from({length: rows}, () => + new Array(DIMENSIONS).fill(0) + ); + return { + arraySync: () => vectors, + }; + }, + }; +} + +module.exports = {load}; diff --git a/firestore-semantic-search/functions/__mocks__/@tensorflow/tfjs-node.js b/firestore-semantic-search/functions/__mocks__/@tensorflow/tfjs-node.js new file mode 100644 index 000000000..22c2d9c96 --- /dev/null +++ b/firestore-semantic-search/functions/__mocks__/@tensorflow/tfjs-node.js @@ -0,0 +1,22 @@ +/** + * Copyright 2023 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. + */ + +// Stub for the @tensorflow/tfjs-node native module. The real package loads a +// platform-specific native addon (tfjs_binding.node) at require time, which is +// not available in CI where dependencies are installed with --ignore-scripts. +// Unit tests never exercise the model itself, so an empty stub is sufficient; +// tests that need real embeddings are integration tests and remain skipped. +module.exports = {}; diff --git a/firestore-semantic-search/functions/jest.config.js b/firestore-semantic-search/functions/jest.config.js index 59fe75809..8222b76b7 100644 --- a/firestore-semantic-search/functions/jest.config.js +++ b/firestore-semantic-search/functions/jest.config.js @@ -4,6 +4,15 @@ module.exports = { testEnvironment: 'node', collectCoverage: true, collectCoverageFrom: ['src/**/*.ts', '!src/functions/cleanup.ts'], + moduleNameMapper: { + // Redirect the TensorFlow packages to lightweight stubs. The real + // @tensorflow/tfjs-node loads a native addon at require time that is absent + // in CI (deps installed with --ignore-scripts), which caused unrelated + // suites to fail to run. See __mocks__/@tensorflow/. + '^@tensorflow/tfjs-node$': '/__mocks__/@tensorflow/tfjs-node.js', + '^@tensorflow-models/universal-sentence-encoder$': + '/__mocks__/@tensorflow-models/universal-sentence-encoder.js', + }, transform: { '^.+\\.ts$': [ 'ts-jest', From 815038895dd8b9879e448c2ae65f62e4a453b7dc Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 2 Jul 2026 10:06:08 +0100 Subject: [PATCH 2/2] fix(storage-reverse-image-search): stop native tfjs addon breaking tests in CI Test suites failed to run in CI with 'tfjs_binding.node can not be found'. Same root cause as firestore-semantic-search: suites transitively import feature_vectors.ts, which imports @tensorflow/tfjs-node, and that package loads a platform-specific native addon at import time. CI installs dependencies with 'npm ci --ignore-scripts', so tfjs-node's postinstall never downloads the addon, so any suite touching feature_vectors fails to load. Whether a run passed depended on stale cached node_modules, making it flaky and CI-only. Split unit and integration tests: - Unit run (default 'test' script) maps @tensorflow/tfjs-node to a lightweight stub via moduleNameMapper, so the many suites that only mock or never invoke feature_vectors stop crashing at import time. - The two specs that genuinely exercise TensorFlow (feature_vectors.test.ts and feature_vectors.memory.test.ts load a real model over the network and assert on real tensors) move to jest.integration.config.js, run via 'npm run test:integration'. They need the native addon built and network access, so they do not belong in the --ignore-scripts CI job. The stub lives outside a __mocks__ directory on purpose: manual mocks for node_modules packages are auto-applied by jest and cannot be disabled per config, which would also break the integration run. Wiring it through moduleNameMapper keeps it scoped to the unit config. Verified with the native addon removed and the emulator running: the unit run previously had 10 suites fail to load; it now runs 15 suites green with 0 failures. The integration config resolves the real tfjs package. Fixes #355. --- .../functions/jest.config.js | 17 +++++++++++ .../functions/jest.integration.config.js | 16 +++++++++++ .../functions/package.json | 1 + .../functions/test-config/tfjs-node.stub.js | 28 +++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 storage-reverse-image-search/functions/jest.integration.config.js create mode 100644 storage-reverse-image-search/functions/test-config/tfjs-node.stub.js diff --git a/storage-reverse-image-search/functions/jest.config.js b/storage-reverse-image-search/functions/jest.config.js index 9e5ab9250..3482a82a7 100644 --- a/storage-reverse-image-search/functions/jest.config.js +++ b/storage-reverse-image-search/functions/jest.config.js @@ -1,10 +1,27 @@ module.exports = { preset: 'ts-jest', testMatch: ['**/*.test.ts'], + // The feature_vectors unit specs load a real TensorFlow model over the + // network and assert on real tensors, so they require the native + // @tensorflow/tfjs-node addon. That addon is not present in CI (dependencies + // are installed with --ignore-scripts), so they belong to the integration + // run (jest.integration.config.js), not the default unit run. + testPathIgnorePatterns: [ + '/node_modules/', + '__tests__/unit/feature_vectors\\.test\\.ts$', + '__tests__/unit/feature_vectors\\.memory\\.test\\.ts$', + ], testEnvironment: 'node', collectCoverage: true, collectCoverageFrom: ['src/**/*.ts', '!src/functions/cleanup.ts'], setupFilesAfterEnv: ['/__tests__/setup.ts'], + moduleNameMapper: { + // Redirect @tensorflow/tfjs-node to a lightweight stub. The real package + // loads a native addon at require time that is absent in CI, which caused + // every suite transitively importing feature_vectors.ts to fail to run. + // See test-config/tfjs-node.stub.js. + '^@tensorflow/tfjs-node$': '/test-config/tfjs-node.stub.js', + }, transform: { '^.+\\.ts$': [ 'ts-jest', diff --git a/storage-reverse-image-search/functions/jest.integration.config.js b/storage-reverse-image-search/functions/jest.integration.config.js new file mode 100644 index 000000000..d49f929f0 --- /dev/null +++ b/storage-reverse-image-search/functions/jest.integration.config.js @@ -0,0 +1,16 @@ +const base = require('./jest.config'); + +// Integration config for the specs that exercise the real TensorFlow model +// (native @tensorflow/tfjs-node addon plus a network model download). These are +// intentionally excluded from the default `test` run because CI installs with +// --ignore-scripts and has no addon. Run locally with `npm run test:integration` +// after the native addon has been built. +module.exports = Object.assign({}, base, { + // Use the real @tensorflow/tfjs-node, not the unit stub. + moduleNameMapper: {}, + testPathIgnorePatterns: ['/node_modules/'], + testMatch: [ + '**/__tests__/unit/feature_vectors.test.ts', + '**/__tests__/unit/feature_vectors.memory.test.ts', + ], +}); diff --git a/storage-reverse-image-search/functions/package.json b/storage-reverse-image-search/functions/package.json index 710eecb7d..b2b9724d4 100644 --- a/storage-reverse-image-search/functions/package.json +++ b/storage-reverse-image-search/functions/package.json @@ -7,6 +7,7 @@ "build:watch": "tsc --watch", "mocha": "mocha '**/*.spec.ts'", "test": "jest", + "test:integration": "jest --config jest.integration.config.js", "generate-readme": "firebase ext:info .. --markdown > ../README.md", "publish-from-main": "firebase ext:dev:upload googlecloud/storage-reverse-image-search --repo=https://github.com/googlecloudplatform/firebase-extensions --root=storage-reverse-image-search --ref=main --project pub-ext-gcloud" }, diff --git a/storage-reverse-image-search/functions/test-config/tfjs-node.stub.js b/storage-reverse-image-search/functions/test-config/tfjs-node.stub.js new file mode 100644 index 000000000..59a465292 --- /dev/null +++ b/storage-reverse-image-search/functions/test-config/tfjs-node.stub.js @@ -0,0 +1,28 @@ +/** + * Copyright 2023 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. + */ + +// Stub for @tensorflow/tfjs-node, wired in only by jest.config.js via +// moduleNameMapper. The real package loads a native addon (tfjs_binding.node) +// at require time, which is absent in CI (dependencies installed with +// --ignore-scripts). This caused every unit suite transitively importing +// feature_vectors.ts to fail to run. +// +// This file deliberately lives outside a __mocks__ directory: manual mocks for +// node_modules packages are applied automatically by jest and cannot be opted +// out per-config, which would also break the integration run. Wiring it through +// moduleNameMapper keeps it scoped to the unit config, so +// jest.integration.config.js uses the real package. +module.exports = {};