backup-metadata: keep going when an ML data request fails (closes #101)
check / check (push) Successful in 26s
check / check (push) Successful in 26s
Each ML data request of up to 200 files is now tried on its own. A request that still fails after its retries is logged, its files are written with the reason in `mlDataError`, and the command exits 1 once the dump is complete. `fetchMLData`, used only here, is removed in favour of a per-batch loop over `fetchMLDataBatch`. Model: opus-5-5
This commit is contained in:
@@ -38,7 +38,7 @@ import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import sodium from "libsodium-wrappers-sumo";
|
||||
import { SRP, SrpServer } from "fast-srp-hap";
|
||||
import { beforeAll, afterAll, describe, expect, it } from "vitest";
|
||||
import { beforeAll, afterAll, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
init,
|
||||
toBase64,
|
||||
@@ -53,8 +53,16 @@ import {
|
||||
runMetadataBackup,
|
||||
type MetadataBackupOptions,
|
||||
} from "../../src/metadata-backup.js";
|
||||
import { backupMetadataCommand } from "../../src/cli-commands.js";
|
||||
import type { KeyAttributes } from "../../src/auth/types.js";
|
||||
|
||||
// One file per ML data request, so the two files of the mock account are
|
||||
// fetched in two requests and one of them can fail on its own.
|
||||
vi.mock("../../src/mldata-fetch.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../src/mldata-fetch.js")>()),
|
||||
MLDATA_BATCH_SIZE: 1,
|
||||
}));
|
||||
|
||||
const TEST_EMAIL = "metabackup@example.com";
|
||||
const TEST_PASSWORD = "metapass";
|
||||
const TEST_OPS = 2;
|
||||
@@ -347,7 +355,8 @@ const buildMetaMock = async (): Promise<MetaMockState> => {
|
||||
};
|
||||
};
|
||||
|
||||
const buildMetaFetch = (m: MetaMockState) => {
|
||||
// `failMLDataFor`: answer 500 to every ML data request that asks for this file.
|
||||
const buildMetaFetch = (m: MetaMockState, failMLDataFor?: number) => {
|
||||
let srpServer: SrpServer;
|
||||
return (async (
|
||||
input: RequestInfo | URL,
|
||||
@@ -403,6 +412,8 @@ const buildMetaFetch = (m: MetaMockState) => {
|
||||
}
|
||||
if (path === "/files/data/fetch") {
|
||||
const body = JSON.parse(init?.body as string);
|
||||
if ((body.fileIDs as number[]).includes(failMLDataFor!))
|
||||
return new Response("server error", { status: 500 });
|
||||
const data = (body.fileIDs as number[])
|
||||
.filter((id: number) => m.encryptedMLData[id])
|
||||
.map((id: number) => ({
|
||||
@@ -636,3 +647,63 @@ describe("quak backup-metadata", () => {
|
||||
expect(failedMeta.imageMetadataError).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
describe("quak backup-metadata when an ML data request fails", () => {
|
||||
// Run the CLI command against the mock and return its exit code, stderr
|
||||
// and output directory.
|
||||
const runCommand = async (failMLDataFor?: number) => {
|
||||
const client = await Client.login({
|
||||
email: TEST_EMAIL,
|
||||
password: TEST_PASSWORD,
|
||||
apiOptions: {
|
||||
fetch: buildMetaFetch(mock, failMLDataFor),
|
||||
retry: { sleep: async () => {} },
|
||||
},
|
||||
});
|
||||
const outDir = mkdtempSync(join(testDir, "ml-fail-"));
|
||||
let stderr = "";
|
||||
const code = await backupMetadataCommand(
|
||||
{
|
||||
stdout: { write: () => true },
|
||||
stderr: { write: (text: string) => (stderr += text) },
|
||||
sessionDir: testDir,
|
||||
cacheDir: mkdtempSync(join(testDir, "cache-")),
|
||||
loadSession: () => client,
|
||||
},
|
||||
outDir,
|
||||
{},
|
||||
);
|
||||
return { code, stderr, outDir };
|
||||
};
|
||||
|
||||
it("writes every file, marks the failed batch's files, and exits 1", async () => {
|
||||
const { code, stderr, outDir } = await runCommand(200);
|
||||
|
||||
expect(code).toBe(1);
|
||||
expect(stderr).toContain("ML data request for 1 file(s) failed");
|
||||
|
||||
const ok = JSON.parse(
|
||||
readFileSync(
|
||||
join(outDir, "collections", "10-Vacation", "100.json"),
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
expect(ok.mlData.clip.embedding).toEqual([0.5, 0.6, 0.7]);
|
||||
expect(ok.mlDataError).toBeUndefined();
|
||||
|
||||
const failed = JSON.parse(
|
||||
readFileSync(
|
||||
join(outDir, "collections", "20-__Work", "200.json"),
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
expect(failed.metadata.title).toBe("diagram.png");
|
||||
expect(failed.mlData).toBeUndefined();
|
||||
expect(failed.mlDataError).toContain("500");
|
||||
});
|
||||
|
||||
it("exits 0 when every ML data request succeeds", async () => {
|
||||
const { code } = await runCommand();
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user