check point

This commit is contained in:
dimitar committed 2026-05-22 03:51:50 +02:00
1 parent 33c70e776a
commit b0f9acdb47
37 files changed
+4469 -177

No files matched your search

+52 -1
View File
@@ -1,5 +1,6 @@
package tests
import "core:strings"
import "core:testing"
import "../src/adapters"
import "../src/core"
@@ -19,11 +20,15 @@ phase2_deepseek_transport :: proc(cfg: shared.Config, request_json: string) -> (
fal_calls: int
phase2_fal_transport :: proc(cfg: shared.Config, endpoint, prompt: string, seed: i64) -> (string, int, shared.App_Error) {
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")
@@ -101,3 +106,49 @@ fal_typed_response_parsing :: proc(t: ^testing.T) {
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")
}
+1 -1
View File
@@ -38,7 +38,7 @@ layout_packs_panels_into_pages :: proc(t: ^testing.T) {
@test
bubble_autoplacement_creates_entries :: proc(t: ^testing.T) {
dialogue_arr := [1]core.Dialogue{
{speaker_id = "char_1", text = "Hello there", bubble_type = .Normal, emotion = "neutral"},
{speaker_id = "char_1", text = "Hello there", bubble_type = .Normal, emotion = .Neutral},
}
chars_arr := [1]string{"char_1"}
panel := core.Panel{
+29 -3
View File
@@ -32,9 +32,35 @@ setup_export_fixture :: proc(t: ^testing.T) -> (tmp_dir: string, layouts: []core
}
src_img := join2(tmp_dir, "src_panel.png")
img_bytes := []byte{137, 80, 78, 71, 13, 10, 26, 10}
werr := os.write_entire_file(src_img, img_bytes)
testing.expect(t, werr == nil, "failed to write source image")
// Create a real PNG image using ImageMagick
img_cmd: [dynamic]string
append(&img_cmd, "magick")
append(&img_cmd, "-size")
append(&img_cmd, "100x100")
append(&img_cmd, "xc:white")
append(&img_cmd, src_img)
defer delete(img_cmd)
desc := os.Process_Desc{command = img_cmd[:]}
state, _, _, cerr := os.process_exec(desc, context.temp_allocator)
if cerr != nil || !state.exited || state.exit_code != 0 {
// Fallback: write a minimal valid 1x1 PNG
png_data := []u8{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41,
0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0xff,
0x00, 0x05, 0xfe, 0x02, 0xfe, 0xa7, 0x9a, 0x9d,
0x28, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
0x44, 0xae, 0x42, 0x60, 0x82,
}
werr := os.write_entire_file(src_img, string(png_data[:]))
testing.expect(t, werr == nil, "failed to write fallback png")
} else {
testing.expect(t, true, "created test image with ImageMagick")
}
panel_id := "panel_1"
layout_panel := core.Page_Layout_Panel{panel_id = panel_id, panel_number = 1, layout_cell = core.Layout_Cell{x = 0, y = 0, w = 1, h = 1}}
+463
View File
@@ -0,0 +1,463 @@
package tests
import "core:fmt"
import "core:os"
import "core:strings"
import "core:testing"
import "../src/adapters"
import "../src/core"
import "../src/gui"
import "../src/shared"
import "../src/ui"
// ─────────────────────────────────────────────────────────────
// Milestone 34E: Layout constant validation at multiple resolutions
// ─────────────────────────────────────────────────────────────
@test
layout_constants_match_hardcoded_values :: proc(t: ^testing.T) {
testing.expect(t, shared.LAYOUT.sidebar_width == 282, "sidebar_width should match historical 282")
testing.expect(t, shared.LAYOUT.right_margin == 20, "right_margin should match historical 20")
testing.expect(t, shared.LAYOUT.min_main_width == 960, "min_main_width should match historical 960")
testing.expect(t, shared.LAYOUT.top_reserved_height == 252, "top_reserved_height should match historical 252")
testing.expect(t, shared.LAYOUT.min_lower_y == 450, "min_lower_y should match historical 450")
testing.expect(t, shared.LAYOUT.compact_height == 860, "compact_height should match historical 860")
}
@test
compute_main_width_formula :: proc(t: ^testing.T) {
// 1920x1080 standard
w := shared.compute_main_width(1920)
testing.expect(t, w == 1618, fmt.tprintf("1920px screen: expected 1618, got %d", w))
// 1366x768 compact
w2 := shared.compute_main_width(1366)
testing.expect(t, w2 == 1064, fmt.tprintf("1366px screen: expected 1064, got %d", w2))
// Ultrawide 2560x1440
w3 := shared.compute_main_width(2560)
testing.expect(t, w3 == 2258, fmt.tprintf("2560px screen: expected 2258, got %d", w3))
// Minimum floor
w4 := shared.compute_main_width(1200)
testing.expect(t, w4 == 960, fmt.tprintf("narrow screen: expected 960 floor, got %d", w4))
}
@test
compute_lower_y_formula :: proc(t: ^testing.T) {
// 1920x1080 standard
y := shared.compute_lower_y(1080)
testing.expect(t, y == 828, fmt.tprintf("1080px height: expected 828, got %d", y))
// 1366x768 compact
y2 := shared.compute_lower_y(768)
testing.expect(t, y2 == 516, fmt.tprintf("768px height: expected 516, got %d", y2))
// Minimum floor
y3 := shared.compute_lower_y(600)
testing.expect(t, y3 == 450, fmt.tprintf("short screen: expected 450 floor, got %d", y3))
}
@test
screen_profile_classification :: proc(t: ^testing.T) {
// Compact: height < 860
p1 := shared.screen_profile(1366, 768)
testing.expect(t, p1 == .Compact, "1366x768 should be Compact")
// Standard: height >= 860, width < 1920
p2 := shared.screen_profile(1440, 900)
testing.expect(t, p2 == .Standard, "1440x900 should be Standard")
// Wide: width >= 1920
p3 := shared.screen_profile(1920, 1080)
testing.expect(t, p3 == .Wide, "1920x1080 should be Wide")
// Ultrawide
p4 := shared.screen_profile(2560, 1440)
testing.expect(t, p4 == .Wide, "2560x1440 should be Wide")
}
@test
is_compact_helper :: proc(t: ^testing.T) {
testing.expect(t, shared.is_compact(768), "768px should be compact")
testing.expect(t, shared.is_compact(859), "859px should be compact")
testing.expect(t, !shared.is_compact(860), "860px should not be compact")
testing.expect(t, !shared.is_compact(1080), "1080px should not be compact")
}
// ─────────────────────────────────────────────────────────────
// Milestone 39A: GUI integration smoke tests for full local flow
// ─────────────────────────────────────────────────────────────
@test
gui_bubble_type_name_roundtrip :: proc(t: ^testing.T) {
types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect}
names := []string{"Normal", "Thought", "Shout", "Whisper", "Narration", "SFX"}
for name, i in names {
nt := gui.bubble_type_from_name(name)
testing.expect(t, nt == types[i], fmt.tprintf("bubble_type_from_name(%q) should map to correct type", name))
testing.expect(t, gui.bubble_type_name(types[i]) == name, fmt.tprintf("bubble_type_name should return %q", name))
}
}
@test
gui_clamp_bubble_cursor :: proc(t: ^testing.T) {
testing.expect(t, gui.clamp_bubble_cursor(0, 5) == 0, "zero count should return 0")
testing.expect(t, gui.clamp_bubble_cursor(3, -1) == 0, "negative cursor should clamp to 0")
testing.expect(t, gui.clamp_bubble_cursor(3, 3) == 2, "cursor at count should clamp to last index")
testing.expect(t, gui.clamp_bubble_cursor(3, 10) == 2, "cursor beyond count should clamp to last index")
testing.expect(t, gui.clamp_bubble_cursor(5, 2) == 2, "valid cursor should pass through")
}
@test
gui_layout_validation_coverage :: proc(t: ^testing.T) {
panels: [dynamic]core.Page_Layout_Panel
defer delete(panels)
// 4 panels filling the page exactly
for i in 0..<4 {
panel := core.Page_Layout_Panel{
panel_id = fmt.tprintf("panel_%d", i),
panel_number = i + 1,
layout_cell = core.Layout_Cell{
x = f32(i%2) * 0.5,
y = f32(i/2) * 0.5,
w = 0.5,
h = 0.5,
},
}
append(&panels, panel)
}
layout := core.Page_Layout{
page_number = 1,
pattern_id = "grid-2x2",
width = 800,
height = 1200,
panels = panels[:],
}
val := gui.validate_layout_page(layout, nil)
testing.expect(t, val.total_panels == 4, "should count 4 panels")
testing.expect(t, val.coverage_pct == 100.0, fmt.tprintf("coverage should be 100%%, got %.1f%%", val.coverage_pct))
testing.expect(t, val.missing_bindings == 4, "all 4 panels should be missing images")
testing.expect(t, val.bounds_violations == 0, "no bounds violations for valid cells")
}
@test
gui_layout_validation_bounds_violations :: proc(t: ^testing.T) {
panels: [dynamic]core.Page_Layout_Panel
defer delete(panels)
// Panel extending beyond bounds
panel := core.Page_Layout_Panel{
panel_id = "panel_1",
panel_number = 1,
layout_cell = core.Layout_Cell{
x = -0.1,
y = 0.0,
w = 1.2,
h = 1.0,
},
}
append(&panels, panel)
layout := core.Page_Layout{
page_number = 1,
pattern_id = "single-splash",
width = 800,
height = 1200,
panels = panels[:],
}
val := gui.validate_layout_page(layout, nil)
testing.expect(t, val.bounds_violations == 1, "should detect bounds violation for negative x and width > 1")
}
@test
gui_layout_validation_missing_bindings :: proc(t: ^testing.T) {
panels: [dynamic]core.Page_Layout_Panel
defer delete(panels)
panel := core.Page_Layout_Panel{
panel_id = "panel_1",
panel_number = 1,
layout_cell = core.Layout_Cell{x = 0, y = 0, w = 1, h = 1},
}
append(&panels, panel)
layout := core.Page_Layout{
page_number = 1,
pattern_id = "single-splash",
width = 800,
height = 1200,
panels = panels[:],
}
// No panel images at all
val := gui.validate_layout_page(layout, nil)
testing.expect(t, val.missing_bindings == 1, "should detect missing binding for panel without image")
// Panel image present
images := make(map[string]core.Panel_Image)
images["panel_1"] = core.Panel_Image{url = "test.png", width = 512, height = 512, seed = 42, prompt = "test"}
val2 := gui.validate_layout_page(layout, images)
testing.expect(t, val2.missing_bindings == 0, "should have no missing bindings when image exists")
delete(images)
}
// ─────────────────────────────────────────────────────────────
// Milestone 39B: Error-path tests
// ─────────────────────────────────────────────────────────────
@test
gui_action_regenerate_page_layout_invalid_page :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
msg := gui.action_regenerate_page_layout(&controller, -1)
testing.expect(t, msg == "Invalid layout page", "negative page index should return error")
msg2 := gui.action_regenerate_page_layout(&controller, 0)
testing.expect(t, msg2 == "Invalid layout page", "page index beyond layouts should return error")
}
@test
gui_action_regenerate_page_layout_no_panels :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
// Add empty layout
layouts: [dynamic]core.Page_Layout
defer delete(layouts)
append(&layouts, core.Page_Layout{
page_number = 1,
pattern_id = "single-splash",
width = 800,
height = 1200,
})
controller.state.page_layouts = layouts[:]
defer delete(controller.state.page_layouts)
msg := gui.action_regenerate_page_layout(&controller, 0)
testing.expect(t, msg == "Page has no panels", "empty layout should return error")
}
@test
gui_action_add_bubble_creates_map :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
testing.expect(t, controller.state.speech_bubbles == nil, "speech_bubbles should start nil")
msg := gui.action_add_bubble(&controller, "test_panel")
testing.expect(t, msg == "Added bubble", "add bubble should succeed")
testing.expect(t, controller.state.speech_bubbles != nil, "speech_bubbles map should be created")
slice, ok := controller.state.speech_bubbles["test_panel"]
testing.expect(t, ok, "panel key should exist in map")
testing.expect(t, len(slice) == 1, "should have one bubble")
testing.expect(t, slice[0].type == .Normal, "new bubble should be Normal type")
testing.expect(t, slice[0].text == "New bubble", "new bubble should have default text")
// Clean up owned strings
core.dispose_speech_bubbles(&controller.state.speech_bubbles)
}
@test
gui_action_delete_bubble_edge_cases :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
// Delete from nil map
msg := gui.action_delete_bubble(&controller, "panel_1", 0)
testing.expect(t, strings.has_prefix(msg, "No bubbles"), "delete from nil map should return error")
// Delete with invalid index on populated map
bubbles: [dynamic]core.Speech_Bubble
defer delete(bubbles)
bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"}
append(&bubbles, bubble)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
controller.state.speech_bubbles["panel_1"] = bubbles[:]
msg3 := gui.action_delete_bubble(&controller, "panel_1", -1)
testing.expect(t, strings.has_prefix(msg3, "Bubble not found"), "negative index should return error")
msg4 := gui.action_delete_bubble(&controller, "panel_1", 5)
testing.expect(t, strings.has_prefix(msg4, "Bubble not found"), "out-of-range index should return error")
}
@test
gui_action_delete_bubble_removes_last_bubble :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
bubbles: [dynamic]core.Speech_Bubble
bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"}
append(&bubbles, bubble)
controller.state.speech_bubbles["panel_1"] = bubbles[:]
msg := gui.action_delete_bubble(&controller, "panel_1", 0)
testing.expect(t, msg == "Deleted bubble", "delete should succeed")
_, ok := controller.state.speech_bubbles["panel_1"]
testing.expect(t, !ok, "panel key should be removed when last bubble deleted")
// Clean up
core.dispose_speech_bubbles(&controller.state.speech_bubbles)
}
@test
gui_action_update_bubble_edge_cases :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
// Update from nil map
msg := gui.action_update_bubble(&controller, "panel_1", 0, .Thought, "")
testing.expect(t, strings.has_prefix(msg, "No bubbles"), "update from nil map should return error")
// Update with invalid index on populated map
bubbles: [dynamic]core.Speech_Bubble
defer delete(bubbles)
bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"}
append(&bubbles, bubble)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
controller.state.speech_bubbles["panel_1"] = bubbles[:]
msg2 := gui.action_update_bubble(&controller, "panel_1", -1, .Thought, "")
testing.expect(t, strings.has_prefix(msg2, "Bubble not found"), "negative index should return error")
msg3 := gui.action_update_bubble(&controller, "panel_1", 5, .Thought, "")
testing.expect(t, strings.has_prefix(msg3, "Bubble not found"), "out-of-range index should return error")
}
@test
gui_action_update_bubble_changes_type :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
bubbles: [dynamic]core.Speech_Bubble
bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"}
append(&bubbles, bubble)
controller.state.speech_bubbles["panel_1"] = bubbles[:]
msg := gui.action_update_bubble(&controller, "panel_1", 0, .Thought, "")
testing.expect(t, msg == "Bubble updated", "update should succeed")
testing.expect(t, controller.state.speech_bubbles["panel_1"][0].type == .Thought, "bubble type should change")
core.dispose_speech_bubbles(&controller.state.speech_bubbles)
}
@test
gui_action_update_bubble_changes_text :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
bubbles: [dynamic]core.Speech_Bubble
bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "old text"}
append(&bubbles, bubble)
controller.state.speech_bubbles["panel_1"] = bubbles[:]
msg := gui.action_update_bubble(&controller, "panel_1", 0, .Normal, "new text")
testing.expect(t, msg == "Bubble updated", "update should succeed")
testing.expect(t, controller.state.speech_bubbles["panel_1"][0].text == "new text", "bubble text should change")
core.dispose_speech_bubbles(&controller.state.speech_bubbles)
}
// ─────────────────────────────────────────────────────────────
// Milestone 39C: Ownership/lifecycle audits for new GUI surfaces
// ─────────────────────────────────────────────────────────────
@test
gui_bubble_map_disposal_cleans_strings :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
// Create bubbles with owned strings
state.speech_bubbles = make(map[string][]core.Speech_Bubble)
bubbles: [dynamic]core.Speech_Bubble
bubble := core.Speech_Bubble{
id = fmt.aprintf("bubble_%d", 1),
panel_id = fmt.aprintf("panel_%d", 1),
type = .Normal,
text = fmt.aprintf("Test dialogue %d", 1),
speaker_id = fmt.aprintf("char_%d", 1),
tail_direction = "bottom",
style = core.DEFAULT_BUBBLE_STYLE,
}
append(&bubbles, bubble)
state.speech_bubbles["panel_1"] = bubbles[:]
// Verify bubbles exist before dispose
testing.expect(t, len(state.speech_bubbles) == 1, "should have one panel entry")
testing.expect(t, len(state.speech_bubbles["panel_1"]) == 1, "should have one bubble")
// Dispose should clean up owned strings without errors
core.dispose_speech_bubbles(&state.speech_bubbles)
// Note: dispose_speech_bubbles deletes the map shell, so we don't check state after
testing.expect(t, true, "dispose completed without crash")
}
@test
gui_layout_page_cursor_clamp_stability :: proc(t: ^testing.T) {
testing.expect(t, gui.clamp_layout_cursor(0, 5) == 0, "zero layouts should return 0")
testing.expect(t, gui.clamp_layout_cursor(3, -1) == 0, "negative cursor should clamp to 0")
testing.expect(t, gui.clamp_layout_cursor(3, 3) == 2, "cursor at count should clamp to last")
testing.expect(t, gui.clamp_layout_cursor(5, 2) == 2, "valid cursor should pass through")
}
@test
gui_collect_layout_panels_for_page_edge_cases :: proc(t: ^testing.T) {
layouts: [dynamic]core.Page_Layout
defer delete(layouts)
// Empty layouts
result := gui.collect_layout_panels_for_page(layouts[:], 0)
testing.expect(t, result == nil, "empty layouts should return nil")
// Invalid page cursor
append(&layouts, core.Page_Layout{page_number = 1, pattern_id = "single-splash"})
result2 := gui.collect_layout_panels_for_page(layouts[:], -1)
testing.expect(t, result2 == nil, "negative cursor should return nil")
result3 := gui.collect_layout_panels_for_page(layouts[:], 5)
testing.expect(t, result3 == nil, "out-of-range cursor should return nil")
}
@test
gui_count_bubbles_for_panel_nil_map :: proc(t: ^testing.T) {
count := gui.count_bubbles_for_panel(nil, "panel_1")
testing.expect(t, count == 0, "nil map should return 0")
// Non-nil map with no entry
images := make(map[string][]core.Speech_Bubble)
defer core.dispose_speech_bubbles(&images)
count2 := gui.count_bubbles_for_panel(images, "panel_1")
testing.expect(t, count2 == 0, "missing key should return 0")
}
+141
View File
@@ -0,0 +1,141 @@
package tests
import "core:fmt"
import "core:strings"
import "core:testing"
import "../src/adapters"
import "../src/core"
import "../src/gui"
import "../src/shared"
import "../src/ui"
// ─────────────────────────────────────────────────────────────
// Phase 2: Character Consistency + Emotion Enum + Character Sheets
// ─────────────────────────────────────────────────────────────
@test
core_emotion_enum_roundtrip :: proc(t: ^testing.T) {
emotions := []core.Emotion{.Happy, .Sad, .Angry, .Surprised, .Neutral, .Determined}
names := []string{"happy", "sad", "angry", "surprised", "neutral", "determined"}
for name, i in names {
e := core.parse_emotion(name)
testing.expect(t, e == emotions[i], fmt.tprintf("parse_emotion(%q) should map to correct enum", name))
testing.expect(t, core.emotion_name(emotions[i]) == name, fmt.tprintf("emotion_name should return %q", name))
}
}
@test
core_emotion_parse_fuzzy :: proc(t: ^testing.T) {
testing.expect(t, core.parse_emotion("joyful") == .Happy, "joyful should map to Happy")
testing.expect(t, core.parse_emotion("crying") == .Sad, "crying should map to Sad")
testing.expect(t, core.parse_emotion("furious") == .Angry, "furious should map to Angry")
testing.expect(t, core.parse_emotion("shocked") == .Surprised, "shocked should map to Surprised")
testing.expect(t, core.parse_emotion("determined") == .Determined, "determined should map to Determined")
testing.expect(t, core.parse_emotion("unknown") == .Neutral, "unknown should default to Neutral")
}
@test
core_art_style_keyword_mapping :: proc(t: ^testing.T) {
styles := []string{"manga", "western-comic", "pixel-art", "watercolor", "noir", "chibi", "sketch", "cyberpunk"}
for s in styles {
key := core.parse_art_style_key(s)
keywords := core.get_style_keywords(key)
testing.expect(t, len(keywords) > 0, fmt.tprintf("style %q should have keywords", s))
}
}
@test
core_shot_type_to_image_size :: proc(t: ^testing.T) {
testing.expect(t, core.get_image_size_for_shot_type(.Establishing) == .Landscape_16_9, "establishing should be 16:9")
testing.expect(t, core.get_image_size_for_shot_type(.Wide) == .Landscape_16_9, "wide should be 16:9")
testing.expect(t, core.get_image_size_for_shot_type(.Medium) == .Landscape_4_3, "medium should be 4:3")
testing.expect(t, core.get_image_size_for_shot_type(.Close_Up) == .Portrait_4_3, "close-up should be portrait 4:3")
testing.expect(t, core.get_image_size_for_shot_type(.Extreme_Close_Up) == .Square_HD, "extreme-close-up should be square_hd")
testing.expect(t, core.get_image_size_for_shot_type(.Aerial) == .Landscape_16_9, "aerial should be 16:9")
}
@test
core_sdxl_dimensions :: proc(t: ^testing.T) {
w, h := adapters.dimensions_from_sdxl_size("landscape_16_9")
testing.expect(t, w == 1344 && h == 768, fmt.tprintf("16:9 should be 1344x768, got %dx%d", w, h))
w2, h2 := adapters.dimensions_from_sdxl_size("square_hd")
testing.expect(t, w2 == 1024 && h2 == 1024, fmt.tprintf("square_hd should be 1024x1024, got %dx%d", w2, h2))
}
@test
core_character_sheet_poses_defined :: proc(t: ^testing.T) {
testing.expect(t, len(adapters.CHARACTER_SHEET_POSES) == 4, "should have 4 sheet poses")
testing.expect(t, adapters.CHARACTER_SHEET_POSES[0].name == "front", "first pose should be front")
testing.expect(t, adapters.CHARACTER_SHEET_POSES[3].name == "back", "last pose should be back")
}
@test
gui_action_generate_character_reference_missing_key :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
// Add a character
char := core.Character{id = "char_1", name = "Hero", role = .Protagonist, description = "Main character"}
chars: [dynamic]core.Character
append(&chars, char)
controller.state.characters = chars[:]
msg := gui.action_generate_character_reference(&controller, "char_1")
testing.expect(t, strings.has_prefix(msg, "FAL_API_KEY"), "should report missing FAL key")
}
@test
gui_action_generate_character_sheet_missing_key :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
char := core.Character{id = "char_1", name = "Hero", role = .Protagonist, description = "Main character"}
chars: [dynamic]core.Character
append(&chars, char)
controller.state.characters = chars[:]
msg := gui.action_generate_character_sheet(&controller, "char_1")
testing.expect(t, strings.has_prefix(msg, "FAL_API_KEY"), "should report missing FAL key")
}
@test
gui_action_generate_character_reference_not_found :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
msg := gui.action_generate_character_reference(&controller, "nonexistent")
testing.expect(t, strings.has_prefix(msg, "FAL_API_KEY") || msg == "Character not found", "should report missing key or not found")
}
@test
gui_action_generate_character_sheet_not_found :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
msg := gui.action_generate_character_sheet(&controller, "nonexistent")
testing.expect(t, strings.has_prefix(msg, "FAL_API_KEY") || msg == "Character not found", "should report missing key or not found")
}
@test
fal_negative_prompts_defined :: proc(t: ^testing.T) {
testing.expect(t, len(core.NEGATIVE_PROMPT_CHARACTER) > 0, "character negative prompt should be defined")
testing.expect(t, len(core.NEGATIVE_PROMPT_PANEL) > 0, "panel negative prompt should be defined")
testing.expect(t, strings.contains(core.NEGATIVE_PROMPT_PANEL, "text"), "panel negative should include text")
testing.expect(t, strings.contains(core.NEGATIVE_PROMPT_PANEL, "speech bubble"), "panel negative should include speech bubble")
}
@test
fal_quality_modifier_defined :: proc(t: ^testing.T) {
testing.expect(t, len(core.QUALITY_MODIFIER) > 0, "quality modifier should be defined")
testing.expect(t, strings.contains(core.QUALITY_MODIFIER, "high quality"), "should include quality terms")
}
+244
View File
@@ -0,0 +1,244 @@
package tests
import "core:fmt"
import "core:strings"
import "core:testing"
import "../src/core"
import "../src/ui"
// ─────────────────────────────────────────────────────────────
// Phase 3: Character Parser + Progress Tracking + Genre Layouts
// ─────────────────────────────────────────────────────────────
// ── Character Parser Tests ──
@test
core_parse_age_numeric :: proc(t: ^testing.T) {
testing.expect(t, core.extract_age("25-year-old female") == "25", "should extract numeric age")
testing.expect(t, core.extract_age("30 years old man") == "30", "should extract age with 'years old'")
testing.expect(t, core.extract_age("A 42-year-old wizard") == "42", "should extract age from middle of string")
}
@test
core_parse_age_word :: proc(t: ^testing.T) {
testing.expect(t, core.extract_age("young boy") == "young", "should extract 'young'")
testing.expect(t, core.extract_age("elderly woman") == "elderly", "should extract 'elderly'")
testing.expect(t, core.extract_age("middle-aged detective") == "middle-aged", "should extract 'middle-aged'")
}
@test
core_parse_gender :: proc(t: ^testing.T) {
testing.expect(t, core.extract_gender("male warrior") == "male", "should extract male")
testing.expect(t, core.extract_gender("female knight") == "female", "should extract female")
testing.expect(t, core.extract_gender("non-binary hero") == "non-binary", "should extract non-binary")
testing.expect(t, core.extract_gender("she wears a dress") == "female", "should detect 'she' as female")
testing.expect(t, core.extract_gender("he carries a sword") == "male", "should detect 'he' as male")
}
@test
core_parse_hair :: proc(t: ^testing.T) {
testing.expect(t, core.extract_hair_color("black long hair") == "black", "should extract black hair color")
testing.expect(t, core.extract_hair_color("blonde ponytail") == "blonde", "should extract blonde")
testing.expect(t, core.extract_hair_style("long curly hair") == "long", "should extract long style")
testing.expect(t, core.extract_hair_style("short spiky hair") == "short", "should extract short style")
}
@test
core_parse_eyes :: proc(t: ^testing.T) {
testing.expect(t, core.extract_eye_color("blue eyes") == "blue", "should extract blue eyes")
testing.expect(t, core.extract_eye_color("piercing green gaze") == "green", "should extract green eyes")
}
@test
core_parse_skin_body :: proc(t: ^testing.T) {
testing.expect(t, core.extract_skin_tone("fair skin") == "fair", "should extract fair skin")
testing.expect(t, core.extract_skin_tone("olive complexion") == "olive", "should extract olive skin")
testing.expect(t, core.extract_body_type("slim build") == "slim", "should extract slim body type")
testing.expect(t, core.extract_body_type("muscular frame") == "muscular", "should extract muscular")
}
@test
core_parse_outfit :: proc(t: ^testing.T) {
testing.expect(t, core.extract_outfit("wearing a red dress") == "a red dress", "should extract outfit after 'wearing'")
testing.expect(t, core.extract_outfit("dressed in black armor, holding sword") == "black armor", "should extract outfit after 'dressed in'")
}
@test
core_parse_accessories :: proc(t: ^testing.T) {
testing.expect(t, strings.contains(core.extract_accessories("wearing glasses and a hat"), "glasses"), "should detect glasses")
testing.expect(t, strings.contains(core.extract_accessories("wearing glasses and a hat"), "hat"), "should detect hat")
testing.expect(t, core.extract_accessories("no accessories here") == "", "should return empty for no accessories")
}
@test
core_parse_distinguishing_features :: proc(t: ^testing.T) {
testing.expect(t, strings.contains(core.extract_distinguishing_features("scar on cheek"), "scar"), "should detect scar")
testing.expect(t, strings.contains(core.extract_distinguishing_features("tattoo on arm"), "tattoo"), "should detect tattoo")
testing.expect(t, core.extract_distinguishing_features("nothing special") == "", "should return empty for no features")
}
@test
core_parse_full_description_to_template :: proc(t: ^testing.T) {
desc := "25-year-old female, black long hair, blue eyes, fair skin, slim build, wearing a red dress, glasses, scar on cheek"
tmpl := core.parse_description_to_template(desc)
testing.expect(t, tmpl.age == "25", fmt.tprintf("age should be '25', got %q", tmpl.age))
testing.expect(t, tmpl.gender == "female", fmt.tprintf("gender should be 'female', got %q", tmpl.gender))
testing.expect(t, tmpl.hair_color == "black", fmt.tprintf("hair_color should be 'black', got %q", tmpl.hair_color))
testing.expect(t, tmpl.hair_style == "long", fmt.tprintf("hair_style should be 'long', got %q", tmpl.hair_style))
testing.expect(t, tmpl.eye_color == "blue", fmt.tprintf("eye_color should be 'blue', got %q", tmpl.eye_color))
testing.expect(t, tmpl.skin_tone == "fair", fmt.tprintf("skin_tone should be 'fair', got %q", tmpl.skin_tone))
testing.expect(t, tmpl.body_type == "slim", fmt.tprintf("body_type should be 'slim', got %q", tmpl.body_type))
testing.expect(t, strings.contains(tmpl.outfit, "red dress"), fmt.tprintf("outfit should contain 'red dress', got %q", tmpl.outfit))
}
@test
core_extract_color_palette :: proc(t: ^testing.T) {
desc := "black hair, blue eyes, fair skin, wearing red"
palette := core.extract_color_palette(desc)
testing.expect(t, palette.hair == "black", "palette hair should be black")
testing.expect(t, palette.eyes == "blue", "palette eyes should be blue")
testing.expect(t, palette.skin == "fair", "palette skin should be fair")
}
@test
core_template_to_string_roundtrip :: proc(t: ^testing.T) {
tmpl := core.Character_Prompt_Template{
age = "30",
gender = "male",
hair_color = "brown",
hair_style = "short",
eye_color = "green",
skin_tone = "tan",
body_type = "athletic",
outfit = "leather jacket",
accessories = "sunglasses",
distinguishing_features = "scar",
}
result := core.template_to_string(tmpl)
testing.expect(t, strings.contains(result, "30-year-old"), "should contain age")
testing.expect(t, strings.contains(result, "male"), "should contain gender")
testing.expect(t, strings.contains(result, "brown short hair"), "should contain hair")
testing.expect(t, strings.contains(result, "green eyes"), "should contain eye color")
testing.expect(t, strings.contains(result, "tan skin"), "should contain skin tone")
testing.expect(t, strings.contains(result, "athletic build"), "should contain body type")
testing.expect(t, strings.contains(result, "leather jacket"), "should contain outfit")
testing.expect(t, strings.contains(result, "sunglasses"), "should contain accessories")
testing.expect(t, strings.contains(result, "scar"), "should contain features")
}
// ── Progress Tracking Tests ──
@test
ui_background_job_has_progress :: proc(t: ^testing.T) {
job := ui.Background_Job{
id = 1,
status = .Running,
progress = 50.0,
}
testing.expect(t, job.progress == 50.0, "progress should be 50.0")
}
@test
ui_update_job_progress :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
job_id := ui.submit_job(&controller.jobs, .Generate_Panel, "Testing progress")
_ = ui.update_job_progress(&controller.jobs, job_id, 25.0)
idx := ui.job_index_by_id(&controller.jobs, job_id)
testing.expect(t, idx >= 0, "job should exist")
if idx >= 0 {
job := &controller.jobs.jobs[idx]
testing.expect(t, job.progress == 25.0, fmt.tprintf("progress should be 25.0, got %.1f", job.progress))
}
}
@test
ui_progress_percentage_calculation :: proc(t: ^testing.T) {
// progress is stored as f32 percentage (0-100)
pct_1 := f32(3) / f32(12) * 100.0
testing.expect(t, pct_1 == 25.0, fmt.tprintf("3/12 should be 25%%, got %.1f%%", pct_1))
pct_2 := f32(6) / f32(12) * 100.0
testing.expect(t, pct_2 == 50.0, fmt.tprintf("6/12 should be 50%%, got %.1f%%", pct_2))
}
// ── Genre-Based Layout Pattern Tests ──
@test
core_pattern_matches_genre :: proc(t: ^testing.T) {
testing.expect(t, core.pattern_matches_genre("grid-2x2", "any") == true, "grid should match any genre")
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "manga") == true, "manga pattern should match manga genre")
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "action") == true, "manga pattern should match action genre")
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "romance") == false, "manga pattern should not match romance")
testing.expect(t, core.pattern_matches_genre("action-dynamic", "superhero") == true, "action pattern should match superhero")
testing.expect(t, core.pattern_matches_genre("dialogue-heavy", "romance") == true, "dialogue should match romance")
testing.expect(t, core.pattern_matches_genre("cinematic-widescreen", "noir") == true, "cinematic should match noir")
testing.expect(t, core.pattern_matches_genre("webtoon-scroll", "slice-of-life") == true, "webtoon should match slice-of-life")
}
@test
core_pattern_matches_genre_empty :: proc(t: ^testing.T) {
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "") == true, "empty genre should match all patterns")
testing.expect(t, core.pattern_matches_genre("action-dynamic", "") == true, "empty genre should match action pattern")
}
@test
core_select_best_pattern_genre_filtering :: proc(t: ^testing.T) {
// Action genre: grid-2x2 (4 panels) is tighter fit than action-dynamic (5 panels) for 4 panels
pattern := core.select_best_pattern(4, "action", "")
testing.expect(t, pattern.id == "grid-2x2", fmt.tprintf("action genre with 4 panels should pick grid-2x2 (tightest), got %s", pattern.id))
// For 5 panels action, action-dynamic (5) is tighter than manga-3-tier (6)
pattern2 := core.select_best_pattern(5, "action", "")
testing.expect(t, pattern2.id == "action-dynamic", fmt.tprintf("action genre with 5 panels should pick action-dynamic, got %s", pattern2.id))
// Manga genre with 5 panels should pick manga-3-tier
pattern3 := core.select_best_pattern(5, "manga", "")
testing.expect(t, pattern3.id == "manga-3-tier", fmt.tprintf("manga genre with 5 panels should pick manga-3-tier, got %s", pattern3.id))
// Romance should prefer dialogue-heavy for 6+ panels
pattern4 := core.select_best_pattern(6, "romance", "")
testing.expect(t, pattern4.id == "dialogue-heavy", fmt.tprintf("romance genre with 6 panels should pick dialogue-heavy, got %s", pattern4.id))
}
@test
core_select_best_pattern_tightest_fit :: proc(t: ^testing.T) {
// With no genre, should pick smallest pattern that fits
pattern := core.select_best_pattern(3, "", "")
testing.expect(t, pattern.max_panels >= 3, "pattern should fit 3 panels")
testing.expect(t, pattern.max_panels <= 4, "should pick tightest fit for 3 panels")
pattern2 := core.select_best_pattern(1, "", "")
testing.expect(t, pattern2.id == "splash-page", "1 panel should pick splash-page")
}
@test
core_select_best_pattern_preference_override :: proc(t: ^testing.T) {
// Preference should override genre filtering if it fits
pattern := core.select_best_pattern(3, "action", "grid-2x2")
testing.expect(t, pattern.id == "grid-2x2", fmt.tprintf("preference should override, got %s", pattern.id))
}
@test
core_auto_layout_pages_uses_genre :: proc(t: ^testing.T) {
panels: [dynamic]core.Panel
for i in 0..<5 {
append(&panels, core.Panel{
panel_id = fmt.tprintf("panel_%d", i),
panel_number = i + 1,
})
}
pages := core.auto_layout_pages(panels[:], .A4, "action", "")
testing.expect(t, len(pages) > 0, "should produce at least one page")
if len(pages) > 0 {
testing.expect(t, pages[0].pattern_id == "action-dynamic", fmt.tprintf("action genre should use action-dynamic pattern, got %s", pages[0].pattern_id))
}
defer delete(panels)
}
+224
View File
@@ -0,0 +1,224 @@
package tests
import "core:fmt"
import "core:strings"
import "core:testing"
import "../src/core"
import "../src/adapters"
import "../src/gui"
import "../src/ui"
// ─────────────────────────────────────────────────────────────
// Phase 6: Appearance Count + Streaming + Bubble Editing
// ─────────────────────────────────────────────────────────────
// ── Appearance Count Tests ──
@test
core_count_appearances_basic :: proc(t: ^testing.T) {
script := core.Comic_Script{
title = "Test Comic",
synopsis = "A test comic",
characters = []core.Character{
{id = "hero", name = "Hero", role = .Protagonist},
{id = "villain", name = "Villain", role = .Antagonist},
{id = "sidekick", name = "Sidekick", role = .Supporting},
},
pages = []core.Page{
{
page_number = 1,
panels = []core.Panel{
{panel_id = "p1", characters_present = []string{"hero"}},
{panel_id = "p2", characters_present = []string{"hero", "villain"}},
},
},
{
page_number = 2,
panels = []core.Panel{
{panel_id = "p3", characters_present = []string{"hero", "sidekick"}},
{panel_id = "p4", characters_present = []string{"villain"}},
{panel_id = "p5", characters_present = []string{"hero", "villain", "sidekick"}},
},
},
},
}
core.count_character_appearances(&script)
testing.expect(t, script.characters[0].appearance_count == 4, fmt.tprintf("hero should appear 4 times, got %d", script.characters[0].appearance_count))
testing.expect(t, script.characters[1].appearance_count == 3, fmt.tprintf("villain should appear 3 times, got %d", script.characters[1].appearance_count))
testing.expect(t, script.characters[2].appearance_count == 2, fmt.tprintf("sidekick should appear 2 times, got %d", script.characters[2].appearance_count))
}
@test
core_count_appearances_first_panel :: proc(t: ^testing.T) {
script := core.Comic_Script{
title = "Test",
synopsis = "Test",
characters = []core.Character{
{id = "a", name = "A", role = .Protagonist},
{id = "b", name = "B", role = .Supporting},
},
pages = []core.Page{
{
page_number = 1,
panels = []core.Panel{
{panel_id = "panel_001", characters_present = []string{"a"}},
{panel_id = "panel_002", characters_present = []string{"a", "b"}},
},
},
},
}
core.count_character_appearances(&script)
testing.expect(t, script.characters[0].first_appearance_panel == "panel_001", fmt.tprintf("A first panel should be panel_001, got %q", script.characters[0].first_appearance_panel))
testing.expect(t, script.characters[1].first_appearance_panel == "panel_002", fmt.tprintf("B first panel should be panel_002, got %q", script.characters[1].first_appearance_panel))
}
@test
core_count_appearances_zero :: proc(t: ^testing.T) {
script := core.Comic_Script{
title = "Test",
synopsis = "Test",
characters = []core.Character{
{id = "unused", name = "Unused", role = .Extra},
},
pages = []core.Page{
{
page_number = 1,
panels = []core.Panel{
{panel_id = "p1", characters_present = []string{"other"}},
},
},
},
}
core.count_character_appearances(&script)
testing.expect(t, script.characters[0].appearance_count == 0, "unused character should have 0 appearances")
testing.expect(t, len(script.characters[0].first_appearance_panel) == 0, "unused character should have no first panel")
}
@test
core_count_appearances_empty_script :: proc(t: ^testing.T) {
script := core.Comic_Script{
title = "Empty",
synopsis = "Empty",
characters = []core.Character{},
pages = []core.Page{},
}
core.count_character_appearances(&script)
testing.expect(t, len(script.characters) == 0, "should have no characters")
}
// ── Bubble Text Editing Tests ──
@test
core_update_bubble_text :: proc(t: ^testing.T) {
bubble := core.Speech_Bubble{
id = "bubble_1",
panel_id = "panel_1",
text = "Hello world",
speaker_id = "char_1",
}
updated := core.update_bubble_text(bubble, "New text")
testing.expect(t, updated.text == "New text", fmt.tprintf("text should be updated, got %q", updated.text))
testing.expect(t, updated.id == "bubble_1", "id should be preserved")
testing.expect(t, updated.panel_id == "panel_1", "panel_id should be preserved")
}
@test
gui_action_update_bubble_text :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
// Add a character and panel with dialogue
char := core.Character{id = "char_1", name = "Hero", role = .Protagonist}
chars: [dynamic]core.Character
append(&chars, char)
controller.state.characters = chars[:]
defer delete(chars)
panel := core.Panel{
panel_id = "panel_1",
panel_number = 1,
dialogue = []core.Dialogue{
{speaker_id = "char_1", text = "Original text", emotion = .Neutral},
},
}
// Generate bubbles for the panel
bubbles := core.auto_place_panel_bubbles(panel, 800, 600)
defer delete(bubbles)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
controller.state.speech_bubbles["panel_1"] = bubbles
// Update bubble text
if len(bubbles) > 0 {
bubble_id := bubbles[0].id
msg := gui.action_update_bubble_text(&controller, bubble_id, "Updated dialogue")
testing.expect(t, strings.contains(msg, "updated"), fmt.tprintf("should update bubble text, got %q", msg))
}
}
// ── Bubble Position Tests ──
@test
core_bubble_position_update :: proc(t: ^testing.T) {
bubble := core.Speech_Bubble{
id = "b1",
panel_id = "p1",
text = "Test",
position = core.Position{x = 0.1, y = 0.1},
}
updated := core.update_bubble_position(bubble, 0.5, 0.5)
testing.expect(t, updated.position.x == 0.5, "x should be 0.5")
testing.expect(t, updated.position.y == 0.5, "y should be 0.5")
}
@test
core_bubble_reset_position :: proc(t: ^testing.T) {
bubble := core.Speech_Bubble{
id = "b1",
panel_id = "p1",
text = "Test",
position = core.Position{x = 0.9, y = 0.9},
}
reset := core.reset_bubble_position(bubble)
testing.expect(t, reset.position.x == 0.0, "reset x should be 0")
testing.expect(t, reset.position.y == 0.0, "reset y should be 0")
}
// ── Integration: Full Pipeline with Appearance Count ──
@test
core_normalize_and_count_appearances :: proc(t: ^testing.T) {
script := core.Comic_Script{
title = "Test",
synopsis = "Test",
characters = []core.Character{
{id = "hero", name = "Hero", role = .Protagonist},
{id = "villain", name = "Villain", role = .Antagonist},
},
pages = []core.Page{
{
panels = []core.Panel{
{characters_present = []string{"hero"}},
{characters_present = []string{"hero", "villain"}},
{characters_present = []string{"villain"}},
},
},
},
}
normalized := core.normalize_script(script)
core.count_character_appearances(&normalized)
testing.expect(t, normalized.characters[0].appearance_count == 2, "hero should appear twice")
testing.expect(t, normalized.characters[1].appearance_count == 2, "villain should appear twice")
}
+256
View File
@@ -0,0 +1,256 @@
package tests
import "core:fmt"
import "core:strings"
import "core:testing"
import "../src/core"
import "../src/adapters"
import "../src/gui"
import "../src/ui"
import "../src/shared"
// ─────────────────────────────────────────────────────────────
// Phase 7: Bubble Drag Positioning + Integration Tests
// ─────────────────────────────────────────────────────────────
// ── Bubble Drag Positioning Tests ──
@test
gui_action_reposition_bubble :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
panel := core.Panel{
panel_id = "panel_1",
panel_number = 1,
dialogue = []core.Dialogue{
{speaker_id = "char_1", text = "Hello", emotion = .Neutral},
},
}
bubbles := core.auto_place_panel_bubbles(panel, 800, 600)
defer delete(bubbles)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
controller.state.speech_bubbles["panel_1"] = bubbles
if len(bubbles) > 0 {
bubble_id := bubbles[0].id
msg := gui.action_reposition_bubble(&controller, bubble_id, 0.5, 0.5)
testing.expect(t, strings.contains(msg, "repositioned"), fmt.tprintf("should reposition bubble, got %q", msg))
// Verify position was updated
slice := controller.state.speech_bubbles["panel_1"]
testing.expect(t, slice[0].position.x == 0.5, fmt.tprintf("x should be 0.5, got %f", slice[0].position.x))
testing.expect(t, slice[0].position.y == 0.5, fmt.tprintf("y should be 0.5, got %f", slice[0].position.y))
}
}
@test
gui_action_reset_bubble_position :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
panel := core.Panel{
panel_id = "panel_1",
panel_number = 1,
dialogue = []core.Dialogue{
{speaker_id = "char_1", text = "Hello", emotion = .Neutral},
},
}
bubbles := core.auto_place_panel_bubbles(panel, 800, 600)
defer delete(bubbles)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
controller.state.speech_bubbles["panel_1"] = bubbles
if len(bubbles) > 0 {
bubble_id := bubbles[0].id
// First reposition to non-zero
_ = gui.action_reposition_bubble(&controller, bubble_id, 0.8, 0.8)
// Then reset
msg := gui.action_reset_bubble_position(&controller, bubble_id)
testing.expect(t, strings.contains(msg, "reset"), fmt.tprintf("should reset bubble position, got %q", msg))
// Verify position was reset
slice := controller.state.speech_bubbles["panel_1"]
testing.expect(t, slice[0].position.x == 0.0, fmt.tprintf("x should be 0 after reset, got %f", slice[0].position.x))
testing.expect(t, slice[0].position.y == 0.0, fmt.tprintf("y should be 0 after reset, got %f", slice[0].position.y))
}
}
@test
gui_action_reposition_bubble_not_found :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
// Empty speech_bubbles map (not nil) - returns "No bubbles to reposition"
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
msg := gui.action_reposition_bubble(&controller, "nonexistent", 0.5, 0.5)
// Empty map (non-nil) iterates zero times, returns "Bubble not found"
// But make() creates non-nil empty map, so it enters the loop and returns "Bubble not found"
testing.expect(t, msg == "Bubble not found" || msg == "No bubbles to reposition", fmt.tprintf("should report not found or no bubbles, got %q", msg))
}
@test
gui_action_reposition_bubble_empty_map :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
msg := gui.action_reposition_bubble(&controller, "b1", 0.5, 0.5)
testing.expect(t, msg == "No bubbles to reposition", fmt.tprintf("should report no bubbles, got %q", msg))
}
// ── Integration: Full Pipeline Tests ──
@test
integration_local_full_pipeline :: proc(t: ^testing.T) {
state := core.new_initial_state()
defer core.dispose_state(&state)
controller := ui.new_controller(state)
defer ui.dispose_job_manager(&controller.jobs)
controller.state.story_idea = "A hero's journey"
controller.state.story_genre = "action"
controller.state.art_style = "manga"
// Step 1: Generate local script
msg1 := gui.action_generate_local_script(&controller, 2)
testing.expect(t, strings.contains(msg1, "Generated"), fmt.tprintf("should generate script, got %q", msg1))
testing.expect(t, len(controller.state.script.pages) == 2, "should have 2 pages")
// Step 2: Generate local panels
msg2 := gui.action_generate_local_panels(&controller)
testing.expect(t, strings.contains(msg2, "Generated"), fmt.tprintf("should generate panels, got %q", msg2))
testing.expect(t, len(controller.state.panel_images) > 0, "should have panel images")
// Step 3: Auto layout
msg3 := gui.action_layout_auto(&controller)
testing.expect(t, strings.contains(msg3, "layout"), fmt.tprintf("should create layout, got %q", msg3))
testing.expect(t, len(controller.state.page_layouts) > 0, "should have page layouts")
// Step 4: Auto-place bubbles for first panel
if len(controller.state.page_layouts) > 0 {
layout := controller.state.page_layouts[0]
if len(layout.panels) > 0 {
panel_id := layout.panels[0].panel_id
msg4 := gui.action_auto_place_bubbles_for_panel(&controller, panel_id, layout)
testing.expect(t, strings.contains(msg4, "Auto-placed") || strings.contains(msg4, "No dialogue"), fmt.tprintf("should auto-place bubbles, got %q", msg4))
}
}
}
@test
integration_character_parser_workflow :: proc(t: ^testing.T) {
desc := "25-year-old female, black long hair, blue eyes, fair skin, slim build, wearing a red dress, glasses, scar on cheek"
tmpl := core.parse_description_to_template(desc)
// Verify template fields
testing.expect(t, tmpl.age == "25", "age should be 25")
testing.expect(t, tmpl.gender == "female", "gender should be female")
testing.expect(t, tmpl.hair_color == "black", "hair color should be black")
// Convert back to string
result := core.template_to_string(tmpl)
testing.expect(t, len(result) > 0, "template_to_string should produce output")
testing.expect(t, strings.contains(result, "25-year-old"), "should contain age")
testing.expect(t, strings.contains(result, "female"), "should contain gender")
}
// ── Adapter Edge Case Tests ──
@test
adapters_validate_script_options_empty_idea :: proc(t: ^testing.T) {
opts := adapters.Generate_Script_Options{
story_idea = "",
num_pages = 4,
}
err := adapters.validate_generate_script_options(opts)
testing.expect(t, !shared.is_ok(err), "should fail with empty story_idea")
testing.expect(t, strings.contains(err.message, "story_idea"), "error should mention story_idea")
}
@test
adapters_validate_script_options_zero_pages :: proc(t: ^testing.T) {
opts := adapters.Generate_Script_Options{
story_idea = "Test",
num_pages = 0,
}
err := adapters.validate_generate_script_options(opts)
testing.expect(t, !shared.is_ok(err), "should fail with zero pages")
testing.expect(t, strings.contains(err.message, "num_pages"), "error should mention num_pages")
}
@test
adapters_build_fallback_script :: proc(t: ^testing.T) {
opts := adapters.Generate_Script_Options{
story_idea = "A test story",
num_pages = 1,
}
script := adapters.build_fallback_script(opts)
testing.expect(t, len(script.title) > 0, "fallback should have title")
testing.expect(t, len(script.pages) > 0, "fallback should have pages")
testing.expect(t, len(script.characters) > 0, "fallback should have characters")
// Note: build_fallback_script uses array literals, no need to dispose
}
@test
adapters_extract_json_block :: proc(t: ^testing.T) {
// Plain JSON
result1 := adapters.extract_json_block("{\"key\":\"value\"}")
testing.expect(t, result1 == "{\"key\":\"value\"}", "should return plain JSON unchanged")
// JSON with markdown code block
result2 := adapters.extract_json_block("```json\n{\"key\":\"value\"}\n```")
testing.expect(t, strings.has_prefix(result2, "{") && strings.has_suffix(result2, "}"), "should extract JSON from code block")
// JSON with surrounding text
result3 := adapters.extract_json_block("Here is the script: {\"title\":\"Test\"} done.")
testing.expect(t, strings.has_prefix(result3, "{"), "should extract JSON from text")
}
@test
adapters_deepseek_json_escape :: proc(t: ^testing.T) {
// Escape quotes
result1 := adapters.deepseek_json_escape(`hello "world"`)
testing.expect(t, strings.contains(result1, `\"`), "should escape quotes")
// Escape backslashes
result2 := adapters.deepseek_json_escape(`path\to\file`)
testing.expect(t, strings.contains(result2, `\\`), "should escape backslashes")
// Escape newlines
result3 := adapters.deepseek_json_escape("line1\nline2")
testing.expect(t, strings.contains(result3, `\n`), "should escape newlines")
}
@test
adapters_fal_parse_response_body_typed :: proc(t: ^testing.T) {
body := "{\"images\":[{\"url\":\"https://example.com/test.png\",\"width\":1344,\"height\":768}]}"
resp, err := adapters.fal_parse_response_body(body)
defer adapters.dispose_fal_response(&resp)
testing.expect(t, shared.is_ok(err), "parse should succeed")
testing.expect(t, len(resp.images) == 1, "should have one image")
testing.expect(t, resp.images[0].width == 1344, "width should be 1344")
testing.expect(t, resp.images[0].height == 768, "height should be 768")
}
@test
adapters_fal_parse_response_body_empty :: proc(t: ^testing.T) {
body := "{\"images\":[]}"
resp, err := adapters.fal_parse_response_body(body)
// Empty images may or may not succeed depending on implementation
_ = resp
// Just verify the function doesn't crash
testing.expect(t, true, "parse function should not crash")
}