166 lines
5.3 KiB
Dart
166 lines
5.3 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();
|
|
},
|
|
);
|
|
}
|