fix: allow reinstall of AI bundles with broken model files (#214)

This commit is contained in:
SnapOtter
2026-06-09 23:07:38 +08:00
parent 3196ba63d5
commit 27a56c774b
2 changed files with 43 additions and 2 deletions
+8 -2
View File
@@ -27,6 +27,7 @@ import {
acquireInstallLock, acquireInstallLock,
getAiDir, getAiDir,
getFeatureStates, getFeatureStates,
getInstallScriptPath,
getManifestPath, getManifestPath,
getModelsDir, getModelsDir,
invalidateCache, invalidateCache,
@@ -35,6 +36,7 @@ import {
markUninstalled, markUninstalled,
releaseInstallLock, releaseInstallLock,
setInstallProgress, setInstallProgress,
verifyBundleModels,
} from "../lib/feature-status.js"; } from "../lib/feature-status.js";
import { requirePermission } from "../permissions.js"; import { requirePermission } from "../permissions.js";
import { requireAuth } from "../plugins/auth.js"; import { requireAuth } from "../plugins/auth.js";
@@ -137,7 +139,11 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise<void>
} }
if (isFeatureInstalled(bundleId)) { if (isFeatureInstalled(bundleId)) {
return reply.status(409).send({ error: `Bundle "${bundleId}" is already installed` }); const modelError = verifyBundleModels(bundleId);
if (!modelError) {
return reply.status(409).send({ error: `Bundle "${bundleId}" is already installed` });
}
markUninstalled(bundleId);
} }
if (!acquireInstallLock(bundleId)) { if (!acquireInstallLock(bundleId)) {
@@ -145,7 +151,7 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise<void>
} }
const jobId = crypto.randomUUID(); const jobId = crypto.randomUUID();
const scriptPath = "/app/packages/ai/python/install_feature.py"; const scriptPath = getInstallScriptPath();
const manifestPath = getManifestPath(); const manifestPath = getManifestPath();
const modelsDir = getModelsDir(); const modelsDir = getModelsDir();
@@ -525,6 +525,41 @@ describe("Composite state - getFeatureStates", () => {
}); });
}); });
describe("auto-repair state transition (install endpoint logic)", () => {
it("markUninstalled clears stale entry when models are broken, allowing reinstall", () => {
mod.markInstalled("background-removal", "1.0.0", ["u2net.onnx"]);
writeTestManifest({
"background-removal": {
models: [{ id: "u2net", path: "u2net.onnx" }],
},
});
mod.invalidateCache();
expect(mod.isFeatureInstalled("background-removal")).toBe(true);
const modelError = mod.verifyBundleModels("background-removal");
expect(modelError).not.toBeNull();
mod.markUninstalled("background-removal");
expect(mod.isFeatureInstalled("background-removal")).toBe(false);
});
it("does not clear entry when models are healthy", () => {
mod.markInstalled("background-removal", "1.0.0", ["u2net.onnx"]);
writeTestManifest({
"background-removal": {
models: [{ id: "u2net", path: "u2net.onnx" }],
},
});
writeFileSync(join(modelsDir, "u2net.onnx"), Buffer.alloc(1024));
mod.invalidateCache();
expect(mod.isFeatureInstalled("background-removal")).toBe(true);
const modelError = mod.verifyBundleModels("background-removal");
expect(modelError).toBeNull();
expect(mod.isFeatureInstalled("background-removal")).toBe(true);
});
});
describe("verifyBundleModels", () => { describe("verifyBundleModels", () => {
it("returns null when all models exist and meet minSize", () => { it("returns null when all models exist and meet minSize", () => {
writeTestManifest({ writeTestManifest({