Feature/python api image discovery (#34)
* Expose chat.analyzeMediaImage in Python API for batch image metadata generation * Fix updateMedia losing linkedPostIds by reading existing sidecar before overwriting * Also preserve author from sidecar when DB value is null (data drift) * Extend MetadataDiffEngine to cover media, scripts, and templates * Redesign MetadataDiffPanel: item-first view with field pills, filtering, and per-item multi-field diffs * Fix task:progress startsWith crash (taskId not id) and nested button violation in field pills * Populate field diffs for file-missing items and show fileMissing badge in UI * feat: extended meta diff * feat: meta diff als reconstructs orphans * chore: updated documentation --------- Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
@@ -1090,6 +1090,114 @@ tags: ["nature", "sunset"]`;
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve linkedPostIds from existing sidecar when updating metadata', async () => {
|
||||
const fs = await import('fs/promises');
|
||||
const filePath = '/mock/media/linked.jpg';
|
||||
const sidecarPath = `${filePath}.meta`;
|
||||
|
||||
// Pre-populate sidecar with linkedPostIds
|
||||
mockFiles.set(normalizePath(sidecarPath), `---
|
||||
id: linked-media-id
|
||||
originalName: "linked.jpg"
|
||||
mimeType: image/jpeg
|
||||
size: 1024
|
||||
createdAt: 2026-01-01T00:00:00.000Z
|
||||
updatedAt: 2026-01-01T00:00:00.000Z
|
||||
author: "Original Author"
|
||||
tags: ["nature", "photo"]
|
||||
linkedPostIds: ["post-1", "post-2"]`);
|
||||
|
||||
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||
const chain = createSelectChain();
|
||||
chain.where = vi.fn().mockReturnValue({
|
||||
...chain,
|
||||
get: vi.fn().mockResolvedValue({
|
||||
id: 'linked-media-id',
|
||||
projectId: 'default',
|
||||
originalName: 'linked.jpg',
|
||||
mimeType: 'image/jpeg',
|
||||
size: 1024,
|
||||
filePath,
|
||||
title: 'Old title',
|
||||
author: 'Original Author',
|
||||
tags: '["nature", "photo"]',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}),
|
||||
});
|
||||
return chain;
|
||||
});
|
||||
|
||||
vi.mocked(fs.writeFile).mockClear();
|
||||
await mediaEngine.updateMedia('linked-media-id', { title: 'New title' });
|
||||
|
||||
// Verify sidecar was written
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
sidecarPath,
|
||||
expect.any(String),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
// The written sidecar content must preserve linkedPostIds AND author
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls.find(
|
||||
c => normalizePath(c[0] as string) === normalizePath(sidecarPath)
|
||||
)?.[1] as string;
|
||||
|
||||
expect(writtenContent).toContain('linkedPostIds: ["post-1", "post-2"]');
|
||||
expect(writtenContent).toContain('author: "Original Author"');
|
||||
expect(writtenContent).toContain('title: "New title"');
|
||||
});
|
||||
|
||||
it('should preserve author from sidecar when DB has null author', async () => {
|
||||
const fs = await import('fs/promises');
|
||||
const filePath = '/mock/media/author-drift.jpg';
|
||||
const sidecarPath = `${filePath}.meta`;
|
||||
|
||||
// Sidecar has author but DB does not (data drift)
|
||||
mockFiles.set(normalizePath(sidecarPath), `---
|
||||
id: author-drift-id
|
||||
originalName: "author-drift.jpg"
|
||||
mimeType: image/jpeg
|
||||
size: 2048
|
||||
createdAt: 2026-01-01T00:00:00.000Z
|
||||
updatedAt: 2026-01-01T00:00:00.000Z
|
||||
author: "hugo"
|
||||
tags: []
|
||||
linkedPostIds: ["post-x"]`);
|
||||
|
||||
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||
const chain = createSelectChain();
|
||||
chain.where = vi.fn().mockReturnValue({
|
||||
...chain,
|
||||
get: vi.fn().mockResolvedValue({
|
||||
id: 'author-drift-id',
|
||||
projectId: 'default',
|
||||
originalName: 'author-drift.jpg',
|
||||
mimeType: 'image/jpeg',
|
||||
size: 2048,
|
||||
filePath,
|
||||
title: 'Old title',
|
||||
author: null, // DB has null, sidecar has "hugo"
|
||||
tags: '[]',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}),
|
||||
});
|
||||
return chain;
|
||||
});
|
||||
|
||||
vi.mocked(fs.writeFile).mockClear();
|
||||
await mediaEngine.updateMedia('author-drift-id', { alt: 'New alt text' });
|
||||
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls.find(
|
||||
c => normalizePath(c[0] as string) === normalizePath(sidecarPath)
|
||||
)?.[1] as string;
|
||||
|
||||
expect(writtenContent).toContain('author: "hugo"');
|
||||
expect(writtenContent).toContain('alt: "New alt text"');
|
||||
expect(writtenContent).toContain('linkedPostIds: ["post-x"]');
|
||||
});
|
||||
|
||||
it('should update FTS index', async () => {
|
||||
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||
const chain = createSelectChain();
|
||||
|
||||
Reference in New Issue
Block a user