feat: complete kouyu_english app codebase, A0 specifications and .gitignore

This commit is contained in:
shen
2026-09-15 15:58:33 +08:00
parent b8072673d8
commit 37c86f7ecb
136 changed files with 19042 additions and 0 deletions
@@ -0,0 +1,154 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:kouyu_english/core/generated_content.dart';
void main() {
const valid = '''{
"schemaVersion":"review-variant-1",
"variantId":"ai-a0-p12-001",
"targetItemId":"A0-P12",
"prompt":"请用英语说你来自哪里。",
"expectedAnswer":"Im from [place]."
}''';
test('accepts a bounded variant for its requested frozen target', () {
final item = decodeGeneratedReviewVariant(
valid,
expectedTargetItemId: 'A0-P12',
);
expect(item, isNotNull);
expect(item!.targetItemId, 'A0-P12');
});
test('rejects an AI variant that changes the target identity', () {
expect(
decodeGeneratedReviewVariant(valid, expectedTargetItemId: 'A0-P01'),
isNull,
);
});
test('rejects unknown schema fields', () {
final invalid = valid.replaceFirst('}', ',"unsafe":"ignore"}');
expect(
decodeGeneratedReviewVariant(invalid, expectedTargetItemId: 'A0-P12'),
isNull,
);
});
const writing = '''{
"schemaVersion":"writing-feedback-1",
"lessonId":"a0-06",
"verdict":"rewrite",
"feedback":"加上 from 就能说清来自哪里。",
"suggestion":"I'm from Hong Kong.",
"missing":["来自哪里"]
}''';
test('accepts bounded writing feedback for its lesson', () {
final feedback = decodeWritingAiFeedback(
writing,
expectedLessonId: 'a0-06',
);
expect(feedback, isNotNull);
expect(feedback!.verdict, 'rewrite');
});
test('rejects writing feedback for a different lesson', () {
expect(decodeWritingAiFeedback(writing, expectedLessonId: 'a0-07'), isNull);
});
const lesson = '''{
"schemaVersion":"lesson-1",
"lessonId":"ai-a0-p12-1",
"revision":1,
"stageVersion":"A0-1.0",
"source":"aiGenerated",
"status":"validated",
"abilityIds":["A0-C06"],
"prerequisiteIds":["a0-05"],
"targetItemIds":["A0-P12"],
"receptiveChunks":[],
"newItemIds":[],
"previewItemIds":[],
"estimatedMinutes":10,
"tasks":[
{"taskId":"l","skill":"listening","type":"listenChoice","prompt":"听后选择。","stimulus":"I am from Hong Kong.","answer":"Hong Kong","targetItemIds":["A0-P12"]},
{"taskId":"s","skill":"speaking","type":"repeat","prompt":"跟读。","stimulus":"I am from Hong Kong.","answer":"I am from Hong Kong.","targetItemIds":["A0-P12"]},
{"taskId":"r","skill":"reading","type":"readAnswer","prompt":"读后回答。","stimulus":"I am from Hong Kong.","answer":"Hong Kong","targetItemIds":["A0-P12"]},
{"taskId":"w","skill":"writing","type":"writeAnswer","prompt":"写一句。","stimulus":"来自香港。","answer":"I am from Hong Kong.","targetItemIds":["A0-P12"]}
]
}''';
test('accepts a bounded four-skill adaptive lesson', () {
final decoded = decodeGeneratedLesson(
lesson,
expectedTargetItemId: 'A0-P12',
);
expect(decoded, isNotNull);
expect(decoded!.tasks.map((task) => task.skill).toSet(), hasLength(4));
});
test('adaptive answer check accepts harmless punctuation variation', () {
final task = decodeGeneratedLesson(
lesson,
expectedTargetItemId: 'A0-P12',
)!.tasks[3];
expect(matchesAdaptiveLessonAnswer(task, "i am from hong kong!"), isTrue);
});
test('adaptive answer check rejects a missing target token', () {
final task = decodeGeneratedLesson(
lesson,
expectedTargetItemId: 'A0-P12',
)!.tasks[3];
expect(matchesAdaptiveLessonAnswer(task, 'I am from Hong.'), isFalse);
});
test('adaptive answer spec accepts listed slots and rejects exclusions', () {
const task = GeneratedLessonTask(
taskId: 'speaking',
skill: 'speaking',
type: 'repeat',
prompt: '说一句。',
stimulus: 'I am from Hong Kong.',
answer: 'I am from Hong Kong.',
targetItemIds: ['A0-P12'],
answerSpec: GeneratedAnswerSpec(
requiredAnyPhrases: [
['I am', "I'm"],
['from'],
['Hong Kong'],
],
acceptedAnswers: ['I am from Hong Kong.', "I'm from Hong Kong."],
forbiddenPhrases: ['Beijing'],
),
);
expect(matchesAdaptiveLessonAnswer(task, "I'm from Hong Kong!"), isTrue);
expect(matchesAdaptiveLessonAnswer(task, 'I am from Beijing.'), isFalse);
expect(matchesAdaptiveLessonAnswer(task, 'Hong Kong.'), isFalse);
});
test('rejects an adaptive lesson with a different frozen target', () {
expect(
decodeGeneratedLesson(lesson, expectedTargetItemId: 'A0-P11'),
isNull,
);
});
test(
'rejects an adaptive lesson that introduces an unknown English word',
() {
final invalid = lesson.replaceFirst('Hong Kong', 'library');
expect(
decodeGeneratedLesson(invalid, expectedTargetItemId: 'A0-P12'),
isNull,
);
},
);
}