fix: more tests more better
This commit is contained in:
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -45,11 +45,37 @@ export class MediaEngine extends EventEmitter {
|
||||
super();
|
||||
}
|
||||
|
||||
private getMediaDir(): string {
|
||||
private getMediaBaseDir(): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
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 {
|
||||
this.currentProjectId = projectId;
|
||||
}
|
||||
@@ -204,12 +230,14 @@ export class MediaEngine extends EventEmitter {
|
||||
const originalName = path.basename(sourcePath);
|
||||
const ext = path.extname(originalName);
|
||||
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 });
|
||||
const destPath = path.join(mediaDir, filename);
|
||||
|
||||
// Copy file to media directory
|
||||
await fs.writeFile(destPath, sourceBuffer);
|
||||
await fs.copyFile(sourcePath, destPath);
|
||||
|
||||
const mediaData: MediaData = {
|
||||
id,
|
||||
|
||||
@@ -67,11 +67,36 @@ export class PostEngine extends EventEmitter {
|
||||
super();
|
||||
}
|
||||
|
||||
private getPostsDir(): string {
|
||||
private getPostsBaseDir(): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
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 {
|
||||
this.currentProjectId = projectId;
|
||||
}
|
||||
@@ -109,7 +134,8 @@ export class PostEngine extends EventEmitter {
|
||||
if (post.author) metadata.author = post.author;
|
||||
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 });
|
||||
|
||||
const fileContent = matter.stringify(post.content, metadata);
|
||||
|
||||
@@ -243,11 +243,11 @@ describe('MediaEngine', () => {
|
||||
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');
|
||||
await mediaEngine.importMedia('/source/image.jpg');
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalled();
|
||||
expect(fs.copyFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should insert media record into database', async () => {
|
||||
@@ -260,8 +260,9 @@ describe('MediaEngine', () => {
|
||||
const fs = await import('fs/promises');
|
||||
await mediaEngine.importMedia('/source/image.jpg');
|
||||
|
||||
// Should write both the media file and the sidecar file
|
||||
expect(vi.mocked(fs.writeFile).mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
// Should copy the media file and write the sidecar file
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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
Reference in New Issue
Block a user