comic/odin/tests/adapters_phase2.odin
2026-05-28 15:20:02 +02:00

155 lines
5.5 KiB
Odin

package tests
import "core:strings"
import "core:testing"
import "../src/adapters"
import "../src/core"
import "../src/shared"
deepseek_calls: int
phase2_deepseek_transport :: proc(cfg: shared.Config, request_json: string) -> (string, int, shared.App_Error) {
_ = cfg
_ = request_json
deepseek_calls += 1
if deepseek_calls == 1 {
return "", 429, shared.ok()
}
return "{\"choices\":[{\"message\":{\"content\":\"{\\\"title\\\":\\\"Test\\\",\\\"synopsis\\\":\\\"A hero rises\\\",\\\"characters\\\":[{\\\"id\\\":\\\"char_001\\\",\\\"name\\\":\\\"Hero\\\",\\\"role\\\":\\\"protagonist\\\",\\\"description\\\":\\\"Determined\\\",\\\"firstAppearancePanel\\\":\\\"panel_001_001\\\"}],\\\"pages\\\":[{\\\"pageNumber\\\":1,\\\"layoutType\\\":\\\"grid\\\",\\\"panels\\\":[{\\\"panelId\\\":\\\"panel_001_001\\\",\\\"panelNumber\\\":1,\\\"shotType\\\":\\\"medium\\\",\\\"description\\\":\\\"Hero stands\\\",\\\"charactersPresent\\\":[\\\"char_001\\\"],\\\"dialogue\\\":[{\\\"speakerId\\\":\\\"char_001\\\",\\\"text\\\":\\\"Let's go!\\\",\\\"bubbleType\\\":\\\"normal\\\",\\\"emotion\\\":\\\"determined\\\"}],\\\"caption\\\":\\\"\\\",\\\"soundEffects\\\":[],\\\"transitionFromPrevious\\\":\\\"none\\\"}]}]}\"}}]}", 200, shared.ok()
}
fal_calls: int
phase2_fal_transport :: proc(cfg: shared.Config, endpoint, prompt, negative_prompt: string, seed: i64, image_size: string, reference_images: []string, reference_strength: f32) -> (string, int, shared.App_Error) {
_ = cfg
_ = endpoint
_ = prompt
_ = negative_prompt
_ = seed
_ = image_size
_ = reference_images
_ = reference_strength
fal_calls += 1
if fal_calls == 1 {
return "", 0, shared.network_error("temporary network issue")
}
return "https://example.com/image.png", 200, shared.ok()
}
@test
deepseek_retries_then_succeeds :: proc(t: ^testing.T) {
deepseek_calls = 0
cfg := shared.Config{
deepseek_api_key = "test-key",
deepseek_base_url = "https://api.deepseek.com",
}
client := adapters.new_deepseek_client()
client.transport = phase2_deepseek_transport
client.max_retries = 3
opts := adapters.Generate_Script_Options{
story_idea = "A hero rises",
genre = "action",
art_style = "manga",
num_pages = 4,
audience = "general",
}
script, err := adapters.generate_comic_script(client, cfg, opts)
defer core.dispose_script_owned(&script)
testing.expect(t, shared.is_ok(err), "deepseek request should eventually succeed")
testing.expect(t, deepseek_calls == 2, "expected one retry before success")
testing.expect(t, len(script.pages) > 0, "generated script should contain pages")
}
@test
fal_queue_enforces_cap :: proc(t: ^testing.T) {
q := adapters.new_fal_queue(1)
first := adapters.try_acquire_slot(&q)
second := adapters.try_acquire_slot(&q)
adapters.release_slot(&q)
third := adapters.try_acquire_slot(&q)
testing.expect(t, first, "first slot acquisition should succeed")
testing.expect(t, !second, "second slot acquisition should fail when saturated")
testing.expect(t, third, "slot acquisition should succeed after release")
}
@test
fal_panel_generation_retries_network_error :: proc(t: ^testing.T) {
fal_calls = 0
cfg := shared.Config{fal_api_key = "test-key"}
q := adapters.new_fal_queue(2)
client := adapters.new_fal_client(&q)
client.transport = phase2_fal_transport
client.max_retries = 3
panel := core.Panel{panel_id = "panel_1", panel_number = 1, description = "Hero jumps over a gap"}
img, err := adapters.generate_panel_image(client, cfg, panel, nil, "manga", "proj_1", "", "")
defer delete(img.prompt)
testing.expect(t, shared.is_ok(err), "fal panel generation should eventually succeed")
testing.expect(t, fal_calls == 2, "expected one retry for network error")
testing.expect(t, len(img.url) > 0, "image url should be set")
}
@test
fal_typed_response_parsing :: proc(t: ^testing.T) {
body := "{\"images\":[{\"url\":\"https://example.com/a.png\",\"width\":1024,\"height\":1024}]}"
resp, err := adapters.fal_parse_response_body(body)
defer adapters.dispose_fal_response(&resp)
testing.expect(t, shared.is_ok(err), "typed parse should succeed")
testing.expect(t, len(resp.images) == 1, "expected one image")
testing.expect(t, resp.images[0].url == "https://example.com/a.png", "expected parsed URL")
}
stream_chunks_received: int
stream_final_received: bool
test_stream_callback :: proc(chunk: string, is_complete: bool) -> () {
stream_chunks_received += 1
if is_complete {
stream_final_received = true
}
}
@test
deepseek_stream_callback_receives_chunks :: proc(t: ^testing.T) {
stream_chunks_received = 0
stream_final_received = false
// Test that the stream callback mechanism works
// We can't easily mock curl streaming in tests, so test the callback type exists
callback: adapters.Stream_Callback = test_stream_callback
testing.expect(t, callback != nil, "stream callback type should be callable")
}
@test
deepseek_stream_requires_api_key :: proc(t: ^testing.T) {
cfg := shared.Config{deepseek_api_key = ""}
opts := adapters.Generate_Script_Options{
story_idea = "Test",
num_pages = 1,
}
_, err := adapters.stream_comic_script_stub(cfg, opts, nil)
testing.expect(t, !shared.is_ok(err), "should fail without API key")
testing.expect(t, strings.contains(err.message, "DEEPSEEK_API_KEY"), "error should mention missing key")
}
@test
deepseek_stream_validates_options :: proc(t: ^testing.T) {
cfg := shared.Config{deepseek_api_key = "test-key"}
opts := adapters.Generate_Script_Options{
story_idea = "",
num_pages = 1,
}
_, err := adapters.stream_comic_script_stub(cfg, opts, nil)
testing.expect(t, !shared.is_ok(err), "should fail with empty story_idea")
}