import 'package:flutter_test/flutter_test.dart'; import 'package:kouyu_english/core/app_state.dart'; import 'package:kouyu_english/core/models.dart'; void main() { test( 'listening help survives resume and cannot earn independent evidence', () { final first = AppState()..lessonStep = LessonStep.listening; first.recordListeningPlayback(slow: true); first.revealListeningTranscript(); first.recordListeningMiss(); final resumed = AppState()..restoreForTest(first.snapshotForTest()); expect(resumed.listeningAudioPlayed, isTrue); expect(resumed.listeningSupport, ListeningSupport.transcript); expect(resumed.listeningAttempts, 1); resumed.completeListening(recognized: true); final evidence = resumed.attemptEvidence .where((item) => item.taskId.endsWith('listening-check')) .toList(); expect(evidence, isNotEmpty); expect( evidence.every((item) => item.outcome == EvidenceKind.assisted), isTrue, ); expect(evidence.first.listeningSupport, ListeningSupport.transcript); expect(evidence.first.firstTry, isFalse); expect(evidence.first.attemptCount, 2); }, ); test('unaided first listening answer earns recognition evidence', () { final state = AppState()..lessonStep = LessonStep.listening; state.recordListeningPlayback(); state.completeListening(recognized: true); final evidence = state.attemptEvidence .where((item) => item.taskId.endsWith('listening-check')) .toList(); expect(evidence, isNotEmpty); expect( evidence.every((item) => item.outcome == EvidenceKind.independentSuccess), isTrue, ); expect(evidence.first.firstTry, isTrue); }); test('a choice without playable audio cannot earn listening recognition', () { final state = AppState()..lessonStep = LessonStep.listening; state.completeListening(recognized: true); final evidence = state.attemptEvidence .where((item) => item.taskId.endsWith('listening-check')) .toList(); expect( evidence.every((item) => item.outcome == EvidenceKind.assisted), isTrue, ); }); test( 'TTS failure survives resume and gives only assisted listening credit', () { final state = AppState()..lessonStep = LessonStep.listening; state.recordListeningAudioUnavailable(); final resumed = AppState()..restoreForTest(state.snapshotForTest()); expect(resumed.listeningAudioUnavailable, isTrue); expect(resumed.listeningSupport, ListeningSupport.transcript); resumed.completeListening(recognized: true); expect(resumed.lessonStep, LessonStep.speaking); expect(resumed.listeningAudioUnavailable, isFalse); expect( resumed.attemptEvidence .where((entry) => entry.taskId.endsWith('listening-check')) .every((entry) => entry.outcome == EvidenceKind.assisted), isTrue, ); }, ); test('skipping follow-reading does not create speaking evidence', () { final state = AppState()..lessonStep = LessonStep.speaking; state.completeSpeaking(); expect(state.lessonStep, LessonStep.reading); expect( state.attemptEvidence.where( (entry) => entry.taskId.endsWith('-speaking'), ), isEmpty, ); }); test('the fourth checkpoint keeps its 30-day audit interval', () { final state = AppState(); state.reviewQueue.add( ReviewItem( id: 'A0-P02', target: "What's your name?", prompt: '问名字', hint: "What's your name?", dueAt: DateTime.now().subtract(const Duration(days: 1)), skill: '回忆表达', successfulReviews: 3, ), ); state.completeReview( state.reviewQueue.single, assisted: false, firstTry: true, attemptCount: 1, rawAnswer: "What's your name?", ); final next = state.reviewQueue.single; expect(next.successfulReviews, 4); expect( next.dueAt.difference(DateTime.now()).inDays, inInclusiveRange(29, 30), ); expect(state.attemptEvidence.last.firstTry, isTrue); expect(state.attemptEvidence.last.attemptCount, 1); }); test('later clean mastery audits lengthen to 45 days', () { final state = AppState(); state.reviewQueue.add( ReviewItem( id: 'A0-P02', target: "What's your name?", prompt: '问名字', hint: "What's your name?", dueAt: DateTime.now().subtract(const Duration(days: 1)), skill: '回忆表达', successfulReviews: 4, ), ); state.completeReview( state.reviewQueue.single, assisted: false, firstTry: true, attemptCount: 1, rawAnswer: "What's your name?", ); expect( state.reviewQueue.single.dueAt.difference(DateTime.now()).inDays, inInclusiveRange(44, 45), ); }); test( 'slow dictation cannot advance a checkpoint even if caller says independent', () { final state = AppState(); state.reviewQueue.add( ReviewItem( id: 'A0-P02', target: "What's your name?", prompt: '听写', hint: "What's your name?", dueAt: DateTime.now().subtract(const Duration(days: 1)), skill: '听写', ), ); state.completeReview( state.reviewQueue.single, assisted: false, firstTry: true, attemptCount: 1, listeningSupport: ListeningSupport.slow, rawAnswer: "What's your name?", ); expect(state.reviewQueue.single.successfulReviews, 0); expect(state.attemptEvidence.last.outcome, EvidenceKind.assisted); expect( state.attemptEvidence.last.listeningSupport, ListeningSupport.slow, ); }, ); }