ML data that does not decrypt is recorded with its reason (closes #198)
check / check (push) Canceled after 0s

`fetchMLDataBatch` dropped an entry that did not decrypt, gunzip or parse,
so the file looked like one with no ML data and the run exited 0. It now
returns the reason beside the payloads, also for ML data that is not a
JSON object. The library gives the reasons from its last finished ML data
fetch in `status().mlDataErrors` and the `fetchMLData` "done" event.
`quak backup` writes the reason as `mlDataError` for a file with no ML
data cached and records the file in `failures.json`. `backup-metadata`
writes `mlDataError` for it and exits 1; `runMetadataBackup` returns
`filesWithMLDataError`.

Judgement call: a file whose ML data is cached keeps it in its JSON when a
newer copy does not decode, as when the fetch fails.

Model: opus-5-5
This commit is contained in:
2026-10-16 04:16:06 +00:00
parent 960a7ce8a6
commit 129c648680
13 changed files with 473 additions and 84 deletions
+51 -6
View File
@@ -489,15 +489,15 @@ describe("quak backup-metadata", () => {
});
});
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) => {
describe("quak backup-metadata when a file's ML data cannot be read", () => {
// Run the CLI command against `fetch`, the mock unless given, and return
// its exit code, stderr and output directory.
const runCommand = async (fetch = buildMetaFetch(mock)) => {
const client = await Client.login({
email: TEST_EMAIL,
password: TEST_PASSWORD,
apiOptions: {
fetch: buildMetaFetch(mock, failMLDataFor),
fetch,
retry: { sleep: async () => {} },
},
});
@@ -518,7 +518,9 @@ describe("quak backup-metadata when an ML data request fails", () => {
};
it("writes every file, marks the failed batch's files, and exits 1", async () => {
const { code, stderr, outDir } = await runCommand(200);
const { code, stderr, outDir } = await runCommand(
buildMetaFetch(mock, 200),
);
expect(code).toBe(1);
expect(stderr).toContain("ML data request for 1 file(s) failed");
@@ -543,6 +545,49 @@ describe("quak backup-metadata when an ML data request fails", () => {
expect(failed.mlDataError).toContain("500");
});
// The server sends ML data for diagram.png encrypted under a key that is
// not the file's, so quak cannot decrypt it.
it("writes the reason for ML data that does not decrypt, and exits 1", async () => {
const otherKey = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const { header, ciphertext } = encryptBlob(
new Uint8Array(gzipSync(Buffer.from("{}"))),
otherKey,
);
const { code, stderr, outDir } = await runCommand(
buildMetaFetch({
...mock,
encryptedMLData: {
...mock.encryptedMLData,
200: {
encryptedData: toBase64(ciphertext),
decryptionHeader: toBase64(header),
},
},
}),
);
expect(code).toBe(1);
const reason = "secretstream chunk authentication failed";
expect(stderr).toContain(
`ML data of file 200 could not be read: ${reason}\n`,
);
const failed = JSON.parse(
readFileSync(
join(outDir, "collections", "20-__Work", "200.json"),
"utf-8",
),
);
expect(failed.mlData).toBeUndefined();
expect(failed.mlDataError).toBe(reason);
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]);
});
it("exits 0 when every ML data request succeeds", async () => {
const { code } = await runCommand();
expect(code).toBe(0);