Files
openmaic/OpenMAIC/tests/pbl/v2/submission.test.ts
2026-08-16 14:58:47 +08:00

188 lines
6.1 KiB
TypeScript

/**
* Tests for the submission helpers.
*
* Targets the v2-specific behaviours of submission storage and the
* summarize-for-evaluator helper added in PR 6.1.
*/
import { describe, expect, it } from 'vitest';
import {
addSubmission,
latestSubmissionForMicrotask,
listSubmissionsForMicrotask,
summarizeLatestSubmissionForMicrotask,
summarizeSubmissionsForMicrotask,
} from '@/lib/pbl/v2/operations/runtime/submission';
import { buildRevisionGuidanceMessage } from '@/components/scene-renderers/pbl/v2/submission';
import type { PBLEvaluation, PBLProjectV2 } from '@/lib/pbl/v2/types';
function mkProject(): PBLProjectV2 {
return {
uiPhase: 'workspace',
title: 't',
description: 'd',
proficiency: 'intermediate',
language: 'zh-CN',
tags: [],
status: 'active',
roles: [],
milestones: [],
submissions: [],
evaluations: [],
threads: [],
engagementEvents: [],
createdAt: 'ts',
updatedAt: 'ts',
};
}
describe('addSubmission', () => {
it('appends with id, microtaskId, kind, content, timestamp', () => {
const p = mkProject();
const sub = addSubmission(p, {
microtaskId: 't1',
kind: 'text',
content: 'hello',
});
expect(sub.id).toMatch(/^sub_/);
expect(sub.microtaskId).toBe('t1');
expect(sub.kind).toBe('text');
expect(p.submissions).toHaveLength(1);
expect(p.submissions[0]).toBe(sub);
expect(p.updatedAt).toBe(sub.createdAt);
});
it('preserves filename + mimeType for file kind', () => {
const p = mkProject();
const sub = addSubmission(p, {
microtaskId: 't1',
kind: 'file',
content: 'def x(): pass',
filename: 'x.py',
mimeType: 'text/x-python',
});
expect(sub.filename).toBe('x.py');
expect(sub.mimeType).toBe('text/x-python');
});
});
describe('listSubmissionsForMicrotask', () => {
it('filters by microtaskId', () => {
const p = mkProject();
addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'a' });
addSubmission(p, { microtaskId: 't2', kind: 'text', content: 'b' });
addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'c' });
const subs = listSubmissionsForMicrotask(p, 't1');
expect(subs.map((s) => s.content)).toEqual(['a', 'c']);
});
it('returns empty array when none exist', () => {
expect(listSubmissionsForMicrotask(mkProject(), 'nope')).toEqual([]);
});
});
describe('latestSubmissionForMicrotask', () => {
it('returns the newest submission for a microtask', () => {
const p = mkProject();
const older = addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'older wrong' });
const newer = addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'newer correct' });
older.createdAt = '2026-05-30T00:00:00.000Z';
newer.createdAt = '2026-05-30T00:01:00.000Z';
expect(latestSubmissionForMicrotask(p, 't1')).toBe(newer);
});
});
describe('summarizeLatestSubmissionForMicrotask', () => {
it('includes only the latest submission content', () => {
const p = mkProject();
const older = addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'older wrong' });
const newer = addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'newer correct' });
older.createdAt = '2026-05-30T00:00:00.000Z';
newer.createdAt = '2026-05-30T00:01:00.000Z';
const out = summarizeLatestSubmissionForMicrotask(p, 't1');
expect(out).toContain('Latest submission');
expect(out).toContain('newer correct');
expect(out).not.toContain('older wrong');
});
});
describe('summarizeSubmissionsForMicrotask', () => {
it('returns empty string when no submissions exist (lets eval-prompts skip the section)', () => {
expect(summarizeSubmissionsForMicrotask(mkProject(), 'nope')).toBe('');
});
it('produces a per-submission header with kind, filename, and timestamp', () => {
const p = mkProject();
addSubmission(p, {
microtaskId: 't1',
kind: 'file',
content: 'print("hi")',
filename: 'hello.py',
});
const out = summarizeSubmissionsForMicrotask(p, 't1');
expect(out).toContain('Submission 1');
expect(out).toContain('file');
expect(out).toContain('hello.py');
expect(out).toContain('print("hi")');
});
it('truncates when content exceeds the char budget with a clear marker', () => {
const p = mkProject();
const big = 'x'.repeat(8000);
addSubmission(p, { microtaskId: 't1', kind: 'text', content: big });
const out = summarizeSubmissionsForMicrotask(p, 't1', 200);
expect(out.length).toBeLessThan(big.length);
expect(out).toContain('[...older submissions truncated]');
});
it('numbers multiple submissions and truncates clean at boundary', () => {
const p = mkProject();
addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'first' });
addSubmission(p, { microtaskId: 't1', kind: 'text', content: 'second' });
const out = summarizeSubmissionsForMicrotask(p, 't1');
expect(out).toContain('Submission 1');
expect(out).toContain('first');
expect(out).toContain('Submission 2');
expect(out).toContain('second');
});
});
describe('buildRevisionGuidanceMessage', () => {
const evaluation: PBLEvaluation = {
id: 'ev-1',
kind: 'task',
microtaskId: 't1',
feedback: 'needs work',
strengths: [],
improvements: ['补上 remove_item(item) 调用', '退出时使用 break'],
score: 45,
createdAt: '2026-05-30T00:00:00.000Z',
};
it('uses the first-revision wording on the first failed task evaluation', () => {
const msg = buildRevisionGuidanceMessage({
evaluation,
instructorId: 'role-i',
microtaskId: 't1',
language: 'zh-CN',
revisionAttempt: 1,
});
expect(msg?.content).toContain('这版先别急着往下走');
});
it('varies the opening after repeated failed task evaluations', () => {
const msg = buildRevisionGuidanceMessage({
evaluation,
instructorId: 'role-i',
microtaskId: 't1',
language: 'zh-CN',
revisionAttempt: 2,
});
expect(msg?.content).toContain('这次还需要再补一轮');
expect(msg?.content).not.toContain('这版先别急着往下走');
});
});