check / check (push) Waiting to run
`quak files COLLECTION` used to wait for a full refresh, which on an empty cache fetched the files of every collection before it listed one. It now reads the collection list, then calls the new `Library.refreshCollection()`, which brings that one collection up to date in the store as a refresh would, and saves it. It sends no request for the files when the stored copy is current. It leaves the refresh cursor alone, so the next refresh still fetches every other collection that changed. `refreshOnce` and `refreshCollection` share the commit step. `quak files` with no argument still waits for a refresh. Model: opus-5-5
248 lines
8.8 KiB
TypeScript
248 lines
8.8 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. `files` without a collection ID,
|
|
* `get` and `get-thumb` force a `Library.fresh()` round-trip before they read;
|
|
* 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` asks the library for the collection list alone
|
|
* (`freshCollections()`, issue #210), and must not wait for a refresh, which
|
|
* would fetch the files of every collection. `files COLLECTION` reads that list
|
|
* too and then brings that one collection up to date
|
|
* (`refreshCollection()`, issue #217); the stand-in serves a collection's files
|
|
* once either has run.
|
|
*
|
|
* `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. `freshCollections()`
|
|
// answers the collection list in that order without a refresh.
|
|
// `refreshCollection()` reveals the files of that one collection.
|
|
// `photos.byID` returns a stand-in `Photo` carrying just the fileID the helper
|
|
// passes through.
|
|
class FakeLibrary implements FreshReadLibrary {
|
|
freshCalls = 0;
|
|
freshCollectionsCalls = 0;
|
|
refreshCollectionIDs: number[] = [];
|
|
private refreshed = false;
|
|
|
|
constructor(
|
|
private readonly collections: Collection[],
|
|
private readonly files: EnteFile[],
|
|
) {}
|
|
|
|
async fresh(): Promise<unknown> {
|
|
this.freshCalls++;
|
|
this.refreshed = true;
|
|
return {};
|
|
}
|
|
|
|
async freshCollections(): Promise<Collection[]> {
|
|
this.freshCollectionsCalls++;
|
|
return this.collections;
|
|
}
|
|
|
|
async refreshCollection(collection: Collection): Promise<void> {
|
|
this.refreshCollectionIDs.push(collection.id);
|
|
}
|
|
|
|
listCollections(): Collection[] {
|
|
return this.refreshed ? this.collections : [];
|
|
}
|
|
|
|
listFiles(collectionID: number): EnteFile[] {
|
|
const current =
|
|
this.refreshed || this.refreshCollectionIDs.includes(collectionID);
|
|
return current
|
|
? 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 reads the collection list alone, 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.freshCollectionsCalls).toBe(1);
|
|
expect(lib.freshCalls).toBe(0);
|
|
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 brings the collection alone up to date, 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. Collection 2's file is not
|
|
// listed.
|
|
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),
|
|
file(20, 2, 1_700_000_000_000_003),
|
|
];
|
|
const lib = new FakeLibrary(
|
|
[collection(1, 100), collection(2, 100)],
|
|
files,
|
|
);
|
|
|
|
const rows = await freshFiles(lib, 1);
|
|
expect(lib.freshCollectionsCalls).toBe(1);
|
|
expect(lib.refreshCollectionIDs).toEqual([1]);
|
|
expect(lib.freshCalls).toBe(0);
|
|
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,
|
|
},
|
|
]);
|
|
});
|
|
|
|
// Without a collection ID, `files` lists every file in the account once
|
|
// (issue #211). File 11 is in collections 1 and 2; collection 2 is
|
|
// enumerated first, so 11 is listed once, with collection 2's record.
|
|
it("freshFiles without a collection ID lists every file once, from the first collection holding it", async () => {
|
|
const files = [
|
|
file(10, 1, 1_700_000_000_000_000),
|
|
file(11, 1, 1_700_000_000_000_001),
|
|
file(20, 2, 1_700_000_000_000_002),
|
|
file(11, 2, 1_700_000_000_000_001),
|
|
];
|
|
const lib = new FakeLibrary(
|
|
[collection(2, 100), collection(1, 200)],
|
|
files,
|
|
);
|
|
|
|
const rows = await freshFiles(lib);
|
|
expect(lib.freshCalls).toBe(1);
|
|
expect(rows?.map((f) => [f.id, f.collectionID])).toEqual([
|
|
[20, 2],
|
|
[11, 2],
|
|
[10, 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.freshCollectionsCalls).toBe(1);
|
|
expect(lib.refreshCollectionIDs).toEqual([]);
|
|
expect(lib.freshCalls).toBe(0);
|
|
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();
|
|
});
|
|
});
|