Files
TREK/server/tests/unit/nest/memories.helpers.test.ts
T
Maurice 2d7889c237 fix(memories): give the photo picker a chronological order it can rely on
The picker got its photos back in whatever order the provider happened to
return them. It groups by day and appends pages lazily, so an unordered page
lands under the wrong heading and pushes photos out of the fold the reader
is looking at.

Immich now asks for order: desc on both the search and the album path. An
older server that does not know the property drops it silently rather than
failing, so each page is sorted again here; the album paths drain every page
before sorting and are covered outright. getAssetInfo reports mediaType, the
same mapping the listing already used, so the detail route and the picker
agree on what an asset is.

Synology gets no sort parameter upstream on either path. Search.Search
documents none, and _fetchSynologyJson maps every app code except 106/107/119
onto a 400, so a parameter one DSM build rejects would take listing down
entirely for that user. Both paths read every page anyway, so ordering the
mapped result costs nothing and cannot fail.

No media type for Synology yet, deliberately. streamSynologyAsset serves
every asset as a JPEG out of SYNO.Foto.Thumbnail, original included; there is
no video byte path. Labelling an asset a video would make the lightbox mount
a player on a still image, which is worse than the missing badge. The badge,
the byte path and the persisted media_type belong together in their own
change.

Five album cases in the Immich integration suite pinned the old arrival
order. Where the position was incidental they now look the asset up by id;
where the order is the point they state the chronological one.
2026-09-08 15:24:21 +02:00

81 lines
3.0 KiB
TypeScript

/**
* The provider-agnostic ordering the memories domain hands to the picker.
*
* Both provider services ask upstream for a descending order now, but neither
* Immich nor Synology guarantees it across versions, and the album paths do not
* run through a sorted search at all. This is the fallback that makes the day
* headings in the picker mean something, so it is pinned here rather than
* inside either provider suite.
*/
import { describe, it, expect } from 'vitest';
import { sortAssetsByTakenAtDesc } from '../../../src/nest/memories/memories.helpers';
const asset = (id: string, takenAt?: string | null) => ({ id, takenAt });
const ids = (assets: { id: string }[]) => assets.map(a => a.id);
describe('sortAssetsByTakenAtDesc', () => {
it('MEM-SORT-001: puts the newest capture first regardless of the order upstream sent', () => {
const out = sortAssetsByTakenAtDesc([
asset('middle', '2026-03-15T09:00:00Z'),
asset('oldest', '2026-03-01T09:00:00Z'),
asset('newest', '2026-03-31T09:00:00Z'),
]);
expect(ids(out)).toEqual(['newest', 'middle', 'oldest']);
});
it('MEM-SORT-002: keeps the upstream order between assets sharing a timestamp', () => {
// Burst shots land on the same second often enough that an unstable sort
// would reshuffle them on every page load.
const out = sortAssetsByTakenAtDesc([
asset('first', '2026-03-15T09:00:00Z'),
asset('second', '2026-03-15T09:00:00Z'),
asset('third', '2026-03-15T09:00:00Z'),
]);
expect(ids(out)).toEqual(['first', 'second', 'third']);
});
it('MEM-SORT-003: sends assets without a usable timestamp to the end, in order', () => {
const out = sortAssetsByTakenAtDesc([
asset('no-date'),
asset('dated', '2026-03-15T09:00:00Z'),
asset('null-date', null),
asset('empty-date', ''),
]);
expect(ids(out)).toEqual(['dated', 'no-date', 'null-date', 'empty-date']);
});
it('MEM-SORT-004: treats an unparsable timestamp as missing rather than sorting on the string', () => {
// Synology hands back an epoch it converts itself; a malformed value must not
// outrank a real date just because it compares high as text.
const out = sortAssetsByTakenAtDesc([
asset('garbage', 'not-a-date'),
asset('real', '2026-03-15T09:00:00Z'),
]);
expect(ids(out)).toEqual(['real', 'garbage']);
});
it('MEM-SORT-005: leaves an empty list alone and does not mutate its input', () => {
expect(sortAssetsByTakenAtDesc([])).toEqual([]);
const input = [asset('a', '2026-01-01T00:00:00Z'), asset('b', '2026-02-01T00:00:00Z')];
sortAssetsByTakenAtDesc(input);
expect(ids(input)).toEqual(['a', 'b']);
});
it('MEM-SORT-006: compares across timezone offsets by instant, not by text', () => {
// 23:00Z is later than 09:00-06:00 (15:00Z) even though the string sorts lower.
const out = sortAssetsByTakenAtDesc([
asset('offset', '2026-03-15T09:00:00-06:00'),
asset('utc', '2026-03-15T23:00:00Z'),
]);
expect(ids(out)).toEqual(['utc', 'offset']);
});
});