ensure generated resume is valid for rxresu.me by using their schema

This commit is contained in:
DaKheera47 2026-01-21 15:18:34 +00:00
parent 6a3a25578a
commit 9363a58e46
3 changed files with 1048 additions and 222 deletions

View File

@ -0,0 +1,159 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { generatePdf } from './pdf.js';
// Define mock data in hoisted block
const { mocks, mockProfile } = vi.hoisted(() => {
const profile = {
sections: {
summary: { content: 'Original Summary' },
skills: {
items: [
{ id: 's1', name: 'Existing Skill', visible: true, description: 'Existing Desc', level: 3, keywords: ['k1'] }
]
},
projects: { items: [] }
},
basics: { headline: 'Original Headline' }
};
return {
mockProfile: profile,
mocks: {
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn().mockResolvedValue(undefined),
access: vi.fn().mockResolvedValue(undefined),
unlink: vi.fn().mockResolvedValue(undefined),
}
};
});
// Configure base mock implementations
mocks.readFile.mockResolvedValue(JSON.stringify(mockProfile));
mocks.writeFile.mockResolvedValue(undefined);
vi.mock('fs/promises', async () => {
return {
default: mocks,
...mocks
};
});
vi.mock('fs', () => ({
existsSync: vi.fn().mockReturnValue(true),
default: { existsSync: vi.fn().mockReturnValue(true) }
}));
vi.mock('../repositories/settings.js', () => ({
getSetting: vi.fn().mockResolvedValue(null),
}));
vi.mock('./projectSelection.js', () => ({
pickProjectIdsForJob: vi.fn().mockResolvedValue([]),
}));
vi.mock('./resumeProjects.js', () => ({
extractProjectsFromProfile: vi.fn().mockReturnValue({ catalog: [], selectionItems: [] }),
resolveResumeProjectsSettings: vi.fn().mockReturnValue({
resumeProjects: { lockedProjectIds: [], aiSelectableProjectIds: [], maxProjects: 2 }
})
}));
vi.mock('child_process', () => ({
spawn: vi.fn().mockImplementation(() => ({
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn().mockImplementation((event, cb) => {
if (event === 'close') cb(0);
return {};
}),
})),
default: {
spawn: vi.fn().mockImplementation(() => ({
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn().mockImplementation((event, cb) => {
if (event === 'close') cb(0);
return {};
}),
}))
}
}));
describe('PDF Service Skills Validation', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.readFile.mockResolvedValue(JSON.stringify(mockProfile));
});
it('should add required schema fields (visible, description) to new skills', async () => {
// AI often returns just name and keywords
const newSkills = [
{ name: 'New Skill', keywords: ['k2'] },
{ name: 'Existing Skill', keywords: ['k3', 'k4'] } // Should merge with s1
];
const tailoredContent = { skills: newSkills };
await generatePdf('job-skills-1', tailoredContent, 'Job Desc');
expect(mocks.writeFile).toHaveBeenCalled();
const callArgs = mocks.writeFile.mock.calls[0];
const savedResumeJson = JSON.parse(callArgs[1] as string);
const skillItems = savedResumeJson.sections.skills.items;
// Check "New Skill"
const newSkill = skillItems.find((s: any) => s.name === 'New Skill');
expect(newSkill).toBeDefined();
// These are the validations failing in user report:
expect(newSkill.visible).toBe(true); // Should default to true
expect(typeof newSkill.description).toBe('string'); // Should default to ""
expect(newSkill.description).toBe('');
// Optional but good to check
expect(newSkill.id).toBeDefined();
expect(newSkill.level).toBe(1);
// Check "Existing Skill" - should preserve existing fields if not overwritten?
// In the implementation, we look up existing.
// existing.visible => true, existing.description => 'Existing Desc', existing.level => 3
const existingSkill = skillItems.find((s: any) => s.name === 'Existing Skill');
expect(existingSkill.visible).toBe(true);
expect(existingSkill.description).toBe('Existing Desc');
expect(existingSkill.level).toBe(3);
expect(existingSkill.keywords).toEqual(['k3', 'k4']); // Should use new keywords or existing? Implementation uses new || existing.
});
it('should sanitize base resume even if no skills are tailored', async () => {
// Mock profile has an invalid skill (missing visible/description in the raw json implied,
// though our mock above has them. Let's make a truly invalid one locally)
const invalidProfile = {
...mockProfile,
sections: {
...mockProfile.sections,
skills: {
items: [
{ name: 'Invalid Skill' } // Missing visible, description, id, level
]
}
}
};
mocks.readFile.mockResolvedValueOnce(JSON.stringify(invalidProfile));
// No tailoring, pass dummy path to bypass getProfile cache and use readFile mock
await generatePdf('job-no-tailor', {}, 'Job Desc', 'dummy.json');
expect(mocks.writeFile).toHaveBeenCalled();
const callArgs = mocks.writeFile.mock.calls[0];
const savedResumeJson = JSON.parse(callArgs[1] as string);
const item = savedResumeJson.sections.skills.items[0];
// Ensure defaults are applied even if we didn't use the tailoring logic block
expect(item.visible).toBe(true);
expect(item.description).toBe('');
expect(item.id).toBeDefined();
});
});

View File

@ -64,6 +64,20 @@ export async function generatePdf(
? JSON.parse(await readFile(baseResumePath, 'utf-8'))
: JSON.parse(JSON.stringify(await getProfile())); // Deep copy from cache
// Sanitize skills: Ensure all skills have required schema fields (visible, description, id, level, keywords)
// This fixes issues where the base JSON uses a shorthand format (missing required fields)
if (baseResume.sections?.skills?.items && Array.isArray(baseResume.sections.skills.items)) {
baseResume.sections.skills.items = baseResume.sections.skills.items.map((skill: any, index: number) => ({
...skill,
id: skill.id || `skill-${Date.now()}-${index}`,
visible: skill.visible ?? true,
// Zod schema requires string, default to empty string if missing
description: skill.description ?? '',
level: skill.level ?? 1,
keywords: skill.keywords || [],
}));
}
// Inject tailored summary
if (tailoredContent.summary) {
if (baseResume.sections?.summary) {
@ -91,7 +105,23 @@ export async function generatePdf(
: null;
if (newSkills && baseResume.sections?.skills) {
baseResume.sections.skills.items = newSkills;
// Ensure each skill item has required schema fields
const existingSkills = baseResume.sections.skills.items || [];
const skillsWithSchema = newSkills.map((newSkill: any, index: number) => {
// Try to find matching existing skill to preserve id and other fields
const existing = existingSkills.find((s: any) => s.name === newSkill.name);
return {
id: newSkill.id || existing?.id || `skill-${Date.now()}-${index}`,
visible: newSkill.visible !== undefined ? newSkill.visible : (existing?.visible ?? true),
name: newSkill.name || existing?.name || '',
description: newSkill.description !== undefined ? newSkill.description : (existing?.description || ''),
level: newSkill.level !== undefined ? newSkill.level : (existing?.level ?? 1),
keywords: newSkill.keywords || existing?.keywords || [],
};
});
baseResume.sections.skills.items = skillsWithSchema;
}
}

File diff suppressed because it is too large Load Diff