P0-1/P0-2/P1-1: dimensions form + E2E tests + PDF export

P0-1 Backend: dimensions column on template entity + validation
P0-1 Frontend: dimensions edit UI in TemplateManager
P0-2: routeAfterGrading unit tests (10 cases), service spec fix + certificate tests, jest-e2e.json
P1-1: proper PDF generation with embedded CJK font via pdf-lib low-level API
This commit is contained in:
Developer
2026-05-19 08:42:03 +08:00
parent 0b0a060967
commit 68371922ca
18 changed files with 663 additions and 72 deletions
@@ -0,0 +1,95 @@
import { routeAfterGrading } from './builder';
describe('routeAfterGrading', () => {
it('should route to interviewer when shouldFollowUp is true (overrides all other logic)', () => {
const result = routeAfterGrading({
shouldFollowUp: true,
currentQuestionIndex: 0,
questionCount: 5,
questions: [],
} as any);
expect(result).toBe('interviewer');
});
it('should route to generator when currentIndex >= questionsLen and currentIndex < targetCount', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: 3,
questionCount: 5,
questions: [{ text: 'q1' }, { text: 'q2' }, { text: 'q3' }],
} as any);
expect(result).toBe('generator');
});
it('should route to interviewer when currentIndex < questionsLen and currentIndex < targetCount', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: 2,
questionCount: 5,
questions: [{ text: 'q1' }, { text: 'q2' }, { text: 'q3' }, { text: 'q4' }, { text: 'q5' }],
} as any);
expect(result).toBe('interviewer');
});
it('should route to analyzer when currentIndex >= targetCount', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: 5,
questionCount: 5,
questions: [{ text: 'q1' }, { text: 'q2' }, { text: 'q3' }, { text: 'q4' }, { text: 'q5' }],
} as any);
expect(result).toBe('analyzer');
});
it('should use default targetCount of 5 when questionCount is undefined', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: 4,
questions: [{ text: 'q1' }, { text: 'q2' }, { text: 'q3' }, { text: 'q4' }],
} as any);
expect(result).toBe('generator');
});
it('should use default targetCount of 5 when questionCount is undefined and index 5 routes to analyzer', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: 5,
questions: [{ text: 'q1' }, { text: 'q2' }, { text: 'q3' }, { text: 'q4' }, { text: 'q5' }],
} as any);
expect(result).toBe('analyzer');
});
it('should handle undefined questions gracefully (defaults to empty array)', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: 0,
questionCount: 5,
} as any);
expect(result).toBe('generator');
});
it('should prevent negative currentQuestionIndex via Math.max(0)', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: -1,
questionCount: 5,
questions: [{ text: 'q1' }, { text: 'q2' }, { text: 'q3' }, { text: 'q4' }, { text: 'q5' }],
} as any);
expect(result).toBe('interviewer');
});
it('should handle completely empty state (no fields provided)', () => {
const result = routeAfterGrading({} as any);
expect(result).toBe('generator');
});
it('should route to interviewer at last index before targetCount boundary', () => {
const result = routeAfterGrading({
shouldFollowUp: false,
currentQuestionIndex: 4,
questionCount: 5,
questions: [{ text: 'q1' }, { text: 'q2' }, { text: 'q3' }, { text: 'q4' }, { text: 'q5' }],
} as any);
expect(result).toBe('interviewer');
});
});
+1 -1
View File
@@ -8,7 +8,7 @@ import { reportAnalyzerNode } from './nodes/analyzer.node';
/**
* Conditional routing logic for the Grader node.
*/
const routeAfterGrading = (state: typeof EvaluationAnnotation.State) => {
export const routeAfterGrading = (state: typeof EvaluationAnnotation.State) => {
const targetCount = state.questionCount || 5;
const questionsLen = state.questions?.length || 0;
const currentIndex = Math.max(0, state.currentQuestionIndex || 0);