check / check (push) Successful in 27s
Route every command through Library.open instead of scanning the client. The read commands (collections, files, get, get-thumb) force a server round-trip with Library.fresh() before reading, so they answer for current state, not a stale cache (owner amendment, issue #36). collections and files list in the library's enumeration order — the order the pre-library CLI printed, not the newest-first projection — and present each file from its own decrypted metadata (raw title, microsecond creationTime). get/get-thumb copy the cached original/thumbnail to --out. backup, backup-metadata, and the thumbnail helpers are unchanged. A new global --cache-dir sets the cache location; point commands open with the background precache off so a one-shot command never downloads the account. Also addresses #17: fix-missing-thumbnails reports a non-JPEG image or a video as skipped (unsupported), distinct from failed, and only a genuine failure exits non-zero. Model: opus-4-8
196 lines
6.6 KiB
TypeScript
196 lines
6.6 KiB
TypeScript
/**
|
|
* Tests for the CLI read helpers (`src/cli-read.ts`, owner amendment to
|
|
* issue #36, issue #52).
|
|
*
|
|
* The `collections`, `files`, `get`, and `get-thumb` commands must answer for
|
|
* current server state, not the local cache, so each helper forces a
|
|
* `Library.fresh()` round-trip before it reads. The stand-in library below
|
|
* serves nothing until `fresh()` has been awaited, so a helper that read
|
|
* without refreshing would come back empty and fail here.
|
|
*
|
|
* `collections` and `files` also list in the library's enumeration order
|
|
* (`listCollections`/`listFiles`) — the order the pre-library CLI printed — not
|
|
* the albums/photos projection's newest-first order. The fixtures are seeded in
|
|
* an enumeration order that a newest-first sort would rearrange, so a
|
|
* regression to the projection order would fail here too. Field values still
|
|
* come from the raw metadata via `cli-output.ts`.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
freshCollections,
|
|
freshFiles,
|
|
freshFile,
|
|
type FreshReadLibrary,
|
|
} from "../../src/cli-read.js";
|
|
import { fileListRow } from "../../src/cli-output.js";
|
|
import type { Photo } from "../../src/library/index.js";
|
|
import type { Collection, EnteFile } from "../../src/model/types.js";
|
|
|
|
const collection = (id: number, updationTime: number): Collection => ({
|
|
id,
|
|
ownerID: 42,
|
|
key: new Uint8Array(),
|
|
name: `album-${id}`,
|
|
type: "album",
|
|
updationTime,
|
|
isShared: false,
|
|
});
|
|
|
|
// Microseconds, as Ente stores times.
|
|
const file = (
|
|
id: number,
|
|
collectionID: number,
|
|
creationTime: number,
|
|
): EnteFile => ({
|
|
id,
|
|
collectionID,
|
|
ownerID: 42,
|
|
key: new Uint8Array(),
|
|
metadata: {
|
|
title: `file-${id}.jpg`,
|
|
fileType: "image",
|
|
creationTime,
|
|
modificationTime: creationTime,
|
|
},
|
|
file: { decryptionHeader: "" },
|
|
thumbnail: { decryptionHeader: "" },
|
|
updationTime: creationTime,
|
|
});
|
|
|
|
// A library that reveals its records only after `fresh()` has been awaited, and
|
|
// serves them in the enumeration order it was given. `photos.byID` returns a
|
|
// stand-in `Photo` carrying just the fileID the helper passes through.
|
|
class FakeLibrary implements FreshReadLibrary {
|
|
freshCalls = 0;
|
|
private refreshed = false;
|
|
|
|
constructor(
|
|
private readonly collections: Collection[],
|
|
private readonly files: EnteFile[],
|
|
) {}
|
|
|
|
async fresh(): Promise<unknown> {
|
|
this.freshCalls++;
|
|
this.refreshed = true;
|
|
return {};
|
|
}
|
|
|
|
listCollections(): Collection[] {
|
|
return this.refreshed ? this.collections : [];
|
|
}
|
|
|
|
getCollection(id: number): Collection | undefined {
|
|
return this.listCollections().find((c) => c.id === id);
|
|
}
|
|
|
|
listFiles(collectionID: number): EnteFile[] {
|
|
return this.refreshed
|
|
? this.files.filter((f) => f.collectionID === collectionID)
|
|
: [];
|
|
}
|
|
|
|
getFileByID(fileID: number): EnteFile | undefined {
|
|
if (!this.refreshed) return undefined;
|
|
return this.files.find((f) => f.id === fileID);
|
|
}
|
|
|
|
photos = {
|
|
byID: ({ fileID }: { fileID: number }): Photo | undefined => {
|
|
if (!this.refreshed) return undefined;
|
|
if (!this.files.some((f) => f.id === fileID)) return undefined;
|
|
return { fileID } as unknown as Photo;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("CLI read helpers (issue #36 amendment, issue #52)", () => {
|
|
it("freshCollections refreshes first, then lists in enumeration order", async () => {
|
|
// Enumeration order 2, 1, 3; a newest-first sort would be 3, 2, 1.
|
|
const lib = new FakeLibrary(
|
|
[collection(2, 200), collection(1, 300), collection(3, 100)],
|
|
[],
|
|
);
|
|
const rows = await freshCollections(lib);
|
|
|
|
expect(lib.freshCalls).toBe(1);
|
|
expect(rows.map((c) => c.id)).toEqual([2, 1, 3]);
|
|
// The projection's newest-first order is a different sequence, so this
|
|
// is not accidentally that order.
|
|
const newestFirst = [...rows]
|
|
.sort((a, b) => b.updationTime - a.updationTime)
|
|
.map((c) => c.id);
|
|
expect(newestFirst).toEqual([1, 2, 3]);
|
|
expect(rows.map((c) => c.id)).not.toEqual(newestFirst);
|
|
});
|
|
|
|
it("freshFiles refreshes first, lists in enumeration order, keeps raw fields", async () => {
|
|
// Enumeration order by id 10, 11, 12; creationTimes ascending, so a
|
|
// newest-first sort would reverse them.
|
|
const files = [
|
|
file(10, 1, 1_700_000_000_000_000),
|
|
file(11, 1, 1_700_000_000_000_001),
|
|
file(12, 1, 1_700_000_000_000_002),
|
|
];
|
|
const lib = new FakeLibrary([collection(1, 100)], files);
|
|
|
|
const rows = await freshFiles(lib, 1);
|
|
expect(lib.freshCalls).toBe(1);
|
|
expect(rows?.map((f) => f.id)).toEqual([10, 11, 12]);
|
|
|
|
// Field values come from raw metadata: microsecond creationTime and the
|
|
// raw title, unchanged.
|
|
expect(rows?.map(fileListRow)).toEqual([
|
|
{
|
|
id: 10,
|
|
title: "file-10.jpg",
|
|
fileType: "image",
|
|
creationTime: 1_700_000_000_000_000,
|
|
collectionID: 1,
|
|
},
|
|
{
|
|
id: 11,
|
|
title: "file-11.jpg",
|
|
fileType: "image",
|
|
creationTime: 1_700_000_000_000_001,
|
|
collectionID: 1,
|
|
},
|
|
{
|
|
id: 12,
|
|
title: "file-12.jpg",
|
|
fileType: "image",
|
|
creationTime: 1_700_000_000_000_002,
|
|
collectionID: 1,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("freshFiles returns undefined for an unknown collection", async () => {
|
|
const lib = new FakeLibrary([collection(1, 100)], []);
|
|
const rows = await freshFiles(lib, 999);
|
|
expect(lib.freshCalls).toBe(1);
|
|
expect(rows).toBeUndefined();
|
|
});
|
|
|
|
it("freshFile refreshes first, then resolves the photo and its raw record", async () => {
|
|
const f = file(10, 1, 1_700_000_000_000_000);
|
|
const lib = new FakeLibrary([collection(1, 100)], [f]);
|
|
|
|
const resolved = await freshFile(lib, 10);
|
|
expect(lib.freshCalls).toBe(1);
|
|
expect(resolved?.photo.fileID).toBe(10);
|
|
expect(resolved?.file.metadata.title).toBe("file-10.jpg");
|
|
expect(resolved?.file.metadata.creationTime).toBe(
|
|
1_700_000_000_000_000,
|
|
);
|
|
});
|
|
|
|
it("freshFile returns undefined for an unknown file", async () => {
|
|
const lib = new FakeLibrary([collection(1, 100)], []);
|
|
const resolved = await freshFile(lib, 404);
|
|
expect(lib.freshCalls).toBe(1);
|
|
expect(resolved).toBeUndefined();
|
|
});
|
|
});
|