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:
@@ -388,6 +388,16 @@ describe("Feature status queries", () => {
|
||||
expect(mod.isToolInstalled("passport-photo")).toBe(true);
|
||||
});
|
||||
|
||||
it("isToolInstalled is false for enhance-faces when only upscale-enhance is installed", () => {
|
||||
mod.markInstalled("upscale-enhance", "1.0.0", []);
|
||||
expect(mod.isToolInstalled("enhance-faces")).toBe(false);
|
||||
});
|
||||
|
||||
it("getFirstMissingBundleForTool names face-detection for enhance-faces when only upscale-enhance is installed", () => {
|
||||
mod.markInstalled("upscale-enhance", "1.0.0", []);
|
||||
expect(mod.getFirstMissingBundleForTool("enhance-faces")).toBe("face-detection");
|
||||
});
|
||||
|
||||
it("getFirstMissingBundleForTool names face-detection when only background-removal is installed", () => {
|
||||
mod.markInstalled("background-removal", "1.0.0", []);
|
||||
expect(mod.getFirstMissingBundleForTool("passport-photo")).toBe("face-detection");
|
||||
@@ -559,6 +569,23 @@ describe("Crash recovery - recoverInterruptedInstalls", () => {
|
||||
expect(existsSync(lockPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("consumes a surviving venv.writing breadcrumb (interrupted venv write)", () => {
|
||||
const marker = join(aiDir, "venv.writing");
|
||||
writeFileSync(
|
||||
marker,
|
||||
JSON.stringify({ bundleId: "ocr", startedAt: "2026-01-01T00:00:00.000Z" }),
|
||||
);
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
expect(() => mod.recoverInterruptedInstalls()).not.toThrow();
|
||||
|
||||
// The breadcrumb is always consumed so it can't retrigger recovery forever.
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
// And the interruption is surfaced in the logs.
|
||||
expect(warn.mock.calls.flat().join(" ")).toMatch(/interrupted|venv/i);
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it("handles missing directories gracefully", async () => {
|
||||
vi.resetModules();
|
||||
const emptyTemp = mkdtempSync(join(tmpdir(), "snapotter-empty-"));
|
||||
|
||||
Reference in New Issue
Block a user