Failed runs end with one fatal line; failure lines are errors
check / check (push) Waiting to run

A command that exits 1 because files or albums failed now ends with one
fatal line saying how many failed and where to look, so -q never leaves
a non-zero exit unexplained: backup points to failures.json, backup-metadata
to mlDataError in each file's JSON, and fix-missing-thumbnails to its
Failed files list. The progress callbacks of the backup, the metadata
backup and the thumbnail helpers take a level, so every line that reports
a failure is logged at error. The library's operation names are shown in
plain words. The README says which lines are error and warning, and which
summary lines --json replaces.

Model: opus-5-5
This commit is contained in:
2026-10-18 19:15:16 +00:00
parent f9969b2525
commit d87a5e7ef5
12 changed files with 253 additions and 63 deletions
+60
View File
@@ -913,6 +913,38 @@ describe("backup", () => {
);
});
// A failed file is an error line both where the backup fails it and in
// the list at the end. The last line is fatal, so `-q`, which prints
// fatal lines only, still says why the backup exits 1.
it("logs a failed file at error, and ends with one fatal line counting the failures", async () => {
const dir = join(root, "backup");
const last = `quak: 1 file(s) and 0 album(s) failed; see ${join(dir, "failures.json")} and the FAILED lines\n`;
const ctx = context(fakeClient({ failID: 101 }));
expect(
await backupCommand(
{ ...ctx, log: stderrLog(stderr, "error") },
dir,
{},
),
).toBe(1);
expect(stderr.text).toBe(
"FAILED original sunset.jpg: HTTP 500 from server\n" +
"\nFailed files:\n" +
" [Vacation] sunset.jpg (id 101): HTTP 500 from server\n" +
last,
);
stderr.text = "";
expect(
await backupCommand(
{ ...ctx, log: stderrLog(stderr, "fatal") },
dir,
{ json: true },
),
).toBe(1);
expect(stderr.text).toBe(last);
});
// `--json` shapes stdout only: the log lines stay on stderr, and the JSON
// takes the place of the summary.
it("prints the result as JSON with --json, still exiting 1 on a failure", async () => {
@@ -1303,6 +1335,34 @@ describe("helper list-missing-thumbnails", () => {
});
});
describe("helper fix-missing-thumbnails", () => {
// File 999 is in no album, so fixing it fails. The list of failed files
// is at error, and the last line is fatal, so `-q`, which prints fatal
// lines only, still says why the command exits 1.
it("lists a failed file at error, and ends with one fatal line counting the failures", async () => {
const last = `quak: 1 file(s) failed; see the "Failed files:" list, or the JSON with --json\n`;
const ctx = { ...context(), log: stderrLog(stderr, "error") };
expect(await fixMissingThumbnailsCommand(ctx, { file: ["999"] })).toBe(
1,
);
expect(stderr.text).toBe(
"\nFailed files:\n" +
" 999\tunknown\tfile not found in any collection\n" +
last,
);
stderr.text = "";
const quiet = { ...context(), log: stderrLog(stderr, "fatal") };
expect(
await fixMissingThumbnailsCommand(quiet, {
file: ["999"],
json: true,
}),
).toBe(1);
expect(stderr.text).toBe(last);
});
});
describe("backup-metadata --exif", () => {
// Runs the command and returns what it printed to stderr.
const backupMetadata = async (opts: { exif?: boolean; all?: boolean }) => {