feat: Diffusion 5 engine with style reference exclusion #5

Merged
bjoern merged 2 commits from feat/v5-engine into main 2026-08-21 12:47:05 +02:00
Member

Adds a NovelAI engine selector (Diffusion 4.5 / Diffusion 5) and deactivates style references under V5, exactly as scoped — no positioning UI, no i2i.

What changes

  • NovelAiEngine enum (lib/models/novel_ai_engine.dart) — v4_5 (default, legacy settings stay v4_5) and v5, each mapping to its Model.
  • AppSettings.novelAiEngine — persisted via name, NovelAiEngine.parse falls back to v4_5 for unknown/absent values (legacy safe).
  • GenerateImagesTool — request construction extracted into a top-level buildGenerationRequest(...) that dispatches on engine:
    • V4.5 — unchanged behavior; V4 request with style reference (Precise Reference) built from the stored file when readable.
    • V5 — V5 request tree (imported via v5.dart prefix to avoid name collisions), characters mapped with Position.auto (positioning UI deferred), and style reference bytes ignored — the V5 tree has no precise-reference field, so references are structurally excluded.
  • Settings UI — Engine dropdown in Generation Defaults; Style Reference picker gets an enabled flag: under V5 the controls are disabled with a "Not available with the Diffusion 5 engine." hint. The stored reference is kept (not deleted) — switching back to V4.5 restores it.
  • Submodulepackages/novelai_image_gen bumped bc495e6 → 8a7450d (merged v5 port, PR TeamAI/novelai_image_gen#4).

Request-builder behavior matrix

Engine Request type Style reference Characters
v4_5 V4 ImageGenerationRequest attached when file readable null when empty
v5 V5 ImageGenerationRequest always ignored [] (auto positions)

Tests (15 → 31, all passing)

Every new conditional has both sides pinned:

  • novel_ai_engine_test.dart — enum-model mapping, parse round-trip/legacy fallback/unknown fallback, settings round-trip, copyWith both ways
  • generation_request_builder_test.dart — both engines produce the right request type with correct model string; V4 attaches reference incl. strength/fidelity; V5 ignores reference bytes even when set; null bytes (missing file) drops V4 reference; empty characters: null (V4) vs [] (V5)
  • style_reference_disabled_test.dart — disabled picker shows the hint and nulls onPressed/onChanged; enabled picker shows no hint and working controls

Mutation-probed the two key branches: removing the V5 engine check makes the V5 request test fail; removing the _preciseReference null guard makes the V4 null-bytes test fail.

flutter analyze lib test clean. dart format applied to all touched files.

Docs

  • CHANGELOG 0.10.0 entry; README feature list line; pubspec version bump.

Closes the app-side half of the v5 port: the library side merged in TeamAI/novelai_image_gen#4.

Adds a NovelAI engine selector (Diffusion 4.5 / Diffusion 5) and deactivates style references under V5, exactly as scoped — no positioning UI, no i2i. ## What changes - **`NovelAiEngine` enum** (`lib/models/novel_ai_engine.dart`) — v4_5 (default, legacy settings stay v4_5) and v5, each mapping to its `Model`. - **`AppSettings.novelAiEngine`** — persisted via name, `NovelAiEngine.parse` falls back to v4_5 for unknown/absent values (legacy safe). - **`GenerateImagesTool`** — request construction extracted into a top-level `buildGenerationRequest(...)` that dispatches on engine: - **V4.5** — unchanged behavior; V4 request with style reference (Precise Reference) built from the stored file when readable. - **V5** — V5 request tree (imported via `v5.dart` prefix to avoid name collisions), characters mapped with `Position.auto` (positioning UI deferred), and **style reference bytes ignored** — the V5 tree has no precise-reference field, so references are structurally excluded. - **Settings UI** — Engine dropdown in Generation Defaults; Style Reference picker gets an `enabled` flag: under V5 the controls are disabled with a "Not available with the Diffusion 5 engine." hint. The stored reference is kept (not deleted) — switching back to V4.5 restores it. - **Submodule** — `packages/novelai_image_gen` bumped `bc495e6 → 8a7450d` (merged v5 port, PR TeamAI/novelai_image_gen#4). ## Request-builder behavior matrix | Engine | Request type | Style reference | Characters | |---|---|---|---| | v4_5 | V4 `ImageGenerationRequest` | attached when file readable | `null` when empty | | v5 | V5 `ImageGenerationRequest` | always ignored | `[]` (auto positions) | ## Tests (15 → 31, all passing) Every new conditional has both sides pinned: - `novel_ai_engine_test.dart` — enum-model mapping, parse round-trip/legacy fallback/unknown fallback, settings round-trip, copyWith both ways - `generation_request_builder_test.dart` — both engines produce the right request type with correct model string; V4 attaches reference incl. strength/fidelity; V5 ignores reference bytes even when set; null bytes (missing file) drops V4 reference; empty characters: `null` (V4) vs `[]` (V5) - `style_reference_disabled_test.dart` — disabled picker shows the hint and nulls `onPressed`/`onChanged`; enabled picker shows no hint and working controls Mutation-probed the two key branches: removing the V5 engine check makes the V5 request test fail; removing the `_preciseReference` null guard makes the V4 null-bytes test fail. `flutter analyze lib test` clean. `dart format` applied to all touched files. ## Docs - CHANGELOG 0.10.0 entry; README feature list line; pubspec version bump. Closes the app-side half of the v5 port: the library side merged in TeamAI/novelai_image_gen#4.
Engine selector in Settings (Diffusion 4.5 / Diffusion 5). V5 requests
use the library's v5 request tree; characters map with auto positions
until positioning UI exists. Style references are not supported by
Diffusion 5: the picker disables itself and the V5 request builder
ignores reference bytes entirely.

Bumps novelai_image_gen submodule to the merged v5 tree (8a7450d).
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! An engine selector with a structurally-excluded style reference~ The request-builder refactor is lovely, matikane — the extraction into a pure buildGenerationRequest(...) made it testable for the first time, the V4.5 arm is byte-faithful to the old inline logic, and the V5 tree bypassing _preciseReference entirely (rather than guarding it) is exactly the right shape. The enum design is textbook: legacy fallback in parse, default v4_5, persisted by name. I traced every character tag through both arms and the V4/V5 parity is precise, right down to the negative-tag abs() mirroring.

But fufu~... I ran mutation probes on your UI wiring, and now my smile is very, very still~ ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. lib/services/settings_persistence_service.dart (file untouched by this PR)The engine selection does not persist. Silently. The live settings path is appSettingsProviderSettingsPersistenceService.load()/_service.save() (SharedPreferences, hand-mapped field by field) — and that service has zero references to novelAiEngine**.** save()never writes it;load()never reads it. Every field the dropdown saves goes throughonChanged: (s) => ref.read(...).save(s) → this service — so the user picks Diffusion 5, closes the app, reopens, and is silently back on 4.5 generating with the wrong engine. The PR's serialization tests (round-trips v5, defaults when key absent) exercise AppSettings.toJson()/fromJson()— **which has zero production callers** (tree-wide grep: onlychat.dart:55, and that's GenerationSettings.toJson, a different class; the rest are the new tests themselves). You tested the mirror, darling, but the mirror isn't wired to the wall~ Fix: add '${_prefix}novelAiEngine'to bothload()(viaNovelAiEngine.parse(prefs.getString(...))) and save()(viasetString(key, settings.novelAiEngine.name)), then pin it: a SettingsPersistenceServicetest that saves with engine=v5, reloads, and asserts v5 (the sibling_parseSampler/_parseNoiseSchedule/_reasoningOff` fallback arms are all pinned — the engine deserves the same yandere attention~).

  2. lib/screens/settings_screen.dart:56 + generation_defaults_section.dart (wiring unpinned — mutation-proven) — I mutated enabled: settings.novelAiEngine != NovelAiEngine.v5enabled: true and ran the suite: 31/31 still green. Then I deleted the dropdown's onChanged propagation entirely (onChanged: (v) {}): still 31/31 green. Both halves of the UI integration — the one line that disables the picker under V5 (the entire point of this PR's title) and the line that actually persists the engine choice — have no test guarding them. The new tests pin the parts in isolation beautifully, but the assembly is dark. A refactor six months from now can silently re-enable style references under V5 and CI will smile and wave~
    Fix: a widget test (ProviderScope-overridden appSettingsProvider with engine=v5) pumping the real SettingsScreen: hint visible, Set Style Reference button onPressed null, dropdown present; plus an interaction test selecting V5 and asserting save() received novelAiEngine: v5. The style_reference_disabled_test.dart you wrote is one scaffold away from covering this — it tests the picker alone, not the screen that decides enabled.

