155 lines
5.5 KiB
Odin
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")
|
|
}
|