Aventuras/docs/development/testing.md
Tony Frazier 376f088642
Some checks are pending
CI / build-desktop (push) Waiting to run
CI / build-android (push) Waiting to run
CI / build-ios (push) Waiting to run
Rust Lint / Format (push) Waiting to run
Rust Lint / Clippy (push) Waiting to run
Move image generation onto the AI SDK (#639)
## Summary

Every image backend now generates through the AI SDK's `generateImage`
(`sdkImage.ts`), with one request path for debug logging, abort and 429
retry. Before, Google went through the SDK while the rest used
hand-written requests, and ComfyUI ran on its own SDK plus a Tauri
WebSocket plugin.

- OpenAI, OpenAI Compatible, OpenRouter and Google use their SDK
providers. Pollinations and Z.AI reuse the OpenAI Compatible helper.
- NanoGPT, A1111 and ComfyUI have no usable SDK provider, so each gets a
small custom `ImageModelV4` (`nanogptImage.ts`, `a1111Image.ts`,
`comfyImage.ts`).
- ComfyUI now submits a workflow and polls `/api/jobs/{id}` over plain
HTTP, and sends a cancel for the job if the user aborts or the app gives
up. It needs ComfyUI 0.26.0 or later (`MIN_COMFYUI_VERSION`); older
servers are refused with a message, and a server that can't report its
version is let through.
- `@saintno/comfyui-sdk`, `@tauri-apps/plugin-websocket`,
`tauri-plugin-websocket` and `buffer` are removed.

## User-facing changes

- **Chutes image provider removed.** Chutes text generation is
unchanged.
- **New OpenAI Compatible image provider.** Base URL is required; API
key is optional.
- **Zhipu is now Z.AI** and defaults to `api.z.ai`.
- **Model discovery fails loudly.** A failed model list throws and the
settings form shows the error and clears the old list. Before, it
silently fell back to a built-in list.
- **Missing credentials are reported by kind** (profile, API key, base
URL) in portrait generation and in the image tools, instead of always
saying "API key required".
- **ComfyUI needs 0.26.0+.**

## Migration 042 (`042_image_profiles.sql`)

Runs in SQL and replaces `migrateImageProfileDefaults` in the settings
store. It does four things:

- Pins `zhipu` API profiles that have no base URL to `open.bigmodel.cn`,
the host they were already using.
- Builds image profiles from legacy image settings that name an API
profile and a model, only when no image profile exists yet.
- Moves `openai` profiles that point at a proxy to OpenAI Compatible.
- Moves every `chutes` image profile to OpenAI Compatible.

`src-tauri/src/image_profiles.rs` holds the Rust tests for it.

## Changes

**Transport**
- `createSdkFetch` and `createPollSdkFetch` (`sdkFetch.ts`): the Tauri
transport for SDK image models, with binary and multipart bodies, the
request guard, debug logging, and redirects returned to the SDK for it
to check. The poll variant keeps a ComfyUI generation's requests in one
debug entry.
- A1111 and ComfyUI send all requests through `createSdkFetch`.
- 429 retry for image models (`retryOn429ImageMiddleware`), with
`Retry-After` capped at `IMAGE_TIMEOUT_DEFAULT`.
- Header redaction (including the Gemini key) and base64 scrubbing in
debug logs.

**Providers and registry**
- `registry.ts` declares labels, API key mode and base URL mode once per
provider (`IMAGE_PROVIDER_LABELS`, `PROVIDER_API_KEY`,
`PROVIDER_BASE_URL`). The images settings tab and `missingCredential`
read them.
- NanoGPT sends the model the closest aspect ratio it lists.
- Model lists are validated with zod (`parseModelList.ts`).

**Docs**
- The Images section of `docs/architecture/overview.md` is rewritten for
the SDK path.
- `docs/development/testing.md` and `ios-build-target.md` are updated.

## Changes outside the main purpose

These touch code that was already on `master`:

- `fetch.ts` (`createTimeoutFetch`): the debug-log helpers move to
`sdk/providers/debugLog.ts` and are typed instead of `any`. Behaviour is
unchanged, and `debugLog` and the body-merge/usage-patch logic gain
tests.
- `sleep` moves from `retryMiddleware.ts` to `utils/async.ts`.
- The debug window replaces an entry in place when the store re-emits
it, instead of appending a duplicate.
- `supportsImageGeneration` uses `Object.hasOwn`, so an inherited key
like `constructor` is not a supported provider.
- `formatImageSize` returns a template-literal type, and the unused
`base64ImageMime` is removed.

## Test plan

- [x] `aubr check`, `aube test`, `aubr lint`
- [x] `cargo test` in `src-tauri` (migration 042 tests)
- [x] Generate an image with each provider you have access to, including
a reference image where the provider supports it.
- [ ] ComfyUI: generate, cancel mid-job and confirm the job is cancelled
on the server, and check that a pre-0.26.0 server is refused.
- [ ] Upgrade a database that has `openai` proxy, `chutes` and `zhipu`
image profiles and confirm each lands as described above.
- [ ] Android release build (not yet checked).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Kz2WsAnqEzPZv9Sj8u9VG4


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Added OpenAI-compatible image generation and expanded image-generation
support for ComfyUI, A1111, NanoGPT, and OpenRouter.
* Existing image-generation settings are migrated to image profiles,
with provider-specific defaults applied where needed.
* Image settings show the credential fields required by each provider,
and errors identify missing credentials.

* **Improvements**
* ComfyUI image generation requires version 0.26.0 or later; image
requests can run for up to five minutes.
  * Debug logs update existing entries instead of duplicating them.

* **Changes**
  * Chutes is no longer available for image generation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:01:47 -04:00

6.2 KiB

Testing

Vitest (vitest.config.ts), covering targeted units under src/lib/ — not a full-coverage suite. Run with npm test.

Rune modules cannot be imported by tests. vitest.config.ts deliberately omits the SvelteKit plugin to keep the suite fast and stable, so any *.svelte.ts file fails at import with $state is not defined. Services reach the stores through vi.mock('#lib/stores/….svelte.ts'), a path TypeScript does not check but local/lib-import-exists does (why); logic that needs testing on its own is extracted into a plain .ts module instead (settingsMigrations.ts, advancedPanelView.ts, stickiness.ts, recentTail.ts are all this pattern). Those modules are production code with real callers, not test scaffolding.

The default environment is node, with no DOM, and no test renders a component. A Svelte-level mistake — a bind: to an undefined value, for instance — passes check, lint and the whole suite, and only fails when the app runs.

A test whose code under test parses HTML with the DOM (utils/narrationClean.ts) opts in per file with // @vitest-environment jsdom as its first line. jsdom is a devDependency only, and every other file stays on node.

Discovery provider tests

The discovery provider unit tests mock corsFetch with representative upstream payloads. They are the CI contract for request construction, result mapping, and card downloads; they do not depend on remote services remaining available or preserving their current catalog.

An opt-in smoke suite exercises public search and download paths against every registered discovery provider:

$env:AVENTURAS_LIVE_DISCOVERY='1'; npx vitest run src/lib/services/discovery/providers/providers.live.test.ts

It is skipped unless AVENTURAS_LIVE_DISCOVERY=1 and must not be enabled in required CI. The suite is intentionally comprehensive: failures from Cloudflare, volatile application protocols, upstream catalog changes, or provider outages are useful signals during an explicit live run, but would make required CI flaky.

Fake provider

scripts/fake-provider.mjs stands in for a provider, so the running app can be driven into failures by hand: rate limits, server errors, hangs, empty and malformed answers, cut streams. It answers as an OpenAI-compatible chat API and as the OpenAI image API, with no dependencies, and logs one line per request. What each request does is chosen by the model name it asks for, so every service can be pointed at its own failure from one profile.

npm run fake-provider

It listens on http://localhost:4010/v1; npm run fake-provider -- 4020 picks another port.

Setup in the app

  1. Settings → API Connection: add a profile, provider OpenAI Compatible, base URL http://localhost:4010/v1, any API key. Make it the main narrative profile, model ok.
  2. Settings → Generation → Agent Profiles: profiles on that API profile, all with Structured Output on — the fake builds structured answers from the request's JSON schema, which it cannot see when the schema travels inside the prompt. One on ok for every task; one on ok-fill for Image Gen and BG Image generation analyzer, so scenes are found and the background is judged changed; one whose model is set to the failure under test, to move a single task onto.
  3. Settings → Images: a profile with provider OpenAI Compatible, the same base URL and model img-ok. No API key is needed; without a profile every story's image settings stay on Text Only.

Chat models. Append -slow to any of them for a 4 s delay before the answer starts.

Model Behaviour
ok a valid answer; structured output is built from the request's schema
ok-fill as ok, with one item in every list and flags true
partial as ok-fill, plus one list element missing its required fields
malformed JSON that does not parse; a plain-text call gets x
empty 200 with no content
fail-400 … fail-404 400 context length, 401 bad key, 403 no access, 404 no such model
fail-429 429 on every request
fail-429x2 429 twice, then a valid answer
fail-429-ra3 429 once with Retry-After: 3, then a valid answer
fail-429-ra900 429 with Retry-After: 900, beyond the request timeout
fail-500, fail-503 500 or 503 on every request
fail-500x1 500 once, then a valid answer
fail-502-html 502 with an HTML page as the body
fail-reset the connection closed with no response
fail-hang never answers
stream-connection-lost streaming only: some text, then the connection drops
stream-cut-error streaming only: some text, then an error event in the stream, as OpenRouter sends
stream-stall streaming only: some text, then silence with the connection open

The narration is always The dragon fell from the sky, and the valley was quiet again., and every scene's quote is The dragon fell from the sky, so images land in the text — including the part a cut stream keeps.

Image models: img-ok, img-fail-400 (content policy), img-fail-401, img-fail-429, img-fail-500, img-empty (200 without an image), img-hang.