check / check (push) Waiting to run
`quak files` with a collection ID used to wait for a full refresh, which on an empty cache fetched the files of every collection in the account. It now reads the collection list, then that one collection's files through the new `Library.freshFiles()`, and stores nothing. The files print in the server's order, and no refresh progress lines appear. Each file prints once, as the latest copy the server sent. `quak files` without a collection ID still waits for a refresh. Some tests ran `quak files` with a collection ID to fill the cache, to show a refresh's progress or to show what `-q` hides. They now run a command that still refreshes. Model: opus-5-5
243 lines
8.6 KiB
TypeScript
243 lines
8.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. `get`, `get-thumb` and `files`
|
|
* without a collection ID force a `Library.fresh()` round-trip before they
|
|
* read. The stand-in library below serves no cached record 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). `files` with a collection ID asks for
|
|
* that list, then for that one collection's files (`freshFiles()`, issue
|
|
* #217). Neither may wait for a refresh, which would fetch the files of every
|
|
* collection.
|
|
*
|
|
* `collections` and `files` also list in the order the library gives them, not
|
|
* the albums/photos projection's newest-first order. The fixtures are seeded in
|
|
* an 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 cached records only after `fresh()` has been
|
|
// awaited, and serves them in the enumeration order it was given.
|
|
// `freshCollections()` and `freshFiles()` answer in that order without a
|
|
// refresh. `photos.byID` returns a stand-in `Photo` carrying just the fileID
|
|
// the helper passes through.
|
|
class FakeLibrary implements FreshReadLibrary {
|
|
freshCalls = 0;
|
|
freshCollectionsCalls = 0;
|
|
// The ID of each collection `freshFiles()` was asked for.
|
|
freshFilesCalls: 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 freshFiles(collection: Collection): Promise<EnteFile[]> {
|
|
this.freshFilesCalls.push(collection.id);
|
|
return this.files.filter((f) => f.collectionID === collection.id);
|
|
}
|
|
|
|
listCollections(): Collection[] {
|
|
return this.refreshed ? this.collections : [];
|
|
}
|
|
|
|
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 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 with a collection ID reads that collection's files alone and keeps their order and raw fields", async () => {
|
|
// Given in the order 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),
|
|
file(20, 2, 1_700_000_000_000_003),
|
|
];
|
|
const lib = new FakeLibrary(
|
|
[collection(1, 100), collection(2, 200)],
|
|
files,
|
|
);
|
|
|
|
const rows = await freshFiles(lib, 1);
|
|
expect(lib.freshCalls).toBe(0);
|
|
expect(lib.freshCollectionsCalls).toBe(1);
|
|
expect(lib.freshFilesCalls).toEqual([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,
|
|
},
|
|
]);
|
|
});
|
|
|
|
// 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.freshCalls).toBe(0);
|
|
expect(lib.freshFilesCalls).toEqual([]);
|
|
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();
|
|
});
|
|
});
|