mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(passport-photo): require the face-detection bundle, not just background-removal (#329)
* fix(passport-photo): require the face-detection bundle, not just background-removal Passport Photo runs face-landmark detection (face_landmarks.py, gated to the face-detection bundle) before background removal (background-removal bundle), but it was only declared under and guarded against background-removal. A user who installed only Background Removal passed every JS-side check, then hit a late "feature_not_installed" from the Python dispatcher gate when the analyze step ran face landmarks, and the UI never told them Face Detection was needed. - shared: add TOOL_EXTRA_BUNDLES + getRequiredBundlesForTool so a tool can declare more than one required bundle (passport-photo needs background-removal and face-detection). enablesTools is untouched, so the one-tool-per-bundle invariant still holds. - api: isToolInstalled() now checks every required bundle; add getFirstMissingBundleForTool() so the analyze and base routes, pipeline (both guards) and batch report the bundle the user actually still needs. - web: the proactive install prompt (tool-page) and features-store treat a tool as installed only when all required bundles are present, and point the prompt at the first missing one (sequential install, no new UI). Refs #327 * test(passport-photo): deterministic integration coverage for the two-bundle guard Boots the real API with an isolated DATA_DIR and controls installed.json to prove the HTTP route behavior end-to-end: - nothing installed -> 501 naming background-removal - only background-removal installed -> 501 naming face-detection (issue #327) - both installed -> guard passes (not 501) - base route reports face-detection too Refs #327
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
FEATURE_BUNDLES,
|
||||
getBundleForTool,
|
||||
getRequiredBundlesForTool,
|
||||
getToolsForBundle,
|
||||
PYTHON_SIDECAR_TOOLS,
|
||||
TOOL_BUNDLE_MAP,
|
||||
TOOL_EXTRA_BUNDLES,
|
||||
} from "@snapotter/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -100,3 +102,51 @@ describe("Feature bundle edge cases", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRequiredBundlesForTool", () => {
|
||||
it("returns the primary bundle for a single-bundle tool", () => {
|
||||
expect(getRequiredBundlesForTool("remove-background")).toEqual(["background-removal"]);
|
||||
});
|
||||
|
||||
it("returns [] for non-AI tools", () => {
|
||||
expect(getRequiredBundlesForTool("resize")).toEqual([]);
|
||||
expect(getRequiredBundlesForTool("nonexistent-tool")).toEqual([]);
|
||||
});
|
||||
|
||||
it("includes the primary bundle plus extras for cross-bundle tools", () => {
|
||||
// Passport Photo runs face-landmark detection (face-detection) on top of
|
||||
// background removal (its primary bundle).
|
||||
expect(getRequiredBundlesForTool("passport-photo")).toEqual([
|
||||
"background-removal",
|
||||
"face-detection",
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists the primary bundle first", () => {
|
||||
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
||||
const required = getRequiredBundlesForTool(toolId);
|
||||
if (required.length > 0) {
|
||||
expect(required[0]).toBe(TOOL_BUNDLE_MAP[toolId]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("never lists a bundle twice", () => {
|
||||
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
||||
const required = getRequiredBundlesForTool(toolId);
|
||||
expect(new Set(required).size).toBe(required.length);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("TOOL_EXTRA_BUNDLES", () => {
|
||||
it("references only real bundles and never the tool's own primary bundle", () => {
|
||||
for (const [toolId, extras] of Object.entries(TOOL_EXTRA_BUNDLES)) {
|
||||
const primary = TOOL_BUNDLE_MAP[toolId];
|
||||
for (const bundleId of extras) {
|
||||
expect(FEATURE_BUNDLES[bundleId], `Unknown extra bundle ${bundleId}`).toBeDefined();
|
||||
expect(bundleId, `${toolId} lists its primary bundle as an extra`).not.toBe(primary);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user