mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: reliable, self-healing AI feature-bundle installs (#472)
Make on-demand AI feature-bundle installs reliable and self-healing, closing the failure modes behind most "some tool doesn't work" reports. Multi-bundle installs: tools needing more than one bundle (Passport Photo, Enhance Faces) install every required bundle from one action and stay not-installed until all are present. Verified across all 19 AI tools. Downloads: self-heal the accelerated Hugging Face (Xet) client so an upgraded venv no longer silently falls back to slow urllib; restart instead of corrupting a resumed partial when a proxy ignores Range and returns 200; verify the completed size; fail fast on disk-full and HTTP 4xx; retry transient errors five times; add hf_transfer fallback and document Xet egress. Install integrity: crash-atomic venv writes so a killed or out-of-space install can no longer tear the shared venv and break other tools; a boot breadcrumb reseeds a torn venv to a clean state automatically; a post-install smoke import test refuses to record a bundle whose libraries cannot load; an install watchdog stops a wedged installer that would otherwise hold the venv writer lock forever. Adds unit and end-to-end tests for every failure mode above.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
// @vitest-environment jsdom
|
||||
import type { FeatureBundleState } from "@snapotter/shared";
|
||||
import { TOOLS } from "@snapotter/shared";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ToolCard } from "@/components/common/tool-card";
|
||||
import { useFeaturesStore } from "@/stores/features-store";
|
||||
import { usePinnedToolsStore } from "@/stores/pinned-tools-store";
|
||||
|
||||
// Make the store's optimistic persistence a no-op so the test stays server-free.
|
||||
@@ -14,6 +16,23 @@ vi.mock("@/lib/api", () => ({
|
||||
|
||||
const resize = TOOLS.find((tool) => tool.id === "resize");
|
||||
if (!resize) throw new Error("resize tool missing from TOOLS");
|
||||
const passportPhoto = TOOLS.find((tool) => tool.id === "passport-photo");
|
||||
if (!passportPhoto) throw new Error("passport-photo tool missing from TOOLS");
|
||||
|
||||
function makeBundleState(overrides: Partial<FeatureBundleState> = {}): FeatureBundleState {
|
||||
return {
|
||||
id: "background-removal",
|
||||
name: "Background Removal",
|
||||
description: "Remove backgrounds",
|
||||
status: "not_installed",
|
||||
installedVersion: null,
|
||||
estimatedSize: "4-5 GB",
|
||||
enablesTools: ["remove-background", "passport-photo"],
|
||||
progress: null,
|
||||
error: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -24,6 +43,16 @@ beforeEach(() => {
|
||||
loaded: true,
|
||||
loadError: false,
|
||||
});
|
||||
useFeaturesStore.setState({
|
||||
bundles: [],
|
||||
loaded: true,
|
||||
loadError: false,
|
||||
installing: {},
|
||||
errors: {},
|
||||
queued: [],
|
||||
installAllActive: false,
|
||||
startTimes: {},
|
||||
});
|
||||
});
|
||||
|
||||
function renderCard(showPin: boolean) {
|
||||
@@ -56,3 +85,28 @@ describe("ToolCard pin button", () => {
|
||||
expect(usePinnedToolsStore.getState().pinnedTools).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolCard AI bundle status", () => {
|
||||
it("treats multi-bundle tools as not installed when an extra bundle is missing", () => {
|
||||
useFeaturesStore.setState({
|
||||
bundles: [
|
||||
makeBundleState({ id: "background-removal", status: "installed" }),
|
||||
makeBundleState({
|
||||
id: "face-detection",
|
||||
name: "Face Detection",
|
||||
status: "not_installed",
|
||||
enablesTools: ["blur-faces", "red-eye-removal", "smart-crop"],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<ToolCard tool={passportPhoto} variant="descriptive" />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// One SVG is the tool icon, the second is the missing-AI-bundle indicator.
|
||||
expect(container.querySelectorAll("svg")).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user