feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
import 'package:words999/services/oracle_master_prompt_generator.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
await WordsRepository().init();
|
||||
});
|
||||
|
||||
test('Generates new Master Prompt for Archetype Enrichment as Architect of Meanings', () {
|
||||
final repo = WordsRepository();
|
||||
final word = repo.allWords[0]; // e.g. #000
|
||||
|
||||
final prompt = OracleMasterPromptGenerator.generateArchetypeEnrichmentPrompt(
|
||||
word,
|
||||
customCore: 'Фундаментальный закон всепоглощающей трансформации',
|
||||
customOracle: 'Предыдущее описание сути',
|
||||
);
|
||||
|
||||
expect(prompt.isNotEmpty, true);
|
||||
expect(prompt.contains('Архитектор Смыслов, верховный мифолог, культуролог и онтологический герменевт'), true);
|
||||
expect(prompt.contains('Абсолютный запрет на гадательную терминологию'), true);
|
||||
expect(prompt.contains('Запрет на лень и краткость'), true);
|
||||
expect(prompt.contains('Обязательный мультикультурный синтез'), true);
|
||||
expect(prompt.contains('Метрика: В самом конце ответа ты обязан выдать точное количество символов'), true);
|
||||
expect(prompt.contains('ВХОДНЫЕ ДАННЫЕ АРХЕТИПА:'), true);
|
||||
expect(prompt.contains(word.word.toUpperCase()), true);
|
||||
expect(prompt.contains('## ЯДРО'), true);
|
||||
expect(prompt.contains('## ОРАКУЛ'), true);
|
||||
expect(prompt.contains('## ТЕНЬ'), true);
|
||||
expect(prompt.contains('## ВОПРОС'), true);
|
||||
expect(prompt.contains('## ЖЕСТ'), true);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:words999/services/houses_service.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Archon Strict Noun Verification Tests', () {
|
||||
late HousesService service;
|
||||
late WordsRepository repo;
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
repo = WordsRepository();
|
||||
await repo.init(remoteDailyShuffle: false);
|
||||
service = HousesService();
|
||||
});
|
||||
|
||||
test('All 10 Archons are strictly nouns across 100 random sessions', () {
|
||||
for (int run = 0; run < 100; run++) {
|
||||
final session = service.createNewSession();
|
||||
expect(session.sectors.length, 10);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final archon = session.sectors[i].archonWord;
|
||||
expect(
|
||||
archon.partOfSpeech.toLowerCase().trim(),
|
||||
'существительное',
|
||||
reason: 'Archon #${i + 1} «${archon.word}» is not a noun! (pos=${archon.partOfSpeech}, cat=${archon.category})',
|
||||
);
|
||||
expect(
|
||||
archon.category.toLowerCase().contains('действие'),
|
||||
isFalse,
|
||||
reason: 'Archon «${archon.word}» has action category!',
|
||||
);
|
||||
expect(
|
||||
archon.word.toLowerCase().trim(),
|
||||
isNot(equals('положить')),
|
||||
reason: '«положить» must never be an Archon!',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Word «положить» is strictly a verb in WordsRepository', () {
|
||||
final w = repo.getWordByName('положить');
|
||||
expect(w, isNotNull);
|
||||
expect(w!.partOfSpeech.toLowerCase().trim(), 'глагол');
|
||||
expect(w.category, contains('Действие'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/models/culture_branch.dart';
|
||||
import 'package:words999/models/word_model.dart';
|
||||
|
||||
void main() {
|
||||
final kb = json.decode(File('assets/data/oracle_kb.json').readAsStringSync()) as Map<String, dynamic>;
|
||||
|
||||
test('Cyrillic word boundaries work (Dart \\b is ASCII-only)', () {
|
||||
final sumer = kCultureBranches.firstWhere((b) => b.id == 'sumer');
|
||||
expect(sumer.matches(' В Шумере знак UD '), isTrue);
|
||||
expect(sumer.matches(' безутешный '), isFalse); // "уту" inside a word must not match
|
||||
});
|
||||
|
||||
test('ДЕНЬ is decomposed into many human traditions', () {
|
||||
final w = const WordItem(id: 6, word: 'день', partOfSpeech: '', frequency: 1)
|
||||
.enrichWithKb(kb['день'] as Map<String, dynamic>);
|
||||
final c = w.resolvedCultures;
|
||||
for (final name in [
|
||||
'Шумер и Месопотамия',
|
||||
'Древний Египет',
|
||||
'Иудейская традиция и Каббала',
|
||||
'Античность (Греция и Рим)',
|
||||
'Кельты и друиды',
|
||||
'Древний Китай',
|
||||
'Полинезия и Океания',
|
||||
'Мезоамерика и Анды',
|
||||
'Алхимия и герметизм',
|
||||
]) {
|
||||
expect(c.containsKey(name), isTrue, reason: 'missing $name; got ${c.keys}');
|
||||
}
|
||||
});
|
||||
|
||||
test('НОЧЬ covers Islamic/Sufi and Sumerian layers', () {
|
||||
final w = const WordItem(id: 7, word: 'ночь', partOfSpeech: '', frequency: 1)
|
||||
.enrichWithKb(kb['ночь'] as Map<String, dynamic>);
|
||||
final c = w.resolvedCultures;
|
||||
expect(c.keys, containsAll(['Ислам и суфизм', 'Шумер и Месопотамия', 'Древний Египет', 'Древний Китай']));
|
||||
});
|
||||
|
||||
test('explicit cultures override auto-extraction and survive toMap/fromMap', () {
|
||||
const w = WordItem(id: 1, word: 'x', partOfSpeech: '', frequency: 1, cultures: {'Славянская традиция': 'Навь'});
|
||||
final back = WordItem.fromMap(w.toMap());
|
||||
expect(back.resolvedCultures, {'Славянская традиция': 'Навь'});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:words999/services/day_numerology.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
await WordsRepository().init(remoteDailyShuffle: false);
|
||||
});
|
||||
|
||||
test('calendar reduction keeps master numbers', () {
|
||||
expect(DayNumerology.reduce(11), 11);
|
||||
expect(DayNumerology.reduce(22), 22);
|
||||
expect(DayNumerology.reduce(33), 33);
|
||||
expect(DayNumerology.reduce(29), 11);
|
||||
expect(DayNumerology.reduce(2026), 1);
|
||||
expect(DayNumerology.reduce(10), 1);
|
||||
|
||||
final ordinary = DayNumerology.ofDate(DateTime(2026, 10, 5));
|
||||
expect(ordinary.value, 7);
|
||||
expect(ordinary.word, 'семь');
|
||||
expect(ordinary.formula, '5 + 1 + 1 = 7');
|
||||
|
||||
expect(DayNumerology.ofDate(DateTime(2016, 1, 1)).value, 11);
|
||||
expect(DayNumerology.ofDate(DateTime(2016, 2, 11)).value, 22);
|
||||
expect(DayNumerology.ofDate(DateTime(2009, 11, 29)).value, 33);
|
||||
});
|
||||
|
||||
test('code words and twelve stations live inside the thousand', () {
|
||||
final repo = WordsRepository();
|
||||
for (final name in DayNumerology.words.values) {
|
||||
expect(repo.getWordByName(name), isNotNull, reason: name);
|
||||
}
|
||||
|
||||
final file = File('assets/data/traditions_canon.json');
|
||||
final canon = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
final numerology = canon['numerology'] as Map<String, dynamic>;
|
||||
final stations = numerology['stations'] as List;
|
||||
expect(stations, hasLength(12));
|
||||
for (final raw in stations) {
|
||||
final station = raw as Map<String, dynamic>;
|
||||
final primary = station['primary'] as Map<String, dynamic>;
|
||||
final word = primary['word'] as String;
|
||||
final item = repo.getWordByName(word);
|
||||
expect(item, isNotNull, reason: word);
|
||||
expect(item!.id, primary['id']);
|
||||
expect((station['name'] as String).isNotEmpty, isTrue);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/services/external_symbols_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('ExternalSymbolsRepository loads 2300+ symbols and supports search and illustrations', () async {
|
||||
final repo = ExternalSymbolsRepository();
|
||||
await repo.init();
|
||||
|
||||
expect(repo.isLoaded, isTrue);
|
||||
expect(repo.totalCount, greaterThanOrEqualTo(1400));
|
||||
|
||||
// Test search
|
||||
final results = repo.search('акант');
|
||||
expect(results.isNotEmpty, isTrue);
|
||||
expect(results.first.term.toLowerCase(), contains('акант'));
|
||||
expect(results.first.sources.isNotEmpty, isTrue);
|
||||
|
||||
// Test with illustrations
|
||||
final withImages = repo.search('', onlyWithImages: true);
|
||||
expect(withImages.isNotEmpty, isTrue);
|
||||
expect(withImages.first.hasIllustrations, isTrue);
|
||||
expect(withImages.first.illustrations.first['file'], isNotEmpty);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/models/syncretic_session_model.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
import 'package:words999/services/oracle_master_prompt_generator.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
await WordsRepository().init();
|
||||
});
|
||||
|
||||
test('Generates complete Geomancy Master Prompt for Great Shield', () {
|
||||
final repo = WordsRepository();
|
||||
final word = repo.allWords[0];
|
||||
|
||||
final judge = GeomancyPromptFigure(
|
||||
position: 'Верховный Судья',
|
||||
name: 'Большая Удача',
|
||||
latinName: 'Fortuna Major',
|
||||
arabicName: 'Al-Nasra al-Dakhila',
|
||||
lines: [1, 1, 2, 2],
|
||||
element: 'Огонь',
|
||||
planet: 'Солнце',
|
||||
lore: 'Высший триумф, защита и вход благодати.',
|
||||
linkedWord: word,
|
||||
);
|
||||
|
||||
final reconciler = GeomancyPromptFigure(
|
||||
position: 'Примиритель',
|
||||
name: 'Прибыль',
|
||||
latinName: 'Acquisitio',
|
||||
arabicName: 'Al-Qabd al-Dakhil',
|
||||
lines: [2, 1, 1, 2],
|
||||
element: 'Воздух',
|
||||
planet: 'Юпитер',
|
||||
lore: 'Умножение ресурсов, духовное обогащение.',
|
||||
linkedWord: word,
|
||||
);
|
||||
|
||||
final prompt = OracleMasterPromptGenerator.generateGeomancyMasterPrompt(
|
||||
question: 'Исход ключевого решения',
|
||||
layoutTitle: 'Великий Щит Судьбы (16 фигур)',
|
||||
isShield: true,
|
||||
judge: judge,
|
||||
reconciler: reconciler,
|
||||
rightWitness: judge,
|
||||
leftWitness: reconciler,
|
||||
);
|
||||
|
||||
expect(prompt.isNotEmpty, true);
|
||||
expect(prompt.contains("АРАБСКАЯ ГЕОМАНТИЯ ('ИЛМ АР-РАМЛ)"), true);
|
||||
expect(prompt.contains('ВЕРХОВНЫЙ СУДЬЯ'), true);
|
||||
expect(prompt.contains('ПРИМИРИТЕЛЬ'), true);
|
||||
expect(prompt.contains('Fortuna Major'), true);
|
||||
expect(prompt.contains('Acquisitio'), true);
|
||||
expect(prompt.contains('Исход ключевого решения'), true);
|
||||
expect(prompt.contains('ТРЕБОВАНИЯ К ИНТЕРПРЕТАЦИИ НЕЙРОСЕТЬЮ'), true);
|
||||
});
|
||||
|
||||
test('Generates Cross-System Master Prompt with Great Geomantic Shield structure', () {
|
||||
final repo = WordsRepository();
|
||||
final word = repo.allWords[0];
|
||||
|
||||
final items = [
|
||||
ReadingSignItem(title: '1-я Мать: Via', subtitle: 'Огонь • Солнце', symbol: '· · · ·', lore: 'Путь, движение', linkedWord: word),
|
||||
ReadingSignItem(title: '2-я Мать: Populus', subtitle: 'Вода • Луна', symbol: '·· ·· ·· ··', lore: 'Толпа, пассивность', linkedWord: word),
|
||||
ReadingSignItem(title: '3-я Мать: Conjunctio', subtitle: 'Воздух • Меркурий', symbol: '·· · · ··', lore: 'Соединение, союз', linkedWord: word),
|
||||
ReadingSignItem(title: '4-я Мать: Carcer', subtitle: 'Земля • Сатурн', symbol: '· ·· ·· ·', lore: 'Тюрьма, фиксация', linkedWord: word),
|
||||
ReadingSignItem(title: '1-я Дочь: Fortuna Major', subtitle: 'Огонь • Солнце', symbol: '· · ·· ··', lore: 'Великая удача'),
|
||||
ReadingSignItem(title: '2-я Дочь: Acquisitio', subtitle: 'Воздух • Юпитер', symbol: '·· · · ··', lore: 'Прибыль'),
|
||||
ReadingSignItem(title: '3-я Дочь: Puella', subtitle: 'Воздух • Венера', symbol: '· · ·· ·', lore: 'Девушка'),
|
||||
ReadingSignItem(title: '4-я Дочь: Puer', subtitle: 'Огонь • Марс', symbol: '· · · ··', lore: 'Юноша'),
|
||||
ReadingSignItem(title: '1-й Племянник: Amissio', subtitle: 'Земля • Венера', symbol: '· ·· · ··', lore: 'Потеря'),
|
||||
ReadingSignItem(title: '2-й Племянник: Albus', subtitle: 'Вода • Меркурий', symbol: '·· ·· · ··', lore: 'Белизна'),
|
||||
ReadingSignItem(title: '3-й Племянник: Rubeus', subtitle: 'Огонь • Марс', symbol: '·· · ·· ··', lore: 'Краснота'),
|
||||
ReadingSignItem(title: '4-й Племянник: Tristitia', subtitle: 'Земля • Сатурн', symbol: '·· ·· ·· ·', lore: 'Печаль'),
|
||||
ReadingSignItem(title: 'Правый Свидетель (Внешнее): Fortuna Minor', subtitle: 'Огонь • Солнце', symbol: '· · ·· ·', lore: 'Малая удача'),
|
||||
ReadingSignItem(title: 'Левый Свидетель (Внутреннее): Laetitia', subtitle: 'Воздух • Юпитер', symbol: '· ·· ·· ··', lore: 'Радость'),
|
||||
ReadingSignItem(title: 'СУДЬЯ (Итог): Fortuna Major', subtitle: 'Огонь • Солнце', symbol: '· · ·· ··', lore: 'Триумф и истина', linkedWord: word),
|
||||
ReadingSignItem(title: 'Примиритель: Populus', subtitle: 'Вода • Луна', symbol: '·· ·· ·· ··', lore: 'Окончательное согласие'),
|
||||
];
|
||||
|
||||
final geomancyReading = SyncreticReading(
|
||||
id: 'reading_geomancy_test',
|
||||
systemId: 'geomancy',
|
||||
systemName: "Арабская Геомантия ('Илм ар-рамл)",
|
||||
systemIcon: '⌛',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: 'Великий Геомантический Щит (16 фигур)',
|
||||
inputMethod: 'sand_ritual',
|
||||
items: items,
|
||||
question: 'Каков исход предприятия?',
|
||||
);
|
||||
|
||||
final session = SyncreticSession(
|
||||
id: 'session_test_1',
|
||||
date: DateTime.now(),
|
||||
mainQuestion: 'Каков исход предприятия?',
|
||||
readings: [geomancyReading],
|
||||
);
|
||||
|
||||
final crossPrompt = OracleMasterPromptGenerator.generateCrossSystemComparison(session);
|
||||
|
||||
expect(crossPrompt.contains('ВЕЛИКИЙ МЕТА-МАСТЕР-ПРОМПТ'), true);
|
||||
expect(crossPrompt.contains('СВЯЩЕННАЯ АРХИТЕКТУРА ВЕЛИКОГО ГЕОМАНТИЧЕСКОГО ЩИТА (16 САКРАЛЬНЫХ ФИГУР)'), true);
|
||||
expect(crossPrompt.contains('ВЕРХОВНЫЙ СУДЬЯ'), true);
|
||||
expect(crossPrompt.contains('ПРИМИРИТЕЛЬ'), true);
|
||||
expect(crossPrompt.contains('ДВА СВИДЕТЕЛЯ'), true);
|
||||
expect(crossPrompt.contains('ЧЕТЫРЕ МАТЕРИ'), true);
|
||||
expect(crossPrompt.contains('ЧЕТЫРЕ ДОЧЕРИ'), true);
|
||||
expect(crossPrompt.contains('ЧЕТЫРЕ ПЛЕМЯННИКА'), true);
|
||||
expect(crossPrompt.contains('Геомантический вектор: Итоговый вердикт выносит Верховный Судья'), true);
|
||||
expect(crossPrompt.contains('вердикт Верховного Судьи Геомантии (Аль-Кади)'), true);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:words999/services/houses_service.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('HousesService 33x30 Field & 10 Archons Tests', () {
|
||||
late HousesService service;
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final repo = WordsRepository();
|
||||
await repo.init(remoteDailyShuffle: false);
|
||||
service = HousesService();
|
||||
});
|
||||
|
||||
test('Session generation creates exactly 10 Archons and 990 Houses without overlap', () {
|
||||
final session = service.createNewSession(question: 'Тестовый вопрос');
|
||||
|
||||
expect(session.sectors.length, equals(10));
|
||||
expect(session.grid.length, equals(990));
|
||||
|
||||
final allWords = <String>{};
|
||||
|
||||
// Check Archons
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final s = session.sectors[i];
|
||||
expect(s.index, equals(i));
|
||||
expect(s.startCol, equals(i * 3));
|
||||
expect(s.endCol, equals(i * 3 + 2));
|
||||
expect(s.archonWord.word.isNotEmpty, isTrue);
|
||||
allWords.add(s.archonWord.word.toLowerCase());
|
||||
}
|
||||
|
||||
expect(allWords.length, equals(10), reason: '10 Archons must be unique');
|
||||
|
||||
// Check Houses (33 rows x 30 cols)
|
||||
for (int r = 0; r < 33; r++) {
|
||||
for (int c = 0; c < 30; c++) {
|
||||
final idx = r * 30 + c;
|
||||
final cell = session.grid[idx];
|
||||
expect(cell.row, equals(r));
|
||||
expect(cell.col, equals(c));
|
||||
expect(cell.houseIndex, equals(idx));
|
||||
expect(cell.sectorIndex, equals(c ~/ 3));
|
||||
expect(cell.houseWord.word.isNotEmpty, isTrue);
|
||||
allWords.add(cell.houseWord.word.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly 1000 unique words partitioned
|
||||
expect(allWords.length, equals(1000));
|
||||
});
|
||||
|
||||
test('Divination with 5 chosen houses populates guests and computes chains', () {
|
||||
final session = service.createNewSession();
|
||||
final chosen = [
|
||||
session.grid[0].copyWith(selectionOrder: 1),
|
||||
session.grid[15].copyWith(selectionOrder: 2),
|
||||
session.grid[42].copyWith(selectionOrder: 3),
|
||||
session.grid[100].copyWith(selectionOrder: 4),
|
||||
session.grid[500].copyWith(selectionOrder: 5),
|
||||
];
|
||||
|
||||
final result = service.completeDivination(session, chosen);
|
||||
|
||||
expect(result.selectedHouses.length, equals(5));
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final h = result.selectedHouses[i];
|
||||
expect(h.guestWord, isNotNull);
|
||||
expect(h.selectionOrder, equals(i + 1));
|
||||
}
|
||||
|
||||
expect(result.chains.isNotEmpty, isTrue);
|
||||
for (final chain in result.chains) {
|
||||
expect(chain.steps.isNotEmpty, isTrue);
|
||||
expect(chain.steps.first.house, isNotNull);
|
||||
expect(chain.steps.first.guest, isNotNull);
|
||||
}
|
||||
});
|
||||
|
||||
test('Chain length is capped at maximum 9 links', () {
|
||||
final session = service.createNewSession();
|
||||
final chosen = [
|
||||
session.grid[0].copyWith(selectionOrder: 1),
|
||||
];
|
||||
|
||||
final result = service.completeDivination(session, chosen);
|
||||
for (final chain in result.chains) {
|
||||
expect(chain.steps.length, lessThanOrEqualTo(9));
|
||||
}
|
||||
});
|
||||
|
||||
test('Querent word is guaranteed to be in houses and its index matches dynamically', () {
|
||||
final repo = WordsRepository();
|
||||
final manWord = repo.allWords.firstWhere((w) => w.word.toLowerCase() == 'мужчина');
|
||||
|
||||
final session1 = service.createNewSession(querentWord: manWord);
|
||||
expect(session1.querentHouseIndex, isNotNull);
|
||||
final qIndex1 = session1.querentHouseIndex!;
|
||||
expect(session1.grid[qIndex1].houseWord.word.toLowerCase(), equals('мужчина'));
|
||||
|
||||
// Reshuffle matrix multiple times: the querent must always match the house word at its index
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final sessionRe = service.createNewSession(querentWord: manWord);
|
||||
expect(sessionRe.querentHouseIndex, isNotNull);
|
||||
final qIdx = sessionRe.querentHouseIndex!;
|
||||
expect(sessionRe.grid[qIdx].houseWord.word.toLowerCase(), equals('мужчина'));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,16 @@ void main() {
|
||||
expect(InterpreterPersona.byId('nope').id, InterpreterPersona.clear.id);
|
||||
});
|
||||
|
||||
test('roster includes warhammer ork shaman and aeldari farseer', () {
|
||||
expect(InterpreterPersona.orc.title, 'Орк-шаман');
|
||||
expect(InterpreterPersona.orc.systemPrompt.toLowerCase(), contains('вааагх'));
|
||||
expect(InterpreterPersona.aeldari.title, 'Всевидящий аэльдари');
|
||||
expect(
|
||||
InterpreterPersona.all.map((p) => p.id),
|
||||
containsAll(['orc', 'aeldari', 'techpriest']),
|
||||
);
|
||||
});
|
||||
|
||||
test('settings persist the selected interpreter voice', () async {
|
||||
await SettingsRepository().setPersonaId(InterpreterPersona.orc.id);
|
||||
expect(SettingsRepository().persona.id, InterpreterPersona.orc.id);
|
||||
@@ -50,6 +60,28 @@ void main() {
|
||||
expect(messages.last.content, contains('проверяемый шаг'));
|
||||
expect(messages.last.content, contains('ОГОНЬ'));
|
||||
expect(messages.last.content, contains('Стоит ли уходить?'));
|
||||
expect(messages.last.content, contains('Закон знака побеждает голос'));
|
||||
expect(messages.first.content.toLowerCase(), isNot(contains('голос самой вечности')));
|
||||
});
|
||||
|
||||
test('empty canon is marked empty and aeldari voice does not invent a closed path', () async {
|
||||
await SettingsRepository().setPersonaId(InterpreterPersona.aeldari.id);
|
||||
final messages = AiOracleService().buildInterpretationMessages(
|
||||
question: '',
|
||||
words: const [
|
||||
SessionWord(
|
||||
word: 'ухо',
|
||||
sessionNumber: 306,
|
||||
partOfSpeech: 'существительное',
|
||||
category: 'Образ / Архетип',
|
||||
symbolicMeaning:
|
||||
'Сакральный символ («ухо»): архетипический образ в поле судьбы.',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(messages.last.content, contains('КАНОН ПУСТ'));
|
||||
expect(messages.last.content, contains('УХО'));
|
||||
expect(messages.last.content.toLowerCase(), isNot(contains('тропа уже почти закрыта')));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:words999/models/word_model.dart';
|
||||
import 'package:words999/services/manual_dive.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const snake = WordItem(
|
||||
id: 12,
|
||||
word: 'змей',
|
||||
partOfSpeech: 'существительное',
|
||||
frequency: 1,
|
||||
category: 'Образ',
|
||||
core: 'Сброс кожи оставляет старый покров на тропе.',
|
||||
);
|
||||
|
||||
test('role prompts name the sign and stay inside their closed menus', () {
|
||||
final cultures = cultureRoutePrompt(snake);
|
||||
expect(cultures, contains('«змей»'));
|
||||
expect(cultures, contains('Шумер и Месопотамия'));
|
||||
expect(cultures, contains('Алхимия и герметизм'));
|
||||
expect(cultures, contains('три названия через запятую'));
|
||||
|
||||
final languages = languageRoutePrompt(snake);
|
||||
expect(languages, contains('санскрит'));
|
||||
expect(languages, contains('церковнославянский'));
|
||||
|
||||
expect(mysticPrompt(snake), contains('Не занимай чужую карту'));
|
||||
expect(psychoPrompt(snake), contains('не архетип'));
|
||||
expect(cultureSlicePrompt(snake, 'Древний Китай'), contains('следа нет'));
|
||||
expect(languageSlicePrompt(snake, 'санскрит'), contains('прямого корня нет'));
|
||||
expect(traditionSlicePrompt(snake, 'Руны'), contains('Руны'));
|
||||
});
|
||||
|
||||
test('route answers map onto the closed registries', () {
|
||||
expect(
|
||||
matchCultureNames('Китай, Египет, выдуманная страна'),
|
||||
['Древний Китай', 'Древний Египет'],
|
||||
);
|
||||
expect(matchLanguageNames('греческий, китайский'), ['древнегреческий', 'древнекитайский']);
|
||||
});
|
||||
|
||||
test('synthesizer JSON fills cultures, a tradition and the core', () {
|
||||
const raw = '''
|
||||
```json
|
||||
{"word":"змей","core":"Кожа сходит кольцом.","oracle":"Старый покров остаётся.","shadow":"Яд в той же коже.","asks":"Какое кольцо уже снято?","step":"Положи кольцо за порог.","binds":["кожа","порог"],"cultures":{"Китай":"Лун отпустил чешую и не стал драконом воды."},"languages":{"санскрит":"прямого корня нет"},"layers":[{"system":"alchemy","title":"Алхимия","law":"Чернота снимает цвет, новое тело его ещё не держит."}]}
|
||||
```
|
||||
''';
|
||||
final draft = parseDiveCanon(raw);
|
||||
expect(draft, isNotNull);
|
||||
expect(draft!.core, 'Кожа сходит кольцом.');
|
||||
expect(draft.cultures, {'Древний Китай': 'Лун отпустил чешую и не стал драконом воды.'});
|
||||
expect(draft.languages, {'санскрит': 'прямого корня нет'});
|
||||
expect(draft.layers!.single.system, 'alchemy');
|
||||
expect(draft.binds, ['кожа', 'порог']);
|
||||
|
||||
final prompt = synthesizerPrompt(
|
||||
word: snake,
|
||||
reports: const {'mystic': 'закон кожи'},
|
||||
core: 'Кожа сходит кольцом.',
|
||||
oracle: '',
|
||||
shadow: '',
|
||||
asks: '',
|
||||
step: '',
|
||||
binds: '',
|
||||
cultures: draft.cultures!,
|
||||
languages: const {},
|
||||
traditions: const {},
|
||||
);
|
||||
expect(prompt, contains('«змей»'));
|
||||
expect(prompt, contains('закон кожи'));
|
||||
expect(prompt, contains('Лун отпустил чешую'));
|
||||
});
|
||||
|
||||
test('manual fields survive save, card and import', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final repo = WordsRepository();
|
||||
await repo.init(remoteDailyShuffle: false);
|
||||
await repo.clearAllCustomEdits();
|
||||
const word = 'змей';
|
||||
|
||||
await repo.saveCustomEdit(
|
||||
word,
|
||||
core: 'Кожа сходит кольцом.',
|
||||
oracle: 'Старый покров остаётся на тропе.',
|
||||
shadow: 'Яд остаётся в снятой коже.',
|
||||
asks: 'Какое кольцо уже снято?',
|
||||
step: 'Положи кольцо за порог.',
|
||||
cultures: const {'Древний Китай': 'Лун отпустил чешую.'},
|
||||
languages: const {'санскрит': 'прямого корня нет'},
|
||||
layers: const [
|
||||
SymbolLayer(system: 'alchemy', title: 'Алхимия', law: 'Чернота снимает цвет.'),
|
||||
],
|
||||
binds: const ['кожа'],
|
||||
reports: const {'mystic': 'закон кожи'},
|
||||
);
|
||||
|
||||
final edited = repo.getWordByName(word)!;
|
||||
expect(edited.core, 'Кожа сходит кольцом.');
|
||||
expect(edited.cultures['Древний Китай'], 'Лун отпустил чешую.');
|
||||
expect(edited.languages['санскрит'], 'прямого корня нет');
|
||||
expect(edited.layers.any((layer) => layer.system == 'alchemy'), isTrue);
|
||||
expect(edited.binds, ['кожа']);
|
||||
expect(repo.customEditOf(word)!['reports']['mystic'], 'закон кожи');
|
||||
|
||||
final exported = repo.exportCustomEditsJson();
|
||||
await repo.clearAllCustomEdits();
|
||||
expect(repo.getWordByName(word)!.languages, isEmpty);
|
||||
|
||||
final count = await repo.importCustomEditsJson(exported);
|
||||
expect(count, 1);
|
||||
final again = repo.getWordByName(word)!;
|
||||
expect(again.cultures['Древний Китай'], 'Лун отпустил чешую.');
|
||||
expect(again.languages['санскрит'], 'прямого корня нет');
|
||||
expect(again.layers.any((layer) => layer.law == 'Чернота снимает цвет.'), isTrue);
|
||||
await repo.clearAllCustomEdits();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('Markdown parser extracts all 5 blocks without loss or substring collisions', () {
|
||||
const input = '''```markdown
|
||||
## 1. ЯДРО / ФОРМУЛА ЗАКОНА:
|
||||
(1-2 чеканных предложения)
|
||||
Огонь — это первородная сингулярность трансформации материи в чистую радиацию духа.
|
||||
|
||||
## 2. ОРАКУЛ (МОНОЛИТНОЕ ЭССЕ):
|
||||
(5-8 абзацев)
|
||||
Праиндоевропейский корень *egni- фиксирует не субстанцию, а событие горения.
|
||||
В шумерском мифе бог Гибил нисходит в тигель трансформации.
|
||||
В алхимии огонь выступает универсальным растворителем формы ради освобождения скрытой субстанции.
|
||||
|
||||
## 3. ТЕНЕВАЯ ЛОВУШКА И ИСКАЖЕНИЕ:
|
||||
(Теневая ловушка принципа)
|
||||
Теневая ловушка принципа — испепеление контекста, мания тотального контроля через разрушение.
|
||||
|
||||
## 4. ЭКЗИСТЕНЦИАЛЬНЫЙ ВОПРОС:
|
||||
(Острый коан)
|
||||
Что именно в твоей структуре ты отказываешься отдать пламени обновления?
|
||||
|
||||
## 5. РИТУАЛЬНЫЙ ФИЗИЧЕСКИЙ ЖЕСТ:
|
||||
(Конкретное микродействие)
|
||||
Взять сухую щепку, разломить её надвое и бросить в открытый огонь, не оборачиваясь.
|
||||
|
||||
Метрика: 4120 символов
|
||||
```''';
|
||||
|
||||
Map<String, String> parseRevisionBlocks(String text) {
|
||||
final blocks = <String, String>{};
|
||||
if (text.trim().isEmpty) return blocks;
|
||||
|
||||
var src = text.trim();
|
||||
if (src.startsWith('```')) {
|
||||
src = src.replaceFirst(RegExp(r'^```[a-zA-Z]*\n?'), '');
|
||||
if (src.endsWith('```')) {
|
||||
src = src.substring(0, src.length - 3).trim();
|
||||
}
|
||||
}
|
||||
|
||||
final lines = src.replaceAll('\r\n', '\n').split('\n');
|
||||
|
||||
String? detectSlot(String line) {
|
||||
final stripped = line.trim();
|
||||
if (stripped.isEmpty || stripped.length > 95) return null;
|
||||
|
||||
final isHeaderFormat = stripped.startsWith('#') ||
|
||||
stripped.startsWith('*') ||
|
||||
stripped.startsWith('[') ||
|
||||
RegExp(r'^(?:\d+[\.\)]|[IVX]+[\.\)])').hasMatch(stripped) ||
|
||||
(stripped.length < 70 && (stripped.endsWith(':') || stripped == stripped.toUpperCase())) ||
|
||||
RegExp(r'\b(?:ЯДРО|ОРАКУЛ|ТЕНЬ|ВОПРОС|ЖЕСТ|КОАН)\b', caseSensitive: false).hasMatch(stripped);
|
||||
|
||||
if (!isHeaderFormat) return null;
|
||||
|
||||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ЯДР[ОАЕУ]|ФОРМУЛ[А-Яа-я]*|ОНТОЛОГИ[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||||
return 'ЯДРО';
|
||||
}
|
||||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ВОПРОС[А-Яа-я]*|КОАН[А-Яа-я]*|РЕФЛЕКСИ[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||||
return 'ВОПРОС';
|
||||
}
|
||||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ТЕН[ЬЯЕИЮ]|ТЕНЕВ[А-Яа-я]*|ЛОВУШК[А-Яа-я]*|ИСКАЖЕН[А-Яа-я]*|СЛЕП[А-Яа-я]*\s+ЗОН[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||||
return 'ТЕНЬ';
|
||||
}
|
||||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ЖЕСТ[А-Яа-я]*|МИКРОДЕЙСТВИ[А-Яа-я]*|ДЕЙСТВИ[ЕЯ]|ПРАКТИК[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||||
return 'ЖЕСТ';
|
||||
}
|
||||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ОРАКУЛ[А-Яа-я]*|СТАТЬ[ЯЕИЮ]|ЭССЕ|ТОЛКОВАНИ[А-Яа-я]*|ОБОГАЩЕНИ[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||||
return 'ОРАКУЛ';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
final headers = <({int lineIdx, String slot})>[];
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final slot = detectSlot(lines[i]);
|
||||
if (slot != null) {
|
||||
headers.add((lineIdx: i, slot: slot));
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
final startLineIdx = headers[i].lineIdx + 1;
|
||||
final slot = headers[i].slot;
|
||||
final endLineIdx = (i + 1 < headers.length) ? headers[i + 1].lineIdx : lines.length;
|
||||
|
||||
var content = lines.sublist(startLineIdx, endLineIdx).join('\n').trim();
|
||||
|
||||
content = content.replaceFirst(
|
||||
RegExp(r'^\s*\([^)]*(?:предложен|абзац|коан|действи|формул|ловушк|стать)[^)]*\)\s*\n?', caseSensitive: false),
|
||||
'',
|
||||
).trim();
|
||||
|
||||
content = content.replaceAll(
|
||||
RegExp(r'(?:\n|\r\n)\s*(?:Метрика|Количество символов|Символов|Длина текста)[:—–\s][\s\S]*$', caseSensitive: false),
|
||||
'',
|
||||
).trim();
|
||||
|
||||
if (content.isNotEmpty) {
|
||||
if (!blocks.containsKey(slot) || content.length > blocks[slot]!.length) {
|
||||
blocks[slot] = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
final result = parseRevisionBlocks(input);
|
||||
expect(result.length, 5);
|
||||
expect(result.containsKey('ЯДРО'), true);
|
||||
expect(result.containsKey('ОРАКУЛ'), true);
|
||||
expect(result.containsKey('ТЕНЬ'), true);
|
||||
expect(result.containsKey('ВОПРОС'), true);
|
||||
expect(result.containsKey('ЖЕСТ'), true);
|
||||
|
||||
expect(result['ЯДРО']!.startsWith('Огонь — это первородная'), true);
|
||||
expect(result['ОРАКУЛ']!.contains('Праиндоевропейский корень'), true);
|
||||
expect(result['ТЕНЬ']!.contains('Теневая ловушка принципа'), true);
|
||||
expect(result['ВОПРОС']!.contains('Что именно в твоей структуре'), true);
|
||||
expect(result['ЖЕСТ']!.contains('Взять сухую щепку'), true);
|
||||
expect(result['ЖЕСТ']!.contains('Метрика'), false);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/models/syncretic_session_model.dart';
|
||||
import 'package:words999/models/divination_instructions.dart';
|
||||
import 'package:words999/screens/numerology_divination_screen.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Numerology & Pythagorean Square Tests', () {
|
||||
test('Instructions repository contains numerology entry', () {
|
||||
final instruction = DivinationInstructionsRepository.get('numerology');
|
||||
expect(instruction.id, equals('numerology'));
|
||||
expect(instruction.title, contains('Пифагор'));
|
||||
expect(instruction.icon, equals('🔢'));
|
||||
expect(instruction.physicalDiceMethod, isNotEmpty);
|
||||
expect(instruction.directInputMethod, isNotEmpty);
|
||||
});
|
||||
|
||||
test('Canonical Alexandrian working numbers calculation (e.g. 15.07.1990)', () {
|
||||
// Date: 15.07.1990
|
||||
// Digits: 1, 5, 0, 7, 1, 9, 9, 0
|
||||
// W1: 1+5+0+7+1+9+9+0 = 32
|
||||
// W2: 3+2 = 5
|
||||
// W3: 32 - 2 * 1 = 30
|
||||
// W4: 3+0 = 3
|
||||
const day = 15;
|
||||
const month = 7;
|
||||
const year = 1990;
|
||||
|
||||
final digits = [1, 5, 0, 7, 1, 9, 9, 0];
|
||||
final w1 = digits.reduce((a, b) => a + b);
|
||||
expect(w1, equals(32));
|
||||
|
||||
final w2 = (3 + 2);
|
||||
expect(w2, equals(5));
|
||||
|
||||
final w3 = w1 - (2 * 1);
|
||||
expect(w3, equals(30));
|
||||
|
||||
final w4 = 3 + 0;
|
||||
expect(w4, equals(3));
|
||||
|
||||
// Digit pool:
|
||||
// Date: 1, 5, 0, 7, 1, 9, 9, 0
|
||||
// W1: 3, 2
|
||||
// W2: 5
|
||||
// W3: 3, 0
|
||||
// W4: 3
|
||||
// Counts for 1..9:
|
||||
// 1: 1, 1 -> 2
|
||||
// 2: 2 -> 1
|
||||
// 3: 3, 3, 3 -> 3
|
||||
// 4: 0
|
||||
// 5: 5, 5 -> 2
|
||||
// 6: 0
|
||||
// 7: 7 -> 1
|
||||
// 8: 0
|
||||
// 9: 9, 9 -> 2
|
||||
final pool = [1, 5, 0, 7, 1, 9, 9, 0, 3, 2, 5, 3, 0, 3];
|
||||
expect(pool.where((d) => d == 1).length, equals(2));
|
||||
expect(pool.where((d) => d == 2).length, equals(1));
|
||||
expect(pool.where((d) => d == 3).length, equals(3));
|
||||
expect(pool.where((d) => d == 4).length, equals(0));
|
||||
expect(pool.where((d) => d == 5).length, equals(2));
|
||||
expect(pool.where((d) => d == 7).length, equals(1));
|
||||
expect(pool.where((d) => d == 9).length, equals(2));
|
||||
});
|
||||
|
||||
test('SyncreticReading accepts numerology format', () {
|
||||
final reading = SyncreticReading(
|
||||
id: 'num_test_1',
|
||||
systemId: 'numerology',
|
||||
systemName: 'Нумерология и Квадрат Пифагора',
|
||||
systemIcon: '🔢',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: 'Психоматрица 3×3 (ЧЖП: 5)',
|
||||
inputMethod: 'manual',
|
||||
items: [
|
||||
const ReadingSignItem(
|
||||
title: 'Число Жизненного Пути: 5',
|
||||
subtitle: '5 — Искатель Свободы и Странник',
|
||||
symbol: '5',
|
||||
lore: 'Динамические перемены и расширение горизонтов.',
|
||||
),
|
||||
const ReadingSignItem(
|
||||
title: 'Ячейка 1: Характер (11)',
|
||||
subtitle: 'Норма',
|
||||
symbol: '1',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final map = reading.toMap();
|
||||
final revived = SyncreticReading.fromMap(map);
|
||||
expect(revived.systemId, equals('numerology'));
|
||||
expect(revived.items.length, equals(2));
|
||||
expect(revived.items.first.title, contains('Число Жизненного Пути: 5'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/models/divination_layout.dart';
|
||||
import 'package:words999/models/divination_instructions.dart';
|
||||
import 'package:words999/services/ogham_feda_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Celtic Ogham Divination System Tests', () {
|
||||
test('Ogham layouts exist with correct counts', () {
|
||||
expect(DivinationLayouts.oghamLayouts.length, 3);
|
||||
expect(DivinationLayouts.oghamLayouts[0].count, 1);
|
||||
expect(DivinationLayouts.oghamLayouts[1].count, 3);
|
||||
expect(DivinationLayouts.oghamLayouts[2].count, 5);
|
||||
});
|
||||
|
||||
test('Ogham instruction is fully defined with history and rules', () {
|
||||
final instr = DivinationInstructionsRepository.get('ogham');
|
||||
expect(instr.id, 'ogham');
|
||||
expect(instr.title, contains('Огам'));
|
||||
expect(instr.icon, '᚛');
|
||||
expect(instr.historyAndEssence.isNotEmpty, true);
|
||||
expect(instr.physicalDiceMethod.isNotEmpty, true);
|
||||
expect(instr.sacredRules.length, greaterThanOrEqualTo(3));
|
||||
});
|
||||
|
||||
test('OghamFedaRepository loads all 20 sacred fedas', () async {
|
||||
final repo = OghamFedaRepository();
|
||||
await repo.init();
|
||||
expect(repo.allFedas.length, 20);
|
||||
|
||||
final beith = repo.byCanonId('beith');
|
||||
expect(beith, isNotNull);
|
||||
expect(beith!.symbol, 'ᚁ');
|
||||
expect(beith.name, contains('Бэйт'));
|
||||
expect(beith.shadow.isNotEmpty, true);
|
||||
|
||||
final idho = repo.byCanonId('idho');
|
||||
expect(idho, isNotNull);
|
||||
expect(idho!.symbol, 'ᚔ');
|
||||
expect(idho.name, contains('Идо'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
import 'package:words999/services/traditions_repository.dart';
|
||||
import 'package:words999/services/lenormand_pairs_repository.dart';
|
||||
import 'package:words999/services/tarot_majors_repository.dart';
|
||||
import 'package:words999/services/ogham_feda_repository.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
await WordsRepository().init();
|
||||
await TraditionsRepository().init();
|
||||
await LenormandPairsRepository().init();
|
||||
await TarotMajorsRepository().init();
|
||||
await OghamFedaRepository().init();
|
||||
});
|
||||
|
||||
test('Traditions matches for sample words in lexicon', () {
|
||||
final repo = TraditionsRepository();
|
||||
expect(repo.traditions.isNotEmpty, true);
|
||||
|
||||
// Look for matches for known symbols
|
||||
final matchesVtor = repo.findMatchesForWord('вторник');
|
||||
expect(matchesVtor.isNotEmpty, true);
|
||||
expect(matchesVtor.any((m) => m.tradition.id == 'days_of_week'), true);
|
||||
|
||||
final matchesKlever = repo.findMatchesForWord('клевер');
|
||||
expect(matchesKlever.isNotEmpty, true);
|
||||
expect(matchesKlever.any((m) => m.tradition.id == 'lenormand'), true);
|
||||
|
||||
final matchesShut = repo.findMatchesForWord('шут');
|
||||
expect(matchesShut.isNotEmpty, true);
|
||||
expect(matchesShut.any((m) => m.tradition.id == 'tarot'), true);
|
||||
});
|
||||
|
||||
test('Words retain both canonical description and oracle meaning', () {
|
||||
final wordsRepo = WordsRepository();
|
||||
final sample = wordsRepo.allWords.take(50);
|
||||
for (final w in sample) {
|
||||
// canonical description must never be empty
|
||||
expect(w.description.isNotEmpty, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:words999/models/houses_session_model.dart';
|
||||
import 'package:words999/models/word_model.dart';
|
||||
import 'package:words999/services/houses_service.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
import 'package:words999/services/oracle_master_prompt_generator.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Querent & Master Prompt Tests', () {
|
||||
late HousesService service;
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final repo = WordsRepository();
|
||||
await repo.init(remoteDailyShuffle: false);
|
||||
service = HousesService();
|
||||
});
|
||||
|
||||
test('Querent is properly initialized and anchored in session', () {
|
||||
const querent = WordItem(
|
||||
id: 1,
|
||||
word: 'ИСТИНА',
|
||||
partOfSpeech: 'существительное',
|
||||
frequency: 50.0,
|
||||
core: 'Незыблемое основание бытия',
|
||||
category: 'Философия',
|
||||
description: 'Чистый свет разума',
|
||||
oracle: 'Всякая тень исчезает перед ликом Истины',
|
||||
shadow: 'Фанатичная убежденность',
|
||||
asks: 'В чем ты боишься признаться себе?',
|
||||
step: 'Взгляни правде в глаза',
|
||||
binds: ['Искажение'],
|
||||
);
|
||||
|
||||
final session = service.createNewSession(
|
||||
question: 'Куда ведет путь?',
|
||||
querentWord: querent,
|
||||
querentHouseIndex: 42,
|
||||
);
|
||||
|
||||
expect(session.querentWord, isNotNull);
|
||||
expect(session.querentWord!.word, 'ИСТИНА');
|
||||
expect(session.querentHouseIndex, 42);
|
||||
expect(session.question, 'Куда ведет путь?');
|
||||
});
|
||||
|
||||
test('Master Prompt compiles full 3-tier hierarchy and sacred geometry', () {
|
||||
const querent = WordItem(
|
||||
id: 2,
|
||||
word: 'ПУТНИК',
|
||||
partOfSpeech: 'существительное',
|
||||
frequency: 45.0,
|
||||
core: 'Движение сквозь миры',
|
||||
category: 'Архетип',
|
||||
description: 'Идущий сквозь пространство и время',
|
||||
oracle: 'Дорога рождается под ногами идущего',
|
||||
shadow: 'Блуждание без цели',
|
||||
asks: 'Каков твой истинный пункт назначения?',
|
||||
step: 'Сделай первый шаг в неизведанное',
|
||||
binds: ['Застой'],
|
||||
);
|
||||
|
||||
var session = service.createNewSession(
|
||||
question: 'Что ожидает на пороге нового цикла?',
|
||||
querentWord: querent,
|
||||
querentHouseIndex: 100,
|
||||
);
|
||||
|
||||
// Open 2 houses with guests
|
||||
final cell1 = session.grid[10];
|
||||
const guest1 = WordItem(
|
||||
id: 3,
|
||||
word: 'ОГОНЬ',
|
||||
partOfSpeech: 'существительное',
|
||||
frequency: 60.0,
|
||||
core: 'Первичная искра трансформации',
|
||||
category: 'Стихия',
|
||||
description: 'Пламя очищения',
|
||||
oracle: 'Огонь сжигает ветхое',
|
||||
shadow: 'Разрушительная ярость',
|
||||
asks: 'Что требует очищения?',
|
||||
step: 'Зажги внутреннее пламя',
|
||||
binds: ['Пепел'],
|
||||
);
|
||||
session = service.revealHouse(session, cell: cell1, guestWord: guest1);
|
||||
|
||||
final cell2 = session.grid[25];
|
||||
const guest2 = WordItem(
|
||||
id: 4,
|
||||
word: 'ЗЕРКАЛО',
|
||||
partOfSpeech: 'существительное',
|
||||
frequency: 55.0,
|
||||
core: 'Отражение скрытой сущности',
|
||||
category: 'Символ',
|
||||
description: 'Гладь осознания',
|
||||
oracle: 'То, что ты видишь снаружи — внутри тебя',
|
||||
shadow: 'Иллюзия и самообман',
|
||||
asks: 'Что отражается в твоем сердце?',
|
||||
step: 'Прими свое отражение',
|
||||
binds: ['Кривое зеркало'],
|
||||
);
|
||||
session = service.revealHouse(session, cell: cell2, guestWord: guest2);
|
||||
|
||||
final prompt = OracleMasterPromptGenerator.generate(session);
|
||||
|
||||
expect(prompt, contains('ОРАКУЛ 999'));
|
||||
expect(prompt, contains('ПУТНИК'));
|
||||
expect(prompt, contains('Что ожидает на пороге нового цикла?'));
|
||||
expect(prompt, contains('ОГОНЬ'));
|
||||
expect(prompt, contains('ЗЕРКАЛО'));
|
||||
expect(prompt, contains('АРХОНТ'));
|
||||
expect(prompt, contains('ДОМ'));
|
||||
expect(prompt, contains('ГОСТЬ'));
|
||||
expect(prompt, contains('САКРАЛЬНАЯ ГЕОМЕТРИЯ'));
|
||||
expect(prompt, contains('ИНСТРУКЦИЯ ДЛЯ ТОЛКОВАНИЯ'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/models/history_entry_model.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
import 'package:words999/services/traditions_repository.dart';
|
||||
import 'package:words999/services/lenormand_pairs_repository.dart';
|
||||
import 'package:words999/services/oracle_master_prompt_generator.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
await WordsRepository().init();
|
||||
await TraditionsRepository().init();
|
||||
await LenormandPairsRepository().init();
|
||||
});
|
||||
|
||||
test('Generates complete high-caliber Master Prompt for OracleSession', () {
|
||||
final repo = WordsRepository();
|
||||
final w1 = repo.allWords[0];
|
||||
final w2 = repo.allWords[1];
|
||||
final w3 = repo.allWords[2];
|
||||
|
||||
final session = OracleSession(
|
||||
id: 'test_session_1',
|
||||
timestamp: DateTime.now(),
|
||||
question: 'Какой следующий шаг в работе?',
|
||||
wordsCount: 3,
|
||||
words: [
|
||||
SessionWord.fromWordItem(w1, positionTitle: 'Исток'),
|
||||
SessionWord.fromWordItem(w2, positionTitle: 'Кризис'),
|
||||
SessionWord.fromWordItem(w3, positionTitle: 'Исход'),
|
||||
],
|
||||
);
|
||||
|
||||
final prompt = OracleMasterPromptGenerator.generateForSession(session);
|
||||
expect(prompt.isNotEmpty, true);
|
||||
expect(prompt.contains('СВЯЩЕННЫЙ РАСКЛАД КЛЕРОМАНТИИ'), true);
|
||||
expect(prompt.contains('Какой следующий шаг в работе?'), true);
|
||||
expect(prompt.contains(w1.word.toUpperCase()), true);
|
||||
expect(prompt.contains(w2.word.toUpperCase()), true);
|
||||
expect(prompt.contains(w3.word.toUpperCase()), true);
|
||||
expect(prompt.contains('Энергетический баланс цепи'), true);
|
||||
expect(prompt.contains('ТРЕБОВАНИЯ К ТВОЕМУ ОТВЕТУ'), true);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/models/syncretic_session_model.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
import 'package:words999/services/traditions_repository.dart';
|
||||
import 'package:words999/services/oracle_master_prompt_generator.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
await WordsRepository().init();
|
||||
await TraditionsRepository().init();
|
||||
});
|
||||
|
||||
test('SyncreticSession accumulates multiple diverse readings and generates Meta-Prompt', () {
|
||||
final repo = WordsRepository();
|
||||
|
||||
final r1 = SyncreticReading(
|
||||
id: 'test_r1',
|
||||
systemId: 'cleromancy',
|
||||
systemName: 'Оракул 999 (Клеромантия)',
|
||||
systemIcon: '🎲',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: '2 знака',
|
||||
inputMethod: 'dice',
|
||||
items: [
|
||||
ReadingSignItem(
|
||||
title: '#142 ВОЛЯ',
|
||||
subtitle: 'Существительное',
|
||||
symbol: '🎲',
|
||||
lore: 'Импульс намерения',
|
||||
linkedWord: repo.allWords[0],
|
||||
),
|
||||
],
|
||||
question: 'Как реализовать проект?',
|
||||
);
|
||||
|
||||
final r2 = SyncreticReading(
|
||||
id: 'test_r2',
|
||||
systemId: 'runes',
|
||||
systemName: 'Руны Старшего Футарка (24)',
|
||||
systemIcon: 'ᚱ',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: 'Бросок 2×d12',
|
||||
inputMethod: 'dice',
|
||||
items: [
|
||||
ReadingSignItem(
|
||||
title: 'Феху (Fehu)',
|
||||
subtitle: 'Этт Фрейи',
|
||||
symbol: 'ᚠ',
|
||||
lore: 'Скот, богатство, первичный огонь творения.',
|
||||
isReversed: false,
|
||||
linkedWord: repo.allWords[1],
|
||||
),
|
||||
],
|
||||
question: 'Как реализовать проект?',
|
||||
);
|
||||
|
||||
final r3 = SyncreticReading(
|
||||
id: 'test_r3',
|
||||
systemId: 'lenormand',
|
||||
systemName: 'Малый Оракул Ленорман',
|
||||
systemIcon: '🪶',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: 'd6+d12: Карта #24',
|
||||
inputMethod: 'dice',
|
||||
items: [
|
||||
ReadingSignItem(
|
||||
title: 'Карта 24 «Сердце»',
|
||||
subtitle: 'Любовь, искренность, страсть',
|
||||
symbol: '#24',
|
||||
lore: 'Сердце указывает на чистую эмоциональную вовлечённость.',
|
||||
linkedWord: repo.allWords[2],
|
||||
),
|
||||
],
|
||||
question: 'Как реализовать проект?',
|
||||
);
|
||||
|
||||
final session = SyncreticSession(
|
||||
id: 'syn_test_day',
|
||||
date: DateTime.now(),
|
||||
mainQuestion: 'Как реализовать проект?',
|
||||
readings: [r1, r2, r3],
|
||||
);
|
||||
|
||||
expect(session.readings.length, 3);
|
||||
|
||||
final metaPrompt = OracleMasterPromptGenerator.generateCrossSystemComparison(session);
|
||||
expect(metaPrompt.isNotEmpty, true);
|
||||
expect(metaPrompt.contains('ВЕЛИКИЙ МЕТА-МАСТЕР-ПРОМПТ'), true);
|
||||
expect(metaPrompt.contains('Как реализовать проект?'), true);
|
||||
expect(metaPrompt.contains('ОРАКУЛ 999'), true);
|
||||
expect(metaPrompt.contains('РУНЫ СТАРШЕГО ФУТАРКА'), true);
|
||||
expect(metaPrompt.contains('МАЛЫЙ ОРАКУЛ ЛЕНОРМАН'), true);
|
||||
expect(metaPrompt.contains('ТОЧКА КОНВЕРГЕНЦИИ'), true);
|
||||
expect(metaPrompt.contains('ТОЧКИ ДИВЕРГЕНЦИИ И ПОЛЯРНОСТИ'), true);
|
||||
});
|
||||
|
||||
test('Matrix chains reading can be recorded and serialized in SyncreticSession', () {
|
||||
final repo = WordsRepository();
|
||||
|
||||
final chainReading = SyncreticReading(
|
||||
id: 'chains_session_99',
|
||||
systemId: 'matrix_chains',
|
||||
systemName: 'Матрица 33×30: Цепочки Домов',
|
||||
systemIcon: '🔗',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: '1 цепочка',
|
||||
inputMethod: 'dice',
|
||||
items: [
|
||||
ReadingSignItem(
|
||||
title: 'Цепочка #1: Петля Судьбы',
|
||||
subtitle: 'Старт: Дом #1 «ОГОНЬ» • Шагов: 3',
|
||||
symbol: '🔄',
|
||||
lore: 'Траектория: «ВОДА» ➔ «ЗЕМЛЯ» ➔ «ОГОНЬ»\nЗамкнутая петля судьбы.',
|
||||
linkedWord: repo.allWords[0],
|
||||
meta: {
|
||||
'chainIndex': 1,
|
||||
'endType': 'loop',
|
||||
'stepsCount': 3,
|
||||
'path': '«ВОДА» ➔ «ЗЕМЛЯ» ➔ «ОГОНЬ»',
|
||||
},
|
||||
),
|
||||
],
|
||||
question: 'Вектор развития',
|
||||
);
|
||||
|
||||
final session = SyncreticSession(
|
||||
id: 'syn_matrix_chains_test',
|
||||
date: DateTime.now(),
|
||||
mainQuestion: 'Вектор развития',
|
||||
readings: [chainReading],
|
||||
);
|
||||
|
||||
final jsonStr = session.toJson();
|
||||
final restored = SyncreticSession.fromJson(jsonStr);
|
||||
|
||||
expect(restored.readings.length, 1);
|
||||
expect(restored.readings.first.systemId, 'matrix_chains');
|
||||
expect(restored.readings.first.items.first.title, 'Цепочка #1: Петля Судьбы');
|
||||
expect(restored.readings.first.items.first.symbol, '🔄');
|
||||
expect(restored.readings.first.items.first.meta?['endType'], 'loop');
|
||||
|
||||
final metaPrompt = OracleMasterPromptGenerator.generateCrossSystemComparison(restored);
|
||||
expect(metaPrompt.contains('МАТРИЦА 33×30: ЦЕПОЧКИ ДОМОВ'), true);
|
||||
expect(metaPrompt.contains('ЦЕПОЧКА #1: ПЕТЛЯ СУДЬБЫ'), true);
|
||||
});
|
||||
|
||||
}
|
||||
+73
-6
@@ -1,30 +1,97 @@
|
||||
import "dart:ui";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:words999/main.dart';
|
||||
import 'package:words999/services/lenormand_pairs_repository.dart';
|
||||
import 'package:words999/services/tarot_majors_repository.dart';
|
||||
import 'package:words999/services/settings_repository.dart';
|
||||
import 'package:words999/services/traditions_repository.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('App smoke test - launch oracle and verify navigation', (WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
tester.view.devicePixelRatio = 2.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
await WordsRepository().init(remoteDailyShuffle: false);
|
||||
await TraditionsRepository().init();
|
||||
await LenormandPairsRepository().init();
|
||||
await TarotMajorsRepository().init();
|
||||
await SettingsRepository().init();
|
||||
await SettingsRepository().setRandomOrgApiKey('');
|
||||
await tester.pumpWidget(const Words999App());
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.text('Оракул 999'), findsOneWidget);
|
||||
expect(find.text('Оракул'), findsOneWidget);
|
||||
// Verify all tabs in order: Оракул, Матрица, Справочник, История, Настройки
|
||||
expect(find.text('Оракул'), findsAtLeast(1));
|
||||
expect(find.text('Справочник'), findsOneWidget);
|
||||
expect(find.text('История'), findsOneWidget);
|
||||
expect(find.text('Настройки'), findsOneWidget);
|
||||
|
||||
// Open Оракул 999 from hub to verify persona picker
|
||||
await tester.tap(find.text('Оракул 999'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Кто толкует знаки'), findsWidgets);
|
||||
expect(find.text('Ясный оракул'), findsWidgets);
|
||||
// Go back to Hub
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to Матрица from Divination Hub
|
||||
await tester.tap(find.text('Матрица 33×30'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Полотно Домов 33×30'), findsOneWidget);
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to Справочник tab
|
||||
await tester.tap(find.text('Справочник'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Справочник 999'), findsOneWidget);
|
||||
expect(find.text('🔤 Слова Оракула'), findsNothing);
|
||||
expect(find.text('🏛️ Каноны Традиций'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('ᚱ Руны'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('24 из 24'), findsOneWidget);
|
||||
expect(find.textContaining('Феху'), findsOneWidget);
|
||||
|
||||
await tester.ensureVisible(find.text('☯️ И Цзин'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('☯️ И Цзин'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('72 из 72'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'Вэй-цзи');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Гексаграмма 64'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), '');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.ensureVisible(find.text('🪶 Ленорман'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('🪶 Ленорман'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Описания и сочетания'), findsWidgets);
|
||||
await tester.tap(find.text('Описания и сочетания').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Значение'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(
|
||||
find.textContaining('Сочетания ('),
|
||||
400,
|
||||
scrollable: find.byType(Scrollable).last,
|
||||
);
|
||||
expect(find.textContaining('Сочетания ('), findsOneWidget);
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to Настройки tab
|
||||
await tester.tap(find.text('Настройки'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Голос толкователя'), findsOneWidget);
|
||||
expect(find.text('Ясный оракул'), findsOneWidget);
|
||||
expect(find.text('Древний орк-оракул'), findsOneWidget);
|
||||
expect(find.text('Техножрец'), findsOneWidget);
|
||||
expect(find.text('Психоаналитик'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:words999/models/syncretic_session_model.dart';
|
||||
import 'package:words999/models/word_of_day_model.dart';
|
||||
import 'package:words999/services/word_of_the_day_service.dart';
|
||||
import 'package:words999/services/words_repository.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
await WordsRepository().init(remoteDailyShuffle: false);
|
||||
await WordOfTheDayService().init();
|
||||
});
|
||||
|
||||
group('WordOfTheDayService & Model Tests', () {
|
||||
test('Date key format and parse consistency', () {
|
||||
final date = DateTime(2026, 9, 28);
|
||||
final key = WordOfTheDayService.formatDateKey(date);
|
||||
expect(key, '2026-09-28');
|
||||
|
||||
final parsed = WordOfTheDayService.parseDateKey(key);
|
||||
expect(parsed.year, 2026);
|
||||
expect(parsed.month, 9);
|
||||
expect(parsed.day, 28);
|
||||
});
|
||||
|
||||
test('Deterministic daily word hash is consistent for identical dates', () {
|
||||
final date1 = DateTime(2026, 9, 28);
|
||||
final date2 = DateTime(2026, 9, 28, 23, 59, 59);
|
||||
|
||||
final idx1 = WordOfTheDayService.calculateWordIndexForDate(date1, 1000);
|
||||
final idx2 = WordOfTheDayService.calculateWordIndexForDate(date2, 1000);
|
||||
expect(idx1, equals(idx2));
|
||||
|
||||
final word1 = WordOfTheDayService().getWordForDate(date1);
|
||||
final word2 = WordOfTheDayService().getWordForDate(date2);
|
||||
expect(word1.id, equals(word2.id));
|
||||
expect(word1.word, equals(word2.word));
|
||||
});
|
||||
|
||||
test('Adjacent dates yield distinct words across calendar', () {
|
||||
final day1 = DateTime(2026, 9, 27);
|
||||
final day2 = DateTime(2026, 9, 28);
|
||||
final day3 = DateTime(2026, 9, 29);
|
||||
|
||||
final idx1 = WordOfTheDayService.calculateWordIndexForDate(day1, 1000);
|
||||
final idx2 = WordOfTheDayService.calculateWordIndexForDate(day2, 1000);
|
||||
final idx3 = WordOfTheDayService.calculateWordIndexForDate(day3, 1000);
|
||||
|
||||
expect(idx1, isNot(equals(idx2)));
|
||||
expect(idx2, isNot(equals(idx3)));
|
||||
});
|
||||
|
||||
test('Triptych mode provides 3 distinct words for Morning, Day, and Evening', () {
|
||||
final date = DateTime(2026, 9, 28);
|
||||
final triptych = WordOfTheDayService().getTriptychWordsForDate(date);
|
||||
|
||||
expect(triptych.morning.id, isNot(equals(triptych.day.id)));
|
||||
expect(triptych.day.id, isNot(equals(triptych.evening.id)));
|
||||
expect(triptych.morning.id, isNot(equals(triptych.evening.id)));
|
||||
});
|
||||
|
||||
test('WordOfDayRecord JSON serialization and copyWith with Triptych', () {
|
||||
const rec = WordOfDayRecord(
|
||||
dateKey: '2026-09-28',
|
||||
wordId: 42,
|
||||
isRevealed: true,
|
||||
isStepCompleted: false,
|
||||
diaryNote: 'Удивительная синхрония в разговоре',
|
||||
isTriptychMode: true,
|
||||
morningWordId: 10,
|
||||
dayWordId: 20,
|
||||
eveningWordId: 30,
|
||||
isMorningRevealed: true,
|
||||
isDayRevealed: true,
|
||||
isEveningRevealed: false,
|
||||
isDayStepCompleted: true,
|
||||
);
|
||||
|
||||
final jsonStr = rec.toJson();
|
||||
final decoded = WordOfDayRecord.fromJson(jsonStr);
|
||||
|
||||
expect(decoded.dateKey, '2026-09-28');
|
||||
expect(decoded.wordId, 42);
|
||||
expect(decoded.isTriptychMode, isTrue);
|
||||
expect(decoded.morningWordId, 10);
|
||||
expect(decoded.dayWordId, 20);
|
||||
expect(decoded.eveningWordId, 30);
|
||||
expect(decoded.isMorningRevealed, isTrue);
|
||||
expect(decoded.isDayRevealed, isTrue);
|
||||
expect(decoded.isEveningRevealed, isFalse);
|
||||
expect(decoded.isDayStepCompleted, isTrue);
|
||||
expect(decoded.isAnyRevealed, isTrue);
|
||||
expect(decoded.areAllTriptychRevealed, isFalse);
|
||||
|
||||
final allRevealed = decoded.copyWith(isEveningRevealed: true);
|
||||
expect(allRevealed.areAllTriptychRevealed, isTrue);
|
||||
});
|
||||
|
||||
test('Reveal specific triptych phases and persist', () async {
|
||||
final service = WordOfTheDayService();
|
||||
final testDate = DateTime(2026, 11, 20);
|
||||
|
||||
await service.setRecordTriptychMode(testDate, true);
|
||||
var rec = service.getRecord(testDate);
|
||||
expect(rec.isTriptychMode, isTrue);
|
||||
expect(rec.isMorningRevealed, isFalse);
|
||||
|
||||
await service.revealWord(testDate, phase: 'morning');
|
||||
rec = service.getRecord(testDate);
|
||||
expect(rec.isMorningRevealed, isTrue);
|
||||
expect(rec.isDayRevealed, isFalse);
|
||||
|
||||
await service.revealWord(testDate, phase: 'day');
|
||||
rec = service.getRecord(testDate);
|
||||
expect(rec.isDayRevealed, isTrue);
|
||||
expect(rec.isEveningRevealed, isFalse);
|
||||
|
||||
await service.revealWord(testDate, phase: 'evening');
|
||||
rec = service.getRecord(testDate);
|
||||
expect(rec.isEveningRevealed, isTrue);
|
||||
expect(rec.areAllTriptychRevealed, isTrue);
|
||||
|
||||
await service.toggleStepCompleted(testDate, true, isDayStep: true);
|
||||
rec = service.getRecord(testDate);
|
||||
expect(rec.isDayStepCompleted, isTrue);
|
||||
});
|
||||
|
||||
test('SyncreticSession can incorporate Triptych reading', () async {
|
||||
final triptych = WordOfTheDayService().getTriptychWordsForDate(DateTime(2026, 9, 28));
|
||||
|
||||
final reading = SyncreticReading(
|
||||
id: 'trip_test_1',
|
||||
systemId: 'word_of_day',
|
||||
systemName: 'Триптих Дня',
|
||||
systemIcon: '⏳',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: 'Триптих Суток',
|
||||
items: [
|
||||
ReadingSignItem(
|
||||
title: 'Утро: ${triptych.morning.word}',
|
||||
linkedWord: triptych.morning,
|
||||
),
|
||||
ReadingSignItem(
|
||||
title: 'День: ${triptych.day.word}',
|
||||
linkedWord: triptych.day,
|
||||
),
|
||||
ReadingSignItem(
|
||||
title: 'Вечер: ${triptych.evening.word}',
|
||||
linkedWord: triptych.evening,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final repo = WordsRepository();
|
||||
await repo.addReadingToSyncreticSession(reading);
|
||||
|
||||
final current = repo.currentSyncreticSession;
|
||||
expect(current.readings.any((r) => r.id == 'trip_test_1'), isTrue);
|
||||
expect(current.readings.firstWhere((r) => r.id == 'trip_test_1').items.length, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -121,5 +121,63 @@ void main() {
|
||||
await repo.clearHistory();
|
||||
expect(repo.sessions.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('Custom edits: save, export, import, reset, and clear', () async {
|
||||
expect(repo.customEditsCount, equals(0));
|
||||
|
||||
final testWord = repo.getWordBySessionNumber(100)!.word;
|
||||
final originalWord = repo.getWordByName(testWord)!;
|
||||
expect(originalWord.isUserEdited, isFalse);
|
||||
|
||||
// Save custom edit
|
||||
await repo.saveCustomEdit(
|
||||
testWord,
|
||||
core: 'Тестовое ядро от пользователя',
|
||||
oracle: 'Тестовое толкование от пользователя',
|
||||
shadow: 'Тестовая тень',
|
||||
asks: 'Тестовый вопрос',
|
||||
step: 'Тестовый жест',
|
||||
);
|
||||
|
||||
expect(repo.customEditsCount, equals(1));
|
||||
final edited = repo.getWordByName(testWord)!;
|
||||
expect(edited.isUserEdited, isTrue);
|
||||
expect(edited.core, equals('Тестовое ядро от пользователя'));
|
||||
expect(edited.oracle, equals('Тестовое толкование от пользователя'));
|
||||
expect(edited.shadow, equals('Тестовая тень'));
|
||||
expect(edited.asks, equals('Тестовый вопрос'));
|
||||
expect(edited.step, equals('Тестовый жест'));
|
||||
|
||||
// Export json
|
||||
final exportedJson = repo.exportCustomEditsJson();
|
||||
expect(exportedJson.contains(testWord), isTrue);
|
||||
expect(exportedJson.contains('Тестовое ядро от пользователя'), isTrue);
|
||||
|
||||
// Reset
|
||||
await repo.resetCustomEdit(testWord);
|
||||
expect(repo.customEditsCount, equals(0));
|
||||
final resetWord = repo.getWordByName(testWord)!;
|
||||
expect(resetWord.isUserEdited, isFalse);
|
||||
expect(resetWord.core, isNot(equals('Тестовое ядро от пользователя')));
|
||||
|
||||
// Import back
|
||||
final importedCount = await repo.importCustomEditsJson(exportedJson);
|
||||
expect(importedCount, equals(1));
|
||||
expect(repo.customEditsCount, equals(1));
|
||||
final reimported = repo.getWordByName(testWord)!;
|
||||
expect(reimported.isUserEdited, isTrue);
|
||||
expect(reimported.core, equals('Тестовое ядро от пользователя'));
|
||||
|
||||
// Clear all
|
||||
await repo.clearAllCustomEdits();
|
||||
expect(repo.customEditsCount, equals(0));
|
||||
expect(repo.getWordByName(testWord)!.isUserEdited, isFalse);
|
||||
});
|
||||
|
||||
test('Word #581 вызывать has appropriate summoning / challenge emojis instead of wood/climbing', () async {
|
||||
final word = repo.getWordByName('вызывать');
|
||||
expect(word, isNotNull);
|
||||
expect(word!.emoji, equals('📯 ⚡'));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user