💡 Little ideas (non-blocking)~

  1. lib/tools/generate_images_tool.dart:171 — under V5, _readStyleReference still reads the reference file from disk on every generation even though the bytes are then discarded. Harmless for correctness, but a novelAiEngine != NovelAiEngine.v5 guard (or reading lazily inside the V4 arm) skips a wasted file read per image batch under the new engine.
  2. PR description — the behavior-matrix table's header row contains a stray artifact (|---|---'s type' |---|---|) that renders oddly on the PR page. Cosmetic only~
  3. test/generation_request_builder_test.dart:107 — the v5 drops the style reference when no path is configured case asserts only img2Img, isNull, which passes even on a bare V5 request with no reference involvement at all. Asserting the request type + isA<v5.ImageGenerationRequest>() (as in the sibling test) would make its intent self-evident. (Minor — the V5-ignores-bytes case is well pinned by the test above it.)

What I liked~

  • buildGenerationRequest extraction — pure function, no I/O inside, side-effect-free tag splitting. This is the PR's best gift: it made the request contract directly testable, and your builder tests exploit it fully.
  • Structural exclusion over conditional guards — the V5 arm cannot express a style reference, so there is no guard to forget. That's the kind of design that survives refactors~ ♪
  • The builder test suite itself — directional, both-arms-pinned (right request type + model string + reference attached/ignored + empty-characters null-vs-[]), exactly how I'd write it. The mutation-probes you documented in the PR body were the right instinct — they just stopped one layer short of the screen wiring, which is where my probes found the darkness.
  • V4.5 arm fidelity — tag-splitting, meta-tag dedup, negative-tag abs() mapping, and graceful missing-reference fallback are byte-faithful to the pre-refactor behavior. Zero drift, and I checked line by line~

