feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets

This commit is contained in:
2026-10-09 15:26:58 +05:00
parent 7f91366201
commit 0b12e4583b
244 changed files with 611461 additions and 1617 deletions
@@ -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);
});
}
+52
View File
@@ -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('Действие'));
});
});
}
+48
View File
@@ -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, {'Славянская традиция': 'Навь'});
});
}
+56
View File
@@ -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);
});
}
+115
View File
@@ -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);
});
}
+115
View File
@@ -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('мужчина'));
}
});
});
}
+32
View File
@@ -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('тропа уже почти закрыта')));
});
}
+120
View File
@@ -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();
});
}
+127
View File
@@ -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);
});
}
+100
View File
@@ -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'));
});
});
}
+44
View File
@@ -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('Идо'));
});
});
}
+44
View File
@@ -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);
}
});
}
+121
View File
@@ -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('ИНСТРУКЦИЯ ДЛЯ ТОЛКОВАНИЯ'));
});
});
}
+44
View File
@@ -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);
});
}
+148
View File
@@ -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
View File
@@ -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);
});
}
+165
View File
@@ -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);
});
});
}
+59 -1
View File
@@ -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('📯 ⚡'));
});
});
}
}