fix: more tests more better

This commit is contained in:
2026-02-10 12:58:26 +01:00
parent 9683fb8b9e
commit 4eecf509cd
5 changed files with 1151 additions and 10 deletions

View File

@@ -91,7 +91,8 @@ Additionally we need another importer to traverse a full website and deduct post
and rebuild posts in the database based on such a web traversal. To be able to do that, use copilot SDK and rebuild posts in the database based on such a web traversal. To be able to do that, use copilot SDK
to integrate copilot directly, so that HTML pages can be directly inspected and turned into actual blog to integrate copilot directly, so that HTML pages can be directly inspected and turned into actual blog
posts in proper structure and proper markdown, despite the source being HTML. This is a variant of the posts in proper structure and proper markdown, despite the source being HTML. This is a variant of the
wordpress importer that directly works on already rendered HTML websites. wordpress importer that directly works on already rendered HTML websites. The importer should only stay
within the actual site it was handled, not following any off-site links.
For this AI support during import to work, the blog application needs to provide post management and media For this AI support during import to work, the blog application needs to provide post management and media
management functionality as proper SDK tools to the copilot instance, so that it will be able to work management functionality as proper SDK tools to the copilot instance, so that it will be able to work
@@ -116,6 +117,11 @@ posts from new import runs if the original posts are already there. In the case
the original post will just be linked to the same tag of the new import, so that the user can see it was the original post will just be linked to the same tag of the new import, so that the user can see it was
referenced by multiple imports. referenced by multiple imports.
Essentially my main idea for imports is that the importer is classes that can read websits from different
sources (starting with wordprss backup and HTTP URL) and that each discovered element is handed to the AI
to convert to markdown and in the case of the HTTP URL also separate out posts, then use the tools to
check for duplicates and update tags or create new posts based on the process.
Import runs can be shown in the main panel, so that the user can see what came with what import and can Import runs can be shown in the main panel, so that the user can see what came with what import and can
manage posts and media from imports that way. Migration is the main interesting part of this tool, because manage posts and media from imports that way. Migration is the main interesting part of this tool, because
migrating blogs is hard work and needs to be properly supported. migrating blogs is hard work and needs to be properly supported.

View File

@@ -45,11 +45,37 @@ export class MediaEngine extends EventEmitter {
super(); super();
} }
private getMediaDir(): string { private getMediaBaseDir(): string {
const userDataPath = app.getPath('userData'); const userDataPath = app.getPath('userData');
return path.join(userDataPath, 'projects', this.currentProjectId, 'media'); return path.join(userDataPath, 'projects', this.currentProjectId, 'media');
} }
private getMediaDir(): string {
// Kept for backwards compatibility - returns base media directory
return this.getMediaBaseDir();
}
/**
* Get the date-based directory for media based on its creation date.
* Format: media/YYYY/MM/
*/
private getMediaDirForDate(date: Date): string {
const baseDir = this.getMediaBaseDir();
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
return path.join(baseDir, year, month);
}
/**
* Get the full path for a media file based on id, extension, and date.
* Returns: media/YYYY/MM/{id}.{ext}
*/
getMediaPathForDate(id: string, ext: string, date: Date): string {
const dir = this.getMediaDirForDate(date);
const extension = ext.startsWith('.') ? ext : `.${ext}`;
return path.join(dir, `${id}${extension}`);
}
setProjectContext(projectId: string): void { setProjectContext(projectId: string): void {
this.currentProjectId = projectId; this.currentProjectId = projectId;
} }
@@ -204,12 +230,14 @@ export class MediaEngine extends EventEmitter {
const originalName = path.basename(sourcePath); const originalName = path.basename(sourcePath);
const ext = path.extname(originalName); const ext = path.extname(originalName);
const filename = `${id}${ext}`; const filename = `${id}${ext}`;
const mediaDir = this.getMediaDir();
// Use date-based directory structure (media/YYYY/MM/)
const mediaDir = this.getMediaDirForDate(now);
await fs.mkdir(mediaDir, { recursive: true }); await fs.mkdir(mediaDir, { recursive: true });
const destPath = path.join(mediaDir, filename); const destPath = path.join(mediaDir, filename);
// Copy file to media directory // Copy file to media directory
await fs.writeFile(destPath, sourceBuffer); await fs.copyFile(sourcePath, destPath);
const mediaData: MediaData = { const mediaData: MediaData = {
id, id,

View File

@@ -67,11 +67,36 @@ export class PostEngine extends EventEmitter {
super(); super();
} }
private getPostsDir(): string { private getPostsBaseDir(): string {
const userDataPath = app.getPath('userData'); const userDataPath = app.getPath('userData');
return path.join(userDataPath, 'projects', this.currentProjectId, 'posts'); return path.join(userDataPath, 'projects', this.currentProjectId, 'posts');
} }
private getPostsDir(): string {
// Kept for backwards compatibility - returns base posts directory
return this.getPostsBaseDir();
}
/**
* Get the date-based directory for a post based on its creation date.
* Format: posts/YYYY/MM/
*/
private getPostsDirForDate(date: Date): string {
const baseDir = this.getPostsBaseDir();
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
return path.join(baseDir, year, month);
}
/**
* Get the full path for a post file based on slug and date.
* Returns: posts/YYYY/MM/{slug}.md
*/
getPostPath(slug: string, date: Date): string {
const dir = this.getPostsDirForDate(date);
return path.join(dir, `${slug}.md`);
}
setProjectContext(projectId: string): void { setProjectContext(projectId: string): void {
this.currentProjectId = projectId; this.currentProjectId = projectId;
} }
@@ -109,7 +134,8 @@ export class PostEngine extends EventEmitter {
if (post.author) metadata.author = post.author; if (post.author) metadata.author = post.author;
if (post.publishedAt) metadata.publishedAt = post.publishedAt.toISOString(); if (post.publishedAt) metadata.publishedAt = post.publishedAt.toISOString();
const postsDir = this.getPostsDir(); // Use date-based directory structure (posts/YYYY/MM/)
const postsDir = this.getPostsDirForDate(post.createdAt);
await fs.mkdir(postsDir, { recursive: true }); await fs.mkdir(postsDir, { recursive: true });
const fileContent = matter.stringify(post.content, metadata); const fileContent = matter.stringify(post.content, metadata);

View File

@@ -243,11 +243,11 @@ describe('MediaEngine', () => {
expect(fs.mkdir).toHaveBeenCalled(); expect(fs.mkdir).toHaveBeenCalled();
}); });
it('should write media file to destination', async () => { it('should copy media file to destination', async () => {
const fs = await import('fs/promises'); const fs = await import('fs/promises');
await mediaEngine.importMedia('/source/image.jpg'); await mediaEngine.importMedia('/source/image.jpg');
expect(fs.writeFile).toHaveBeenCalled(); expect(fs.copyFile).toHaveBeenCalled();
}); });
it('should insert media record into database', async () => { it('should insert media record into database', async () => {
@@ -260,8 +260,9 @@ describe('MediaEngine', () => {
const fs = await import('fs/promises'); const fs = await import('fs/promises');
await mediaEngine.importMedia('/source/image.jpg'); await mediaEngine.importMedia('/source/image.jpg');
// Should write both the media file and the sidecar file // Should copy the media file and write the sidecar file
expect(vi.mocked(fs.writeFile).mock.calls.length).toBeGreaterThanOrEqual(2); expect(vi.mocked(fs.copyFile).mock.calls.length).toBeGreaterThanOrEqual(1);
expect(vi.mocked(fs.writeFile).mock.calls.length).toBeGreaterThanOrEqual(1);
}); });
}); });
@@ -448,4 +449,74 @@ describe('MediaEngine', () => {
expect(media.caption).toBeUndefined(); expect(media.caption).toBeUndefined();
}); });
}); });
describe('Date-based folder structure', () => {
beforeEach(() => {
mockFiles.set('/source/dated-image.jpg', Buffer.from('image-data'));
});
it('should store media in YYYY/MM folder based on createdAt date', async () => {
const fs = await import('fs/promises');
const media = await mediaEngine.importMedia('/source/dated-image.jpg');
const copyCall = vi.mocked(fs.copyFile).mock.calls[0];
expect(copyCall).toBeDefined();
const destPath = copyCall[1] as string;
const year = media.createdAt.getFullYear();
const month = (media.createdAt.getMonth() + 1).toString().padStart(2, '0');
// Path should contain YYYY/MM structure (handle both / and \ separators)
expect(destPath).toMatch(new RegExp(`[/\\\\]${year}[/\\\\]${month}[/\\\\]`));
});
it('should create nested year/month directories on media import', async () => {
const fs = await import('fs/promises');
await mediaEngine.importMedia('/source/dated-image.jpg');
// mkdir should be called with recursive: true
expect(fs.mkdir).toHaveBeenCalled();
const mkdirCalls = vi.mocked(fs.mkdir).mock.calls;
// Should have created directory containing year/month structure
const yearMonthDirCall = mkdirCalls.find((call) => {
const dirPath = call[0] as string;
return dirPath.match(/[/\\]\d{4}[/\\]\d{2}$/);
});
expect(yearMonthDirCall).toBeDefined();
});
it('should return correct path via getMediaPath method', async () => {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const mediaPath = mediaEngine.getMediaPathForDate('test-uuid', 'jpg', now);
// Handle both Windows (\) and Unix (/) path separators
expect(mediaPath).toMatch(new RegExp(`[/\\\\]${year}[/\\\\]${month}[/\\\\]`));
expect(mediaPath).toContain('test-uuid.jpg');
});
it('should handle media from previous years correctly', async () => {
const oldDate = new Date('2021-06-20');
const mediaPath = mediaEngine.getMediaPathForDate('old-id', 'png', oldDate);
expect(mediaPath).toMatch(/[/\\]2021[/\\]06[/\\]/);
expect(mediaPath).toContain('old-id.png');
});
it('should use zero-padded month numbers (01-12)', async () => {
const january = new Date('2024-01-15');
const december = new Date('2024-12-15');
const januaryPath = mediaEngine.getMediaPathForDate('jan-id', 'jpg', january);
const decemberPath = mediaEngine.getMediaPathForDate('dec-id', 'jpg', december);
expect(januaryPath).toMatch(/[/\\]2024[/\\]01[/\\]/);
expect(decemberPath).toMatch(/[/\\]2024[/\\]12[/\\]/);
});
});
}); });

File diff suppressed because it is too large Load Diff