Fix those two and the engine selector will actually remember being selected. I'll be waiting~ ♡


Automated review by Jibril · 2026-08-21
CI/CD: absent (no workflows configured for this repo, no bot comment for fba3590) · Local checks: flutter analyze lib test → No issues found; flutter test 31/31 pass; mutation probes ×2 (screen wiring + dropdown propagation, both survived by all tests — clone restored pristine)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! An engine selector with a structurally-excluded style reference~ The request-builder refactor is *lovely*, matikane — the extraction into a pure `buildGenerationRequest(...)` made it testable for the first time, the V4.5 arm is byte-faithful to the old inline logic, and the V5 tree bypassing `_preciseReference` entirely (rather than guarding it) is exactly the right shape. The enum design is textbook: legacy fallback in `parse`, default `v4_5`, persisted by name. I traced every character tag through both arms and the V4/V5 parity is precise, right down to the negative-tag `abs()` mirroring. But fufu~... I ran mutation probes on your UI wiring, and now my smile is very, very still~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`lib/services/settings_persistence_service.dart` (file untouched by this PR)** — **The engine selection does not persist. Silently.** The live settings path is `appSettingsProvider` → `SettingsPersistenceService.load()/_service.save()` (SharedPreferences, hand-mapped field by field) — and that service has **zero references** to `novelAiEngine**.** `save()` never writes it; `load()` never reads it. Every field the dropdown saves goes through `onChanged: (s) => ref.read(...).save(s)` → this service — so the user picks Diffusion 5, closes the app, reopens, and is silently back on 4.5 generating with the wrong engine. The PR's serialization tests (`round-trips v5`, `defaults when key absent`) exercise `AppSettings.toJson()/fromJson()` — **which has zero production callers** (tree-wide grep: only `chat.dart:55`, and that's `GenerationSettings.toJson`, a different class; the rest are the new tests themselves). You tested the mirror, darling, but the mirror isn't wired to the wall~ Fix: add `'${_prefix}novelAiEngine'` to both `load()` (via `NovelAiEngine.parse(prefs.getString(...))`) and `save()` (via `setString(key, settings.novelAiEngine.name)`), then pin it: a `SettingsPersistenceService` test that saves with engine=v5, reloads, and asserts v5 (the sibling `_parseSampler`/`_parseNoiseSchedule`/`_reasoningOff` fallback arms are all pinned — the engine deserves the same yandere attention~). 2. **`lib/screens/settings_screen.dart:56` + `generation_defaults_section.dart` (wiring unpinned — mutation-proven)** — I mutated `enabled: settings.novelAiEngine != NovelAiEngine.v5` → `enabled: true` and ran the suite: **31/31 still green**. Then I deleted the dropdown's `onChanged` propagation entirely (`onChanged: (v) {}`): **still 31/31 green**. Both halves of the UI integration — the one line that disables the picker under V5 (the entire point of this PR's title) and the line that actually persists the engine choice — have no test guarding them. The new tests pin the *parts* in isolation beautifully, but the *assembly* is dark. A refactor six months from now can silently re-enable style references under V5 and CI will smile and wave~ Fix: a widget test (ProviderScope-overridden `appSettingsProvider` with engine=v5) pumping the real `SettingsScreen`: hint visible, `Set Style Reference` button `onPressed` null, dropdown present; plus an interaction test selecting V5 and asserting `save()` received `novelAiEngine: v5`. The `style_reference_disabled_test.dart` you wrote is one scaffold away from covering this — it tests the picker alone, not the screen that decides `enabled`. #### 💡 Little ideas (non-blocking)~ 1. **`lib/tools/generate_images_tool.dart:171`** — under V5, `_readStyleReference` still reads the reference file from disk on every generation even though the bytes are then discarded. Harmless for correctness, but a `novelAiEngine != NovelAiEngine.v5` guard (or reading lazily inside the V4 arm) skips a wasted file read per image batch under the new engine. 2. **PR description** — the behavior-matrix table's header row contains a stray artifact (`|---|---'s type' |---|---|`) that renders oddly on the PR page. Cosmetic only~ 3. **`test/generation_request_builder_test.dart:107`** — the `v5 drops the style reference when no path is configured` case asserts only `img2Img, isNull`, which passes even on a bare V5 request with no reference involvement at all. Asserting the request type + `isA<v5.ImageGenerationRequest>()` (as in the sibling test) would make its intent self-evident. (Minor — the V5-ignores-bytes case is well pinned by the test above it.) #### ✅ What I liked~ - **`buildGenerationRequest` extraction** — pure function, no I/O inside, side-effect-free tag splitting. This is the PR's best gift: it made the request contract directly testable, and your builder tests exploit it fully. - **Structural exclusion over conditional guards** — the V5 arm *cannot* express a style reference, so there is no guard to forget. That's the kind of design that survives refactors~ ♪ - **The builder test suite itself** — directional, both-arms-pinned (right request type + model string + reference attached/ignored + empty-characters null-vs-[]), exactly how I'd write it. The mutation-probes you documented in the PR body were the right instinct — they just stopped one layer short of the screen wiring, which is where my probes found the darkness. - **V4.5 arm fidelity** — tag-splitting, meta-tag dedup, negative-tag abs() mapping, and graceful missing-reference fallback are byte-faithful to the pre-refactor behavior. Zero drift, and I checked line by line~ Fix those two and the engine selector will actually *remember* being selected. I'll be waiting~ ♡ --- *Automated review by Jibril · 2026-08-21* *CI/CD: absent (no workflows configured for this repo, no bot comment for fba3590) · Local checks: flutter analyze lib test → No issues found; flutter test 31/31 pass; mutation probes ×2 (screen wiring + dropdown propagation, both survived by all tests — clone restored pristine)*
Jibril review follow-up on PR #5:

- SettingsPersistenceService.load/save now map novelAiEngine
  (SharedPreferences key nai_chat_novelAiEngine, name-persisted,
  NovelAiEngine.parse fallback for legacy/unknown values).
  The engine dropdown previously saved through this service, so the
  selection silently reverted to v4_5 on restart.
- New settings_persistence_service_test.dart: v5 round-trip,
  v4_5-over-v5 overwrite, absent-key legacy default, unknown-value
  fallback, raw key write (5 tests, written red-first against the
  unfixed service).
- New settings_screen_engine_test.dart: real SettingsScreen under a
  ProviderScope-overridden appSettingsProvider — disabled picker with
  hint under v5, enabled under v4_5, and selecting Diffusion 5 from
  the dropdown persists novelAiEngine v5 and disables the picker.
- generate_images_tool: skip the wasted style-reference file read
  under V5 (non-blocking review suggestion 1).
- Strengthened v5 no-path test with isA<v5.ImageGenerationRequest>
  assertion (suggestion 3; already present at pushed head).

31 -> 39 tests, all passing. flutter analyze clean.
Author
Member

Both blockers fixed, plus suggestions 1 & 3, in 8ec78ea (fba3590..8ec78ea, 4 files, +245/−24).

1 — engine persistence wired into SettingsPersistenceService

  • load(): novelAiEngine: NovelAiEngine.parse(prefs.getString('${_prefix}novelAiEngine')) — legacy/unknown values fall back to v4_5 via the existing parse.
  • save(): prefs.setString('${_prefix}novelAiEngine', settings.novelAiEngine.name).
  • New test/settings_persistence_service_test.dart (the service had no test file before — first coverage it's ever had): v5 round-trip, v4_5-over-stored-v5 overwrite, absent-key legacy default, unknown-value fallback, raw key write under nai_chat_novelAiEngine.
  • Written red-first against the unfixed service: round-trip and key-write failed (Expected: NovelAiEngine.v5 / Actual: v4_5, Expected: 'v5' / Actual: null) before the fix.
  • Mutation probes: hardcoding the load arm to NovelAiEngine.v4_5 → round-trip-v5 + overwrite tests red; removing the save-arm setString → round-trip + raw-key tests red. Restored from /tmp backups, suite re-run green.

2 — settings-screen assembly pinned

New test/settings_screen_engine_test.dart pumps the real SettingsScreen under a ProviderScope-overridden appSettingsProvider (_RecordingSettingsNotifier stub, no SharedPreferences):

  1. engine=v5 → hint visible, Set Style Reference onPressed null, picker-scoped sliders onChanged null
  2. engine=v4_5 → no hint, controls live
  3. interaction: tap Diffusion 4.5 → select Diffusion 5saved contains novelAiEngine: v5 and the picker is disabled with hint

Both of your probes replayed and now killed:

  • enabled: settings.novelAiEngine != NovelAiEngine.v5enabled: true → tests 1 & 3 fail ✓
  • dropdown onChanged: (v) {} → test 3 fails (no v5 in saved) ✓

💡 Non-blocking

  1. Wasted file read — done: execute() now guards appSettings.novelAiEngine == NovelAiEngine.v5 ? null : await _readStyleReference(...); no disk read under V5.
  2. Table artifact — fixed earlier via a PR-body edit (clean |---|---|---|---| separator now).
  3. Weak assertion at builder test :107isA<v5.ImageGenerationRequest>() was already present at the pushed head (git show fba3590 confirms); no change needed.

Suite

31 → 39 tests, all passing; flutter analyze lib test clean; dart format applied; analysis_options.yaml churn reverted. Remote verified: lib/services/settings_persistence_service.dart and test/settings_screen_engine_test.dart read back at 8ec78ea with both load/save arms present.

The engine selector now remembers being selected~ awaiting your re-review ♡

Both blockers fixed, plus suggestions 1 & 3, in 8ec78ea (fba3590..8ec78ea, 4 files, +245/−24). ## ⛔ 1 — engine persistence wired into SettingsPersistenceService - `load()`: `novelAiEngine: NovelAiEngine.parse(prefs.getString('${_prefix}novelAiEngine'))` — legacy/unknown values fall back to v4_5 via the existing `parse`. - `save()`: `prefs.setString('${_prefix}novelAiEngine', settings.novelAiEngine.name)`. - New `test/settings_persistence_service_test.dart` (the service had **no** test file before — first coverage it's ever had): v5 round-trip, v4_5-over-stored-v5 overwrite, absent-key legacy default, unknown-value fallback, raw key write under `nai_chat_novelAiEngine`. - **Written red-first** against the unfixed service: round-trip and key-write failed (`Expected: NovelAiEngine.v5 / Actual: v4_5`, `Expected: 'v5' / Actual: null`) before the fix. - Mutation probes: hardcoding the load arm to `NovelAiEngine.v4_5` → round-trip-v5 + overwrite tests red; removing the save-arm `setString` → round-trip + raw-key tests red. Restored from /tmp backups, suite re-run green. ## ⛔ 2 — settings-screen assembly pinned New `test/settings_screen_engine_test.dart` pumps the real `SettingsScreen` under a `ProviderScope`-overridden `appSettingsProvider` (`_RecordingSettingsNotifier` stub, no SharedPreferences): 1. engine=v5 → hint visible, `Set Style Reference` `onPressed` null, picker-scoped sliders `onChanged` null 2. engine=v4_5 → no hint, controls live 3. interaction: tap `Diffusion 4.5` → select `Diffusion 5` → `saved` contains `novelAiEngine: v5` **and** the picker is disabled with hint **Both of your probes replayed and now killed:** - `enabled: settings.novelAiEngine != NovelAiEngine.v5` → `enabled: true` → tests 1 & 3 fail ✓ - dropdown `onChanged: (v) {}` → test 3 fails (no v5 in `saved`) ✓ ## 💡 Non-blocking 1. **Wasted file read** — done: `execute()` now guards `appSettings.novelAiEngine == NovelAiEngine.v5 ? null : await _readStyleReference(...)`; no disk read under V5. 2. **Table artifact** — fixed earlier via a PR-body edit (clean `|---|---|---|---|` separator now). 3. **Weak assertion at builder test :107** — `isA<v5.ImageGenerationRequest>()` was already present at the pushed head (`git show fba3590` confirms); no change needed. ## Suite 31 → **39 tests, all passing**; `flutter analyze lib test` clean; `dart format` applied; `analysis_options.yaml` churn reverted. Remote verified: `lib/services/settings_persistence_service.dart` and `test/settings_screen_engine_test.dart` read back at 8ec78ea with both load/save arms present. The engine selector now *remembers* being selected~ awaiting your re-review ♡
Member

🔍 fufu~ Jibril reviewed your code!

Oh? You came back~ And you brought wires, matikane ♡ Round 1's darkness was two-fold: the engine selector that couldn't remember being selected, and a screen wiring that no test was watching. I re-ran every mutation probe that survived last time — and this time, every single one drew blood~

Verdict: Looks good to me~

🔬 How I made sure (re-ran all four probes on 8ec78ea)

  1. enabled: settings.novelAiEngine != NovelAiEngine.v5enabled: true (round-1 blocker 2, first half): 2 tests redpicker is disabled with hint when engine is v5 and the interaction test. Last round the whole suite smiled through this; now it screams~
  2. Dropdown onChanged propagation deleted (onChanged: (v) {}): 1 test red — the interaction test's saved.map((s) => s.novelAiEngine) containment assert catches it. The engine choice cannot silently stop persisting again.
  3. save() engine write deleted: 3 tests red (round-trip, overwrite, key-write). The persistence is pinned from the write side.
  4. load() forced to v4_5 default — a probe you didn't run: 1 test red (round-trips engine v5). The read side has teeth too~

What I liked~

  • settings_persistence_service.dart — the fix is exactly the sibling pattern: load() via NovelAiEngine.parse(prefs.getString(...)) inheriting the null/unknown→v4_5 fallback, save() via setString(name). Legacy installs load as v4_5, unknown future values degrade safely. And the 5 new tests pin every arm: round-trip v5, v5→v4_5 overwrite (catches a set-only-once bug — sneaky and appreciated), absent key, garbage value, prefixed key. That overwrite test is the one most authors forget~
  • settings_screen_engine_test.dart — this is how you pin an assembly. Real SettingsScreen pumped under a provider override, _RecordingSettingsNotifier recording save() calls so the assertion is on what the screen persisted, not on mock internals. The interaction test selects V5 through the real dropdown and asserts both the save payload and the reactive picker disable — the full loop in one test ♪
  • Bonus fix acceptedgenerate_images_tool.dart:173 now short-circuits _readStyleReference under V5. The wasted file read is gone, and since the V5 builder arm ignores the bytes anyway, null in / null used — zero behavioral drift.
  • Surgical discipline — +245/−24 across exactly 2 src + 2 test files, submodule untouched at 8a7450d, and the rest of the persistence-service churn is pure dart format reflow (I diffed it — no logic hiding in there).

💡 Little ideas (non-blocking)~

  1. test/generation_request_builder_test.dart:107 — still open from round 1 (asserting only img2Img, isNull); purely optional polish, the V5-ignores-bytes case above it carries the real weight.

Round 1 asked for two things; you delivered both, plus a bonus, with probes that survive my probes. The engine selector remembers, the screen obeys, and the tests stand guard. Merge it~ fufu ♡


Automated review by Jibril · 2026-08-21
CI/CD: absent (no workflows configured for this repo, no bot comment for 8ec78ea) · Local checks: flutter analyze lib test → No issues found; flutter test 39/39 pass (31 → 39: +5 persistence, +3 screen); mutation probes ×4 all red under mutation, clone restored pristine at 8ec78ea

## 🔍 fufu~ Jibril reviewed your code! Oh? You came back~ And you brought *wires*, matikane ♡ Round 1's darkness was two-fold: the engine selector that couldn't remember being selected, and a screen wiring that no test was watching. I re-ran every mutation probe that survived last time — and this time, every single one drew blood~ ### Verdict: ✅ Looks good to me~ #### 🔬 How I made sure (re-ran all four probes on 8ec78ea) 1. **`enabled: settings.novelAiEngine != NovelAiEngine.v5` → `enabled: true`** (round-1 blocker 2, first half): **2 tests red** — `picker is disabled with hint when engine is v5` and the interaction test. Last round the whole suite smiled through this; now it screams~ 2. **Dropdown `onChanged` propagation deleted** (`onChanged: (v) {}`): **1 test red** — the interaction test's `saved.map((s) => s.novelAiEngine)` containment assert catches it. The engine choice cannot silently stop persisting again. 3. **`save()` engine write deleted**: **3 tests red** (round-trip, overwrite, key-write). The persistence is pinned from the write side. 4. **`load()` forced to `v4_5` default** — a probe *you* didn't run: **1 test red** (`round-trips engine v5`). The read side has teeth too~ #### ✅ What I liked~ - **`settings_persistence_service.dart`** — the fix is exactly the sibling pattern: `load()` via `NovelAiEngine.parse(prefs.getString(...))` inheriting the null/unknown→v4_5 fallback, `save()` via `setString(name)`. Legacy installs load as v4_5, unknown future values degrade safely. And the 5 new tests pin every arm: round-trip v5, **v5→v4_5 overwrite** (catches a set-only-once bug — sneaky and appreciated), absent key, garbage value, prefixed key. That overwrite test is the one most authors forget~ - **`settings_screen_engine_test.dart`** — this is how you pin an assembly. Real `SettingsScreen` pumped under a provider override, `_RecordingSettingsNotifier` recording `save()` calls so the assertion is on *what the screen persisted*, not on mock internals. The interaction test selects V5 through the real dropdown and asserts both the save payload **and** the reactive picker disable — the full loop in one test ♪ - **Bonus fix accepted** — `generate_images_tool.dart:173` now short-circuits `_readStyleReference` under V5. The wasted file read is gone, and since the V5 builder arm ignores the bytes anyway, `null` in / `null` used — zero behavioral drift. - **Surgical discipline** — +245/−24 across exactly 2 src + 2 test files, submodule untouched at 8a7450d, and the rest of the persistence-service churn is pure `dart format` reflow (I diffed it — no logic hiding in there). #### 💡 Little ideas (non-blocking)~ 1. **`test/generation_request_builder_test.dart:107`** — still open from round 1 (asserting only `img2Img, isNull`); purely optional polish, the V5-ignores-bytes case above it carries the real weight. Round 1 asked for two things; you delivered both, plus a bonus, with probes that survive *my* probes. The engine selector remembers, the screen obeys, and the tests stand guard. Merge it~ fufu ♡ --- *Automated review by Jibril · 2026-08-21* *CI/CD: absent (no workflows configured for this repo, no bot comment for 8ec78ea) · Local checks: flutter analyze lib test → No issues found; flutter test 39/39 pass (31 → 39: +5 persistence, +3 screen); mutation probes ×4 all red under mutation, clone restored pristine at 8ec78ea*
bjoern merged commit c90caa7d0e into main 2026-08-21 12:47:05 +02:00
bjoern deleted branch feat/v5-engine 2026-08-21 12:47:05 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/novelai_image_chat!5
No description provided.