merge with practice_engine
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/deck.dart';
|
||||
import 'package:practice_engine/models/deck_config.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/logic/attempt_service.dart';
|
||||
|
||||
void main() {
|
||||
group('Attempt Flow', () {
|
||||
late Deck deck;
|
||||
late AttemptService attemptService;
|
||||
|
||||
setUp(() {
|
||||
final config = const DeckConfig(
|
||||
defaultAttemptSize: 5,
|
||||
requiredConsecutiveCorrect: 3,
|
||||
);
|
||||
|
||||
final questions = List.generate(10, (i) {
|
||||
return Question(
|
||||
id: 'q$i',
|
||||
prompt: 'Question $i',
|
||||
answers: ['A', 'B', 'C'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: i,
|
||||
);
|
||||
});
|
||||
|
||||
deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Test Deck',
|
||||
description: 'Test',
|
||||
questions: questions,
|
||||
config: config,
|
||||
);
|
||||
|
||||
attemptService = AttemptService(seed: 42);
|
||||
});
|
||||
|
||||
test('createAttempt selects correct number of questions', () {
|
||||
final attempt = attemptService.createAttempt(deck: deck);
|
||||
|
||||
expect(attempt.questions.length, equals(deck.config.defaultAttemptSize));
|
||||
});
|
||||
|
||||
test('createAttempt uses custom attempt size', () {
|
||||
final attempt = attemptService.createAttempt(
|
||||
deck: deck,
|
||||
attemptSize: 3,
|
||||
);
|
||||
|
||||
expect(attempt.questions.length, equals(3));
|
||||
});
|
||||
|
||||
test('createAttempt has no duplicate questions', () {
|
||||
final attempt = attemptService.createAttempt(deck: deck);
|
||||
|
||||
final ids = attempt.questions.map((q) => q.id).toList();
|
||||
final uniqueIds = ids.toSet();
|
||||
|
||||
expect(uniqueIds.length, equals(ids.length));
|
||||
});
|
||||
|
||||
test('processAttempt updates deck with results', () {
|
||||
final attempt = attemptService.createAttempt(deck: deck);
|
||||
final answers = <String, int>{};
|
||||
|
||||
// Answer all questions correctly
|
||||
for (final question in attempt.questions) {
|
||||
answers[question.id] = question.correctIndices.first;
|
||||
}
|
||||
|
||||
final result = attemptService.processAttempt(
|
||||
deck: deck,
|
||||
attempt: attempt,
|
||||
answers: answers,
|
||||
);
|
||||
|
||||
expect(result.updatedDeck.currentAttemptIndex,
|
||||
equals(deck.currentAttemptIndex + 1));
|
||||
});
|
||||
|
||||
test('processAttempt calculates correct percentage', () {
|
||||
final attempt = attemptService.createAttempt(
|
||||
deck: deck,
|
||||
attemptSize: 3,
|
||||
);
|
||||
final answers = <String, int>{};
|
||||
|
||||
// Answer 2 out of 3 correctly
|
||||
answers[attempt.questions[0].id] =
|
||||
attempt.questions[0].correctIndices.first;
|
||||
answers[attempt.questions[1].id] =
|
||||
attempt.questions[1].correctIndices.first;
|
||||
answers[attempt.questions[2].id] = 999; // Wrong answer
|
||||
|
||||
final result = attemptService.processAttempt(
|
||||
deck: deck,
|
||||
attempt: attempt,
|
||||
answers: answers,
|
||||
);
|
||||
|
||||
expect(result.result.percentageCorrect, closeTo(66.67, 0.01));
|
||||
expect(result.result.correctCount, equals(2));
|
||||
});
|
||||
|
||||
test('processAttempt tracks incorrect questions', () {
|
||||
final attempt = attemptService.createAttempt(
|
||||
deck: deck,
|
||||
attemptSize: 3,
|
||||
);
|
||||
final answers = <String, int>{};
|
||||
|
||||
// Answer first correctly, rest incorrectly
|
||||
answers[attempt.questions[0].id] =
|
||||
attempt.questions[0].correctIndices.first;
|
||||
answers[attempt.questions[1].id] = 999;
|
||||
answers[attempt.questions[2].id] = 999;
|
||||
|
||||
final result = attemptService.processAttempt(
|
||||
deck: deck,
|
||||
attempt: attempt,
|
||||
answers: answers,
|
||||
);
|
||||
|
||||
expect(result.result.incorrectQuestions.length, equals(2));
|
||||
});
|
||||
|
||||
test('processAttempt updates question streaks correctly', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 2,
|
||||
);
|
||||
|
||||
final testDeck = deck.copyWith(questions: [question]);
|
||||
final attempt = attemptService.createAttempt(
|
||||
deck: testDeck,
|
||||
attemptSize: 1,
|
||||
);
|
||||
|
||||
final answers = {question.id: question.correctIndices.first};
|
||||
|
||||
final result = attemptService.processAttempt(
|
||||
deck: testDeck,
|
||||
attempt: attempt,
|
||||
answers: answers,
|
||||
);
|
||||
|
||||
final updated = result.updatedDeck.questions.first;
|
||||
expect(updated.consecutiveCorrect, equals(3));
|
||||
expect(updated.isKnown, equals(true));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/models/attempt_result.dart';
|
||||
|
||||
void main() {
|
||||
group('AttemptResult', () {
|
||||
test('fromAnswers calculates correct percentage', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
);
|
||||
|
||||
final results = [
|
||||
AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [0],
|
||||
isCorrect: true,
|
||||
statusChange: QuestionStatusChange.unchanged,
|
||||
),
|
||||
AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [1],
|
||||
isCorrect: false,
|
||||
statusChange: QuestionStatusChange.unchanged,
|
||||
),
|
||||
AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [0],
|
||||
isCorrect: true,
|
||||
statusChange: QuestionStatusChange.unchanged,
|
||||
),
|
||||
];
|
||||
|
||||
final attemptResult = AttemptResult.fromAnswers(
|
||||
results: results,
|
||||
timeSpent: 1000,
|
||||
);
|
||||
|
||||
expect(attemptResult.totalQuestions, equals(3));
|
||||
expect(attemptResult.correctCount, equals(2));
|
||||
expect(attemptResult.percentageCorrect, closeTo(66.67, 0.01));
|
||||
});
|
||||
|
||||
test('fromAnswers handles empty results', () {
|
||||
final attemptResult = AttemptResult.fromAnswers(
|
||||
results: [],
|
||||
timeSpent: 0,
|
||||
);
|
||||
|
||||
expect(attemptResult.totalQuestions, equals(0));
|
||||
expect(attemptResult.correctCount, equals(0));
|
||||
expect(attemptResult.percentageCorrect, equals(0.0));
|
||||
});
|
||||
|
||||
test('fromAnswers filters incorrect questions', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
);
|
||||
|
||||
final results = [
|
||||
AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [0],
|
||||
isCorrect: true,
|
||||
statusChange: QuestionStatusChange.unchanged,
|
||||
),
|
||||
AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [1],
|
||||
isCorrect: false,
|
||||
statusChange: QuestionStatusChange.unchanged,
|
||||
),
|
||||
AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [1],
|
||||
isCorrect: false,
|
||||
statusChange: QuestionStatusChange.unchanged,
|
||||
),
|
||||
];
|
||||
|
||||
final attemptResult = AttemptResult.fromAnswers(
|
||||
results: results,
|
||||
timeSpent: 1000,
|
||||
);
|
||||
|
||||
expect(attemptResult.incorrectQuestions.length, equals(2));
|
||||
});
|
||||
|
||||
test('fromAnswers includes all results', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
);
|
||||
|
||||
final results = List.generate(5, (i) {
|
||||
return AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [i % 2],
|
||||
isCorrect: i % 2 == 0,
|
||||
statusChange: QuestionStatusChange.unchanged,
|
||||
);
|
||||
});
|
||||
|
||||
final attemptResult = AttemptResult.fromAnswers(
|
||||
results: results,
|
||||
timeSpent: 2000,
|
||||
);
|
||||
|
||||
expect(attemptResult.allResults.length, equals(5));
|
||||
expect(attemptResult.timeSpent, equals(2000));
|
||||
});
|
||||
});
|
||||
|
||||
group('AnswerResult', () {
|
||||
test('contains question and answer information', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'What is 2+2?',
|
||||
answers: ['3', '4', '5'],
|
||||
correctAnswerIndices: [1],
|
||||
);
|
||||
|
||||
final result = AnswerResult(
|
||||
question: question,
|
||||
userAnswerIndices: [1],
|
||||
isCorrect: true,
|
||||
statusChange: QuestionStatusChange.improved,
|
||||
);
|
||||
|
||||
expect(result.question, equals(question));
|
||||
expect(result.userAnswerIndices, equals([1]));
|
||||
expect(result.isCorrect, equals(true));
|
||||
expect(result.statusChange, equals(QuestionStatusChange.improved));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/deck_config.dart';
|
||||
|
||||
void main() {
|
||||
group('DeckConfig', () {
|
||||
test('has default values', () {
|
||||
const config = DeckConfig();
|
||||
|
||||
expect(config.requiredConsecutiveCorrect, equals(3));
|
||||
expect(config.defaultAttemptSize, equals(10));
|
||||
expect(config.priorityIncreaseOnIncorrect, equals(5));
|
||||
expect(config.priorityDecreaseOnCorrect, equals(2));
|
||||
expect(config.immediateFeedbackEnabled, equals(true));
|
||||
});
|
||||
|
||||
test('can be created with custom values', () {
|
||||
const config = DeckConfig(
|
||||
requiredConsecutiveCorrect: 5,
|
||||
defaultAttemptSize: 20,
|
||||
priorityIncreaseOnIncorrect: 10,
|
||||
priorityDecreaseOnCorrect: 3,
|
||||
immediateFeedbackEnabled: false,
|
||||
);
|
||||
|
||||
expect(config.requiredConsecutiveCorrect, equals(5));
|
||||
expect(config.defaultAttemptSize, equals(20));
|
||||
expect(config.priorityIncreaseOnIncorrect, equals(10));
|
||||
expect(config.priorityDecreaseOnCorrect, equals(3));
|
||||
expect(config.immediateFeedbackEnabled, equals(false));
|
||||
});
|
||||
|
||||
test('copyWith creates new config with updated fields', () {
|
||||
const config = DeckConfig();
|
||||
final updated = config.copyWith(
|
||||
requiredConsecutiveCorrect: 4,
|
||||
immediateFeedbackEnabled: false,
|
||||
);
|
||||
|
||||
expect(updated.requiredConsecutiveCorrect, equals(4));
|
||||
expect(updated.defaultAttemptSize, equals(config.defaultAttemptSize));
|
||||
expect(updated.immediateFeedbackEnabled, equals(false));
|
||||
});
|
||||
|
||||
test('equality works correctly', () {
|
||||
const config1 = DeckConfig();
|
||||
const config2 = DeckConfig();
|
||||
const config3 = DeckConfig(requiredConsecutiveCorrect: 5);
|
||||
|
||||
expect(config1, equals(config2));
|
||||
expect(config1, isNot(equals(config3)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/deck.dart';
|
||||
import 'package:practice_engine/models/deck_config.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
|
||||
void main() {
|
||||
group('Deck', () {
|
||||
late DeckConfig defaultConfig;
|
||||
late List<Question> sampleQuestions;
|
||||
|
||||
setUp(() {
|
||||
defaultConfig = const DeckConfig();
|
||||
sampleQuestions = [
|
||||
Question(
|
||||
id: 'q1',
|
||||
prompt: 'What is 2+2?',
|
||||
answers: ['3', '4', '5'],
|
||||
correctAnswerIndices: [1],
|
||||
isKnown: true,
|
||||
),
|
||||
Question(
|
||||
id: 'q2',
|
||||
prompt: 'What is 3+3?',
|
||||
answers: ['5', '6', '7'],
|
||||
correctAnswerIndices: [1],
|
||||
isKnown: false,
|
||||
),
|
||||
Question(
|
||||
id: 'q3',
|
||||
prompt: 'What is 4+4?',
|
||||
answers: ['7', '8', '9'],
|
||||
correctAnswerIndices: [1],
|
||||
isKnown: true,
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
test('calculates numberOfQuestions correctly', () {
|
||||
final deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Math Deck',
|
||||
description: 'Basic math',
|
||||
questions: sampleQuestions,
|
||||
config: defaultConfig,
|
||||
);
|
||||
|
||||
expect(deck.numberOfQuestions, equals(3));
|
||||
});
|
||||
|
||||
test('calculates knownCount correctly', () {
|
||||
final deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Math Deck',
|
||||
description: 'Basic math',
|
||||
questions: sampleQuestions,
|
||||
config: defaultConfig,
|
||||
);
|
||||
|
||||
expect(deck.knownCount, equals(2));
|
||||
});
|
||||
|
||||
test('calculates practicePercentage correctly', () {
|
||||
final deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Math Deck',
|
||||
description: 'Basic math',
|
||||
questions: sampleQuestions,
|
||||
config: defaultConfig,
|
||||
);
|
||||
|
||||
expect(deck.practicePercentage, closeTo(66.67, 0.01));
|
||||
});
|
||||
|
||||
test('practicePercentage is 0 for empty deck', () {
|
||||
final deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Empty Deck',
|
||||
description: 'No questions',
|
||||
questions: [],
|
||||
config: defaultConfig,
|
||||
);
|
||||
|
||||
expect(deck.practicePercentage, equals(0.0));
|
||||
});
|
||||
|
||||
test('practicePercentage is 100 when all questions are known', () {
|
||||
final allKnown = sampleQuestions.map((q) => q.copyWith(isKnown: true)).toList();
|
||||
final deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'All Known',
|
||||
description: 'All known',
|
||||
questions: allKnown,
|
||||
config: defaultConfig,
|
||||
);
|
||||
|
||||
expect(deck.practicePercentage, equals(100.0));
|
||||
});
|
||||
|
||||
test('copyWith creates new deck with updated fields', () {
|
||||
final deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Math Deck',
|
||||
description: 'Basic math',
|
||||
questions: sampleQuestions,
|
||||
config: defaultConfig,
|
||||
);
|
||||
|
||||
final updated = deck.copyWith(title: 'Updated Title');
|
||||
expect(updated.title, equals('Updated Title'));
|
||||
expect(updated.id, equals(deck.id));
|
||||
expect(updated.questions, equals(deck.questions));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/deck.dart';
|
||||
import 'package:practice_engine/models/deck_config.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/logic/deck_service.dart';
|
||||
|
||||
void main() {
|
||||
group('Manual Override Logic', () {
|
||||
late DeckConfig config;
|
||||
late Deck deck;
|
||||
|
||||
setUp(() {
|
||||
config = const DeckConfig(requiredConsecutiveCorrect: 3);
|
||||
deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Test Deck',
|
||||
description: 'Test',
|
||||
questions: [
|
||||
Question(
|
||||
id: 'q1',
|
||||
prompt: 'Question 1',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 1,
|
||||
isKnown: false,
|
||||
priorityPoints: 5,
|
||||
),
|
||||
Question(
|
||||
id: 'q2',
|
||||
prompt: 'Question 2',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 0,
|
||||
isKnown: false,
|
||||
priorityPoints: 10,
|
||||
),
|
||||
],
|
||||
config: config,
|
||||
);
|
||||
});
|
||||
|
||||
test('markQuestionAsKnown sets streak to threshold and isKnown to true', () {
|
||||
final updated = DeckService.markQuestionAsKnown(
|
||||
deck: deck,
|
||||
questionId: 'q1',
|
||||
);
|
||||
|
||||
final question = updated.questions.firstWhere((q) => q.id == 'q1');
|
||||
expect(question.consecutiveCorrect,
|
||||
equals(config.requiredConsecutiveCorrect));
|
||||
expect(question.isKnown, equals(true));
|
||||
});
|
||||
|
||||
test('markQuestionAsNeedsPractice sets isKnown to false and streak to 0', () {
|
||||
// First mark as known
|
||||
var updated = DeckService.markQuestionAsKnown(
|
||||
deck: deck,
|
||||
questionId: 'q1',
|
||||
);
|
||||
|
||||
// Then mark as needs practice
|
||||
updated = DeckService.markQuestionAsNeedsPractice(
|
||||
deck: updated,
|
||||
questionId: 'q1',
|
||||
);
|
||||
|
||||
final question = updated.questions.firstWhere((q) => q.id == 'q1');
|
||||
expect(question.isKnown, equals(false));
|
||||
expect(question.consecutiveCorrect, equals(0));
|
||||
});
|
||||
|
||||
test('markQuestionAsNeedsPractice preserves priority', () {
|
||||
final updated = DeckService.markQuestionAsNeedsPractice(
|
||||
deck: deck,
|
||||
questionId: 'q1',
|
||||
);
|
||||
|
||||
final question = updated.questions.firstWhere((q) => q.id == 'q1');
|
||||
expect(question.priorityPoints, equals(5));
|
||||
});
|
||||
|
||||
test('manual override with needs practice ignores correctness', () {
|
||||
final question = deck.questions.first;
|
||||
|
||||
// Answer correctly but mark as needs practice
|
||||
final updated = DeckService.updateQuestionWithManualOverride(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
userMarkedNeedsPractice: true,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
// Streak should not increment
|
||||
expect(updated.consecutiveCorrect, equals(question.consecutiveCorrect));
|
||||
|
||||
// Priority should not decrease
|
||||
expect(updated.priorityPoints, equals(question.priorityPoints));
|
||||
|
||||
// isKnown should not change
|
||||
expect(updated.isKnown, equals(question.isKnown));
|
||||
|
||||
// But totalAttempts should increment
|
||||
expect(updated.totalAttempts, equals(question.totalAttempts + 1));
|
||||
});
|
||||
|
||||
test('manual override without needs practice follows normal flow', () {
|
||||
final question = deck.questions.first;
|
||||
|
||||
final updated = DeckService.updateQuestionWithManualOverride(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
userMarkedNeedsPractice: false,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
// Should behave like normal update
|
||||
expect(updated.consecutiveCorrect, equals(question.consecutiveCorrect + 1));
|
||||
expect(updated.priorityPoints,
|
||||
equals(question.priorityPoints - config.priorityDecreaseOnCorrect));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/models/deck_config.dart';
|
||||
import 'package:practice_engine/algorithms/priority_manager.dart';
|
||||
|
||||
void main() {
|
||||
group('PriorityManager', () {
|
||||
late DeckConfig config;
|
||||
|
||||
setUp(() {
|
||||
config = const DeckConfig(
|
||||
priorityIncreaseOnIncorrect: 5,
|
||||
priorityDecreaseOnCorrect: 2,
|
||||
);
|
||||
});
|
||||
|
||||
test('increases priority on incorrect answer', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 10,
|
||||
);
|
||||
|
||||
final updated = PriorityManager.applyAnswerResult(
|
||||
question: question,
|
||||
isCorrect: false,
|
||||
config: config,
|
||||
);
|
||||
|
||||
expect(updated.priorityPoints,
|
||||
equals(10 + config.priorityIncreaseOnIncorrect));
|
||||
});
|
||||
|
||||
test('decreases priority on correct answer', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 10,
|
||||
);
|
||||
|
||||
final updated = PriorityManager.applyAnswerResult(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
);
|
||||
|
||||
expect(updated.priorityPoints,
|
||||
equals(10 - config.priorityDecreaseOnCorrect));
|
||||
});
|
||||
|
||||
test('priority cannot go below 0', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 1,
|
||||
);
|
||||
|
||||
final updated = PriorityManager.applyAnswerResult(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
);
|
||||
|
||||
expect(updated.priorityPoints, equals(0));
|
||||
});
|
||||
|
||||
test('resetPriority sets priority to 0', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 100,
|
||||
);
|
||||
|
||||
final updated = PriorityManager.resetPriority(question);
|
||||
|
||||
expect(updated.priorityPoints, equals(0));
|
||||
});
|
||||
|
||||
test('withPriorityPoints enforces non-negative priority', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 10,
|
||||
);
|
||||
|
||||
final updated = question.withPriorityPoints(-5);
|
||||
|
||||
expect(updated.priorityPoints, equals(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/models/deck_config.dart';
|
||||
import 'package:practice_engine/logic/deck_service.dart';
|
||||
|
||||
void main() {
|
||||
group('Question State Transitions', () {
|
||||
late DeckConfig config;
|
||||
|
||||
setUp(() {
|
||||
config = const DeckConfig(requiredConsecutiveCorrect: 3);
|
||||
});
|
||||
|
||||
test('correct answer increments streak', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 1,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updated.consecutiveCorrect, equals(2));
|
||||
});
|
||||
|
||||
test('incorrect answer resets streak', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 2,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: false,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updated.consecutiveCorrect, equals(0));
|
||||
});
|
||||
|
||||
test('question becomes known when streak reaches threshold', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 2,
|
||||
isKnown: false,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updated.consecutiveCorrect, equals(3));
|
||||
expect(updated.isKnown, equals(true));
|
||||
});
|
||||
|
||||
test('incorrect answer sets isKnown to false', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 3,
|
||||
isKnown: true,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: false,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updated.isKnown, equals(false));
|
||||
expect(updated.consecutiveCorrect, equals(0));
|
||||
});
|
||||
|
||||
test('priority increases on incorrect answer', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 5,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: false,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updated.priorityPoints,
|
||||
equals(5 + config.priorityIncreaseOnIncorrect));
|
||||
});
|
||||
|
||||
test('priority decreases on correct answer', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 10,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updated.priorityPoints,
|
||||
equals(10 - config.priorityDecreaseOnCorrect));
|
||||
});
|
||||
|
||||
test('priority cannot go below 0', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 1,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updated.priorityPoints, equals(0));
|
||||
});
|
||||
|
||||
test('lastAttemptIndex is updated', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
lastAttemptIndex: 5,
|
||||
);
|
||||
|
||||
final updated = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
currentAttemptIndex: 10,
|
||||
);
|
||||
|
||||
expect(updated.lastAttemptIndex, equals(10));
|
||||
});
|
||||
|
||||
test('totalAttempts increments on both correct and incorrect', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
totalAttempts: 5,
|
||||
totalCorrectAttempts: 3,
|
||||
);
|
||||
|
||||
final updatedCorrect = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: true,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updatedCorrect.totalAttempts, equals(6));
|
||||
expect(updatedCorrect.totalCorrectAttempts, equals(4));
|
||||
|
||||
final updatedIncorrect = DeckService.updateQuestionAfterAnswer(
|
||||
question: question,
|
||||
isCorrect: false,
|
||||
config: config,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(updatedIncorrect.totalAttempts, equals(6));
|
||||
expect(updatedIncorrect.totalCorrectAttempts, equals(3));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/deck.dart';
|
||||
import 'package:practice_engine/models/deck_config.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/logic/deck_service.dart';
|
||||
|
||||
void main() {
|
||||
group('Reset Logic', () {
|
||||
late DeckConfig config;
|
||||
late Deck deck;
|
||||
|
||||
setUp(() {
|
||||
config = const DeckConfig(requiredConsecutiveCorrect: 3);
|
||||
deck = Deck(
|
||||
id: 'deck1',
|
||||
title: 'Test Deck',
|
||||
description: 'Test',
|
||||
questions: [
|
||||
Question(
|
||||
id: 'q1',
|
||||
prompt: 'Question 1',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 5,
|
||||
isKnown: true,
|
||||
priorityPoints: 10,
|
||||
lastAttemptIndex: 20,
|
||||
totalCorrectAttempts: 15,
|
||||
totalAttempts: 20,
|
||||
),
|
||||
Question(
|
||||
id: 'q2',
|
||||
prompt: 'Question 2',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
consecutiveCorrect: 2,
|
||||
isKnown: false,
|
||||
priorityPoints: 5,
|
||||
lastAttemptIndex: 15,
|
||||
totalCorrectAttempts: 8,
|
||||
totalAttempts: 12,
|
||||
),
|
||||
],
|
||||
config: config,
|
||||
currentAttemptIndex: 25,
|
||||
);
|
||||
});
|
||||
|
||||
test('resetDeck resets all question states', () {
|
||||
final reset = DeckService.resetDeck(deck: deck);
|
||||
|
||||
for (final question in reset.questions) {
|
||||
expect(question.consecutiveCorrect, equals(0));
|
||||
expect(question.isKnown, equals(false));
|
||||
expect(question.priorityPoints, equals(0));
|
||||
expect(question.lastAttemptIndex, equals(-1));
|
||||
}
|
||||
|
||||
expect(reset.currentAttemptIndex, equals(0));
|
||||
});
|
||||
|
||||
test('resetDeck preserves attempt counts by default', () {
|
||||
final reset = DeckService.resetDeck(deck: deck);
|
||||
|
||||
expect(reset.questions[0].totalAttempts, equals(20));
|
||||
expect(reset.questions[0].totalCorrectAttempts, equals(15));
|
||||
expect(reset.questions[1].totalAttempts, equals(12));
|
||||
expect(reset.questions[1].totalCorrectAttempts, equals(8));
|
||||
});
|
||||
|
||||
test('resetDeck resets attempt counts when requested', () {
|
||||
final reset = DeckService.resetDeck(
|
||||
deck: deck,
|
||||
resetAttemptCounts: true,
|
||||
);
|
||||
|
||||
for (final question in reset.questions) {
|
||||
expect(question.totalAttempts, equals(0));
|
||||
expect(question.totalCorrectAttempts, equals(0));
|
||||
}
|
||||
});
|
||||
|
||||
test('resetDeck resets currentAttemptIndex to 0', () {
|
||||
final reset = DeckService.resetDeck(deck: deck);
|
||||
|
||||
expect(reset.currentAttemptIndex, equals(0));
|
||||
});
|
||||
|
||||
test('resetDeck preserves deck metadata', () {
|
||||
final reset = DeckService.resetDeck(deck: deck);
|
||||
|
||||
expect(reset.id, equals(deck.id));
|
||||
expect(reset.title, equals(deck.title));
|
||||
expect(reset.description, equals(deck.description));
|
||||
expect(reset.config, equals(deck.config));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/algorithms/spaced_repetition.dart';
|
||||
|
||||
void main() {
|
||||
group('SpacedRepetition', () {
|
||||
test('baseKnownProbability for never-seen known question', () {
|
||||
final weight = SpacedRepetition.calculateKnownQuestionWeight(
|
||||
lastAttemptIndex: -1,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(weight, equals(SpacedRepetition.baseKnownProbability));
|
||||
});
|
||||
|
||||
test('probability increases with attempts since last seen', () {
|
||||
final weight1 = SpacedRepetition.calculateKnownQuestionWeight(
|
||||
lastAttemptIndex: 0,
|
||||
currentAttemptIndex: 10,
|
||||
);
|
||||
|
||||
final weight2 = SpacedRepetition.calculateKnownQuestionWeight(
|
||||
lastAttemptIndex: 0,
|
||||
currentAttemptIndex: 30,
|
||||
);
|
||||
|
||||
expect(weight2, greaterThan(weight1));
|
||||
});
|
||||
|
||||
test('probability is capped at maxKnownProbability', () {
|
||||
final weight = SpacedRepetition.calculateKnownQuestionWeight(
|
||||
lastAttemptIndex: 0,
|
||||
currentAttemptIndex: 1000,
|
||||
);
|
||||
|
||||
expect(weight, lessThanOrEqualTo(SpacedRepetition.maxKnownProbability));
|
||||
});
|
||||
|
||||
test('probability increases gradually', () {
|
||||
final weights = <double>[];
|
||||
for (int i = 0; i <= 30; i++) {
|
||||
final weight = SpacedRepetition.calculateKnownQuestionWeight(
|
||||
lastAttemptIndex: 0,
|
||||
currentAttemptIndex: i,
|
||||
);
|
||||
weights.add(weight);
|
||||
}
|
||||
|
||||
// Should be monotonically increasing
|
||||
for (int i = 1; i < weights.length; i++) {
|
||||
expect(weights[i], greaterThanOrEqualTo(weights[i - 1]));
|
||||
}
|
||||
});
|
||||
|
||||
test('getQuestionWeight uses priority for unknown questions', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
isKnown: false,
|
||||
priorityPoints: 10,
|
||||
);
|
||||
|
||||
final weight = SpacedRepetition.getQuestionWeight(
|
||||
question: question,
|
||||
currentAttemptIndex: 0,
|
||||
basePriorityWeight: 1.0,
|
||||
);
|
||||
|
||||
// Should be priority + 1
|
||||
expect(weight, equals(11.0));
|
||||
});
|
||||
|
||||
test('getQuestionWeight uses spaced repetition for known questions', () {
|
||||
final question = Question(
|
||||
id: 'q1',
|
||||
prompt: 'Test',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
isKnown: true,
|
||||
lastAttemptIndex: 5,
|
||||
priorityPoints: 0,
|
||||
);
|
||||
|
||||
final weight = SpacedRepetition.getQuestionWeight(
|
||||
question: question,
|
||||
currentAttemptIndex: 10,
|
||||
basePriorityWeight: 1.0,
|
||||
);
|
||||
|
||||
// Should use spaced repetition probability
|
||||
final expected = SpacedRepetition.calculateKnownQuestionWeight(
|
||||
lastAttemptIndex: 5,
|
||||
currentAttemptIndex: 10,
|
||||
);
|
||||
expect(weight, closeTo(expected, 0.001));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:practice_engine/models/question.dart';
|
||||
import 'package:practice_engine/algorithms/weighted_selector.dart';
|
||||
|
||||
void main() {
|
||||
group('WeightedSelector', () {
|
||||
test('selects correct number of questions', () {
|
||||
final selector = WeightedSelector(seed: 42);
|
||||
final questions = List.generate(10, (i) {
|
||||
return Question(
|
||||
id: 'q$i',
|
||||
prompt: 'Question $i',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: i,
|
||||
);
|
||||
});
|
||||
|
||||
final selected = selector.selectQuestions(
|
||||
candidates: questions,
|
||||
count: 5,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(selected.length, equals(5));
|
||||
});
|
||||
|
||||
test('returns all questions if count >= candidates length', () {
|
||||
final selector = WeightedSelector(seed: 42);
|
||||
final questions = List.generate(5, (i) {
|
||||
return Question(
|
||||
id: 'q$i',
|
||||
prompt: 'Question $i',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
);
|
||||
});
|
||||
|
||||
final selected = selector.selectQuestions(
|
||||
candidates: questions,
|
||||
count: 10,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(selected.length, equals(5));
|
||||
});
|
||||
|
||||
test('returns empty list for empty candidates', () {
|
||||
final selector = WeightedSelector(seed: 42);
|
||||
final selected = selector.selectQuestions(
|
||||
candidates: [],
|
||||
count: 5,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(selected, isEmpty);
|
||||
});
|
||||
|
||||
test('returns empty list for count <= 0', () {
|
||||
final selector = WeightedSelector(seed: 42);
|
||||
final questions = List.generate(5, (i) {
|
||||
return Question(
|
||||
id: 'q$i',
|
||||
prompt: 'Question $i',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
);
|
||||
});
|
||||
|
||||
final selected = selector.selectQuestions(
|
||||
candidates: questions,
|
||||
count: 0,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
expect(selected, isEmpty);
|
||||
});
|
||||
|
||||
test('no duplicates in selection', () {
|
||||
final selector = WeightedSelector(seed: 42);
|
||||
final questions = List.generate(10, (i) {
|
||||
return Question(
|
||||
id: 'q$i',
|
||||
prompt: 'Question $i',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 5,
|
||||
);
|
||||
});
|
||||
|
||||
final selected = selector.selectQuestions(
|
||||
candidates: questions,
|
||||
count: 5,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
|
||||
final ids = selected.map((q) => q.id).toSet();
|
||||
expect(ids.length, equals(selected.length));
|
||||
});
|
||||
|
||||
test('higher priority questions are more likely to be selected', () {
|
||||
final selector = WeightedSelector(seed: 42);
|
||||
final questions = [
|
||||
Question(
|
||||
id: 'low',
|
||||
prompt: 'Low priority',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 1,
|
||||
),
|
||||
Question(
|
||||
id: 'high',
|
||||
prompt: 'High priority',
|
||||
answers: ['A', 'B'],
|
||||
correctAnswerIndices: [0],
|
||||
priorityPoints: 100,
|
||||
),
|
||||
];
|
||||
|
||||
// Run multiple selections and count occurrences
|
||||
int highSelected = 0;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
final selected = selector.selectQuestions(
|
||||
candidates: questions,
|
||||
count: 1,
|
||||
currentAttemptIndex: 0,
|
||||
);
|
||||
if (selected.first.id == 'high') {
|
||||
highSelected++;
|
||||
}
|
||||
}
|
||||
|
||||
// High priority should be selected more often
|
||||
expect(highSelected, greaterThan(50));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user