Files

213 lines
6.7 KiB
Dart

import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:kouyu_english/core/local_store.dart';
import 'dart:convert';
void main() {
test(
'a legacy snapshot migrates into canonical entities without replacement',
() async {
final executor = NativeDatabase.memory();
final store = LocalSnapshotStore.forTesting(executor);
const legacy = '''{
"onboardingComplete": true,
"lessonStep": "reading",
"activeLessonId": "a0-02",
"attemptEvidence": [],
"mastery": [],
"reviews": []
}''';
// Open the schema first, then model the row left by a pre-entity build.
expect(await store.read(), isNull);
await executor.runCustom(
'''INSERT INTO learning_snapshots(snapshot_id, schema_version, payload, updated_at)
VALUES (?, ?, ?, ?)''',
const ['current', 1, legacy, '2026-09-14T00:00:00.000Z'],
);
expect(await store.read(), legacy);
await store.write(legacy);
final restored =
jsonDecode((await store.read())!) as Map<String, dynamic>;
expect(restored['activeLessonId'], 'a0-02');
expect(restored['onboardingComplete'], isTrue);
expect(
await executor.runSelect(
'SELECT payload FROM learning_snapshots WHERE snapshot_id = ?',
const ['current'],
),
[
<String, Object?>{'payload': legacy},
],
);
expect(
await executor.runSelect(
'SELECT profile_id FROM learner_profiles',
const [],
),
[
<String, Object?>{'profile_id': 'current'},
],
);
await executor.close();
},
);
test(
'SQLite entity store restores canonical state without writing a snapshot',
() async {
final executor = NativeDatabase.memory();
final store = LocalSnapshotStore.forTesting(executor);
expect(await store.read(), isNull);
const first = '''{
"revision": 1,
"attemptEvidence": [{"id":"a1","itemId":"A0-P01","taskId":"t1","skill":"speaking","inputMode":"speechToText","outcome":"independentSuccess","createdAt":"2026-09-14T00:00:00.000Z","assisted":false,"variantIndex":0,"originalTranscript":"Hello","transcriptConfirmed":true,"transcriptEdited":false}],
"mastery": [{"id":"A0-P01","label":"Hello","status":"recognize","checkpoint":1,"needsReview":false}],
"reviews": [{"id":"A0-P01","dueAt":"2026-09-15T00:00:00.000Z","attempts":1,"successfulReviews":1,"variantIndex":0}],
"temporaryLexicon": [{"query":"library","definition":"图书馆","provider":"compatible","model":"demo","createdAt":"2026-09-14T00:00:00.000Z"}],
"lessonStep":"speaking", "activeLessonId":"a0-01"
}''';
await store.write(first);
final restored =
jsonDecode((await store.read())!) as Map<String, dynamic>;
expect(restored['lessonStep'], 'speaking');
expect(restored['attemptEvidence'], hasLength(1));
expect(restored['mastery'], hasLength(1));
expect(
await executor.runSelect(
'SELECT snapshot_id FROM learning_snapshots',
const [],
),
isEmpty,
);
expect(
await executor.runSelect(
'SELECT item_id FROM attempt_evidence',
const [],
),
[
<String, Object?>{'item_id': 'A0-P01'},
],
);
await executor.runCustom(
'UPDATE learning_snapshots SET payload = ? WHERE snapshot_id = ?',
const ['{"corrupt":"backup only"}', 'current'],
);
final canonicalAfterSnapshotChange =
jsonDecode((await store.read())!) as Map<String, dynamic>;
expect(canonicalAfterSnapshotChange['mastery'], hasLength(1));
expect(
await executor.runSelect(
'SELECT original_transcript, transcript_confirmed, transcript_edited FROM attempt_evidence',
const [],
),
[
<String, Object?>{
'original_transcript': 'Hello',
'transcript_confirmed': 1,
'transcript_edited': 0,
},
],
);
expect(
await executor.runSelect(
'SELECT definition FROM temporary_lexicon_entries',
const [],
),
[
<String, Object?>{'definition': '图书馆'},
],
);
expect(
await executor.runSelect('SELECT status FROM mastery_items', const []),
[
<String, Object?>{'status': 'recognize'},
],
);
expect(
await executor.runSelect('SELECT item_id FROM review_items', const []),
[
<String, Object?>{'item_id': 'A0-P01'},
],
);
expect(
await executor.runSelect(
'SELECT session_type FROM study_sessions',
const [],
),
[
<String, Object?>{'session_type': 'lesson'},
],
);
await store.write('{"revision":2}');
expect(
(jsonDecode((await store.read())!)
as Map<String, dynamic>)['attemptEvidence'],
isEmpty,
);
await store.clear();
expect(await store.read(), isNull);
expect(
await executor.runSelect(
'SELECT query_key FROM temporary_lexicon_entries',
const [],
),
isEmpty,
);
await executor.close();
},
);
test(
'repeated writes with cached analyses still persist later review progress',
() async {
final executor = NativeDatabase.memory();
final store = LocalSnapshotStore.forTesting(executor);
String snapshot(String dueAt) => jsonEncode({
'onboardingComplete': true,
'activeLessonId': 'a0-01',
'attemptEvidence': [],
'mastery': [],
'reviews': [
{
'id': 'hello',
'target': 'hello',
'prompt': 'Say hello',
'hint': '你好',
'dueAt': dueAt,
'skill': '口头回忆',
},
],
'temporaryLexicon': [
{'query': 'Hello', 'definition': '你好'},
{'query': 'hello', 'definition': '你好'},
],
'sentenceAnalyses': [
{
'query': 'Nice to meet you.',
'payload': {'translation': '很高兴认识你。'},
},
],
});
await store.write(snapshot('2026-09-18T08:00:00.000'));
// A completed review moves the card to tomorrow; this write must land.
await store.write(snapshot('2026-09-19T08:00:00.000'));
final restored =
jsonDecode((await store.read())!) as Map<String, dynamic>;
expect(
(restored['reviews'] as List).single['dueAt'],
'2026-09-19T08:00:00.000',
);
expect(restored['sentenceAnalyses'], hasLength(1));
await executor.close();
},
);
}