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
|
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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user