From f1b70ef369892b9a43026674bdd144bb9859fc0a Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 2 Jul 2026 10:06:08 +0100 Subject: [PATCH] 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 | 17 +++++++++++ .../functions/package.json | 1 + .../functions/test-config/tfjs-node.stub.js | 28 +++++++++++++++++++ 4 files changed, 63 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..0be6aec15 --- /dev/null +++ b/storage-reverse-image-search/functions/jest.integration.config.js @@ -0,0 +1,17 @@ +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 = { + ...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 = {};