comic/odin/tests/app_cli_phase6.odin
2026-05-21 06:10:32 +02:00

527 lines
20 KiB
Odin

package tests
import "core:fmt"
import "core:os"
import "core:strings"
import "core:testing"
import app "../src/app"
import "../src/core"
import "../src/shared"
import "../src/ui"
@test
cli_parse_commands :: proc(t: ^testing.T) {
c1 := app.parse_cli_command(nil)
testing.expect(t, c1.kind == .Demo, "no args should map to demo")
a2 := [1]string{"status"}
c2 := app.parse_cli_command(a2[:])
testing.expect(t, c2.kind == .Status, "status should parse")
a3 := [2]string{"save", "x.json"}
c3 := app.parse_cli_command(a3[:])
testing.expect(t, c3.kind == .Save, "save should parse")
testing.expect(t, c3.path == "x.json", "save path should parse")
a4 := [1]string{"tui"}
c4 := app.parse_cli_command(a4[:])
testing.expect(t, c4.kind == .Tui, "tui should parse")
a5 := [1]string{"gui"}
c5 := app.parse_cli_command(a5[:])
testing.expect(t, c5.kind == .Gui, "gui should parse")
testing.expect(t, app.normalize_tui_command("q") == "quit", "q alias should expand")
testing.expect(t, app.normalize_tui_command("1") == "goto story", "1 alias should map to story")
testing.expect(t, app.normalize_tui_command("?") == "doctor", "? alias should map to doctor")
testing.expect(t, app.normalize_tui_command("r") == "ready", "r alias should map to ready")
testing.expect(t, app.normalize_tui_command("n") == "next", "n alias should map to next")
testing.expect(t, app.normalize_tui_command("p") == "plan", "p alias should map to plan")
testing.expect(t, app.normalize_tui_command("x") == "auto", "x alias should map to auto")
pages, matched, perr := app.parse_generate_script_pages("generate script 6")
testing.expect(t, shared.is_ok(perr), "generate script pages parse should succeed")
testing.expect(t, matched, "generate script pages should match")
testing.expect(t, pages == 6, "generate script pages should parse value")
local_pages, lmatched, lerr := app.parse_generate_script_local_pages("generate script local 3")
testing.expect(t, shared.is_ok(lerr), "generate script local parse should succeed")
testing.expect(t, lmatched, "generate script local should match")
testing.expect(t, local_pages == 3, "generate script local should parse value")
page, pmatch, pperr := app.parse_generate_panels_page("generate panels page 2")
testing.expect(t, shared.is_ok(pperr), "generate panels page parse should succeed")
testing.expect(t, pmatch, "generate panels page should match")
testing.expect(t, page == 2, "generate panels page should parse value")
lpage, lpmatch, lperr := app.parse_generate_panels_local_page("generate panels local page 2")
testing.expect(t, shared.is_ok(lperr), "generate panels local page parse should succeed")
testing.expect(t, lpmatch, "generate panels local page should match")
testing.expect(t, lpage == 2, "generate panels local page should parse value")
fmt_kind, export_path, ematch, eerr := app.parse_export_command("export cbz ./out.cbz")
testing.expect(t, shared.is_ok(eerr), "export parse should succeed")
testing.expect(t, ematch, "export parse should match")
testing.expect(t, fmt_kind == .CBZ, "export format should parse")
testing.expect(t, export_path == "./out.cbz", "export path should parse")
qfmt, qpath, qpages, qmatch, qerr := app.parse_quick_local_command("quick local pdf ./quick.pdf 3")
testing.expect(t, shared.is_ok(qerr), "quick local parse should succeed")
testing.expect(t, qmatch, "quick local parse should match")
testing.expect(t, qfmt == .PDF, "quick local format should parse")
testing.expect(t, qpath == "./quick.pdf", "quick local path should parse")
testing.expect(t, qpages == 3, "quick local pages should parse")
proj, qafmt, qaout, qapages, qamatch, qaerr := app.parse_quick_local_all_command("quick local all ./p.comic.json cbz ./q.cbz 4")
testing.expect(t, shared.is_ok(qaerr), "quick local all parse should succeed")
testing.expect(t, qamatch, "quick local all parse should match")
testing.expect(t, proj == "./p.comic.json", "quick local all project path should parse")
testing.expect(t, qafmt == .CBZ, "quick local all format should parse")
testing.expect(t, qaout == "./q.cbz", "quick local all export path should parse")
testing.expect(t, qapages == 4, "quick local all pages should parse")
aafmt, aapath, aamatch, aaerr := app.parse_auto_all_command("auto all pdf ./auto.pdf")
testing.expect(t, shared.is_ok(aaerr), "auto all parse should succeed")
testing.expect(t, aamatch, "auto all parse should match")
testing.expect(t, aafmt == .PDF, "auto all format should parse")
testing.expect(t, aapath == "./auto.pdf", "auto all path should parse")
aalfmt, aalpath, aalpages, aalmatch, aalerr := app.parse_auto_all_local_command("auto all local cbz ./auto.cbz 3")
testing.expect(t, shared.is_ok(aalerr), "auto all local parse should succeed")
testing.expect(t, aalmatch, "auto all local parse should match")
testing.expect(t, aalfmt == .CBZ, "auto all local format should parse")
testing.expect(t, aalpath == "./auto.cbz", "auto all local path should parse")
testing.expect(t, aalpages == 3, "auto all local pages should parse")
}
@test
cli_save_and_load_roundtrip :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-cli-*", context.temp_allocator)
if terr != nil {
testing.expect(t, false, "failed to create temp dir")
return
}
defer os.remove_all(tmp_dir)
path := fmt.aprintf("%s/project.comic.json", tmp_dir)
defer delete(path)
state := core.new_initial_state()
state.story_idea = "cli story"
save_out, save_err := app.run_cli_command(app.Parsed_CLI_Command{kind = .Save, path = path}, &state)
testing.expect(t, shared.is_ok(save_err), "save command should succeed")
testing.expect(t, strings.contains(save_out, "Saved project"), "save output should mention save")
delete(save_out)
state.story_idea = "changed"
load_out, load_err := app.run_cli_command(app.Parsed_CLI_Command{kind = .Load, path = path}, &state)
testing.expect(t, shared.is_ok(load_err), "load command should succeed")
testing.expect(t, strings.contains(load_out, "Loaded project"), "load output should mention load")
testing.expect(t, state.story_idea == "cli story", "load should restore story")
delete(load_out)
core.dispose_state_owned(&state)
}
@test
cli_tui_generate_script_requires_key :: proc(t: ^testing.T) {
prev := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
defer {
if len(prev) > 0 {
_ = os.set_env("DEEPSEEK_API_KEY", prev)
} else {
_ = os.unset_env("DEEPSEEK_API_KEY")
}
}
_ = os.unset_env("DEEPSEEK_API_KEY")
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out, err := app.run_tui_command(&controller, "generate script 6", &last_job)
testing.expect(t, !shared.is_ok(err), "generate script should fail without configured key")
testing.expect(t, len(out) == 0, "error path should not return output")
}
@test
cli_tui_generate_panels_page_requires_script :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out, err := app.run_tui_command(&controller, "generate panels page 2", &last_job)
testing.expect(t, !shared.is_ok(err), "generate panels page should fail with empty script")
testing.expect(t, len(out) == 0, "error path should not return output")
}
@test
cli_tui_generate_script_local_succeeds_without_key :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out, err := app.run_tui_command(&controller, "generate script local 2", &last_job)
testing.expect(t, shared.is_ok(err), "generate script local should succeed without key")
testing.expect(t, strings.contains(out, "local script generated"), "local generation should return success message")
delete(out)
testing.expect(t, len(controller.state.script.pages) == 2, "local script should create requested pages")
}
@test
cli_tui_layout_and_export_require_data :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out1, err1 := app.run_tui_command(&controller, "layout auto", &last_job)
testing.expect(t, !shared.is_ok(err1), "layout auto should fail without script")
testing.expect(t, len(out1) == 0, "layout error path should not return output")
_, out2, err2 := app.run_tui_command(&controller, "export pdf ./tmp.pdf", &last_job)
testing.expect(t, !shared.is_ok(err2), "export should fail without layouts")
testing.expect(t, len(out2) == 0, "export error path should not return output")
}
@test
cli_tui_local_panels_and_export_pdf :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-cli-local-*", context.temp_allocator)
if terr != nil {
testing.expect(t, false, "failed to create temp dir")
return
}
defer os.remove_all(tmp_dir)
out_pdf := fmt.aprintf("%s/local.pdf", tmp_dir)
defer delete(out_pdf)
state := core.new_initial_state()
controller := ui.new_controller(state)
defer {
for _, img in controller.state.panel_images {
delete(img.url)
delete(img.prompt)
}
delete(controller.state.panel_images)
controller.state.panel_images = nil
ui.dispose_controller(&controller)
}
last_job := 0
_, out1, err1 := app.run_tui_command(&controller, "generate script local 2", &last_job)
testing.expect(t, shared.is_ok(err1), "local script should succeed")
delete(out1)
_, out2, err2 := app.run_tui_command(&controller, "generate panels local", &last_job)
testing.expect(t, shared.is_ok(err2), "local panels should succeed")
delete(out2)
_, out3, err3 := app.run_tui_command(&controller, "layout auto", &last_job)
testing.expect(t, shared.is_ok(err3), "layout auto should succeed")
delete(out3)
export_cmd := fmt.aprintf("export pdf %s", out_pdf)
defer delete(export_cmd)
_, out4, err4 := app.run_tui_command(&controller, export_cmd, &last_job)
testing.expect(t, shared.is_ok(err4), "export should succeed")
delete(out4)
testing.expect(t, os.exists(out_pdf), "exported pdf should exist")
}
@test
cli_tui_quick_local_export_pdf :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-cli-quick-*", context.temp_allocator)
if terr != nil {
testing.expect(t, false, "failed to create temp dir")
return
}
defer os.remove_all(tmp_dir)
out_pdf := fmt.aprintf("%s/quick.pdf", tmp_dir)
defer delete(out_pdf)
state := core.new_initial_state()
controller := ui.new_controller(state)
defer {
for _, img in controller.state.panel_images {
delete(img.url)
delete(img.prompt)
}
delete(controller.state.panel_images)
controller.state.panel_images = nil
ui.dispose_controller(&controller)
}
cmd := fmt.aprintf("quick local pdf %s 3", out_pdf)
defer delete(cmd)
last_job := 0
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
testing.expect(t, shared.is_ok(err), "quick local should succeed")
testing.expect(t, strings.contains(out, "quick local exported"), "quick local should return success output")
delete(out)
testing.expect(t, len(controller.state.script.pages) == 3, "quick local should build requested page count")
testing.expect(t, os.exists(out_pdf), "quick-local exported pdf should exist")
}
@test
cli_tui_quick_local_all_saves_and_exports :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-cli-quick-all-*", context.temp_allocator)
if terr != nil {
testing.expect(t, false, "failed to create temp dir")
return
}
defer os.remove_all(tmp_dir)
project_path := fmt.aprintf("%s/project.comic.json", tmp_dir)
export_path := fmt.aprintf("%s/quick.cbz", tmp_dir)
defer delete(project_path)
defer delete(export_path)
state := core.new_initial_state()
controller := ui.new_controller(state)
defer {
for _, img in controller.state.panel_images {
delete(img.url)
delete(img.prompt)
}
delete(controller.state.panel_images)
controller.state.panel_images = nil
ui.dispose_controller(&controller)
}
cmd := fmt.aprintf("quick local all %s cbz %s 2", project_path, export_path)
defer delete(cmd)
last_job := 0
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
testing.expect(t, shared.is_ok(err), "quick local all should succeed")
testing.expect(t, strings.contains(out, "quick local all saved"), "quick local all should return success output")
delete(out)
testing.expect(t, os.exists(project_path), "quick local all should save project")
testing.expect(t, os.exists(export_path), "quick local all should export artifact")
}
@test
cli_tui_doctor_reports_status :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out, err := app.run_tui_command(&controller, "doctor", &last_job)
testing.expect(t, shared.is_ok(err), "doctor should succeed")
testing.expect(t, strings.contains(out, "Doctor"), "doctor output should include header")
testing.expect(t, strings.contains(out, "deepseek key:"), "doctor output should include deepseek key status")
testing.expect(t, strings.contains(out, "curl:"), "doctor output should include curl status")
delete(out)
}
@test
cli_tui_ready_reports_status :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out, err := app.run_tui_command(&controller, "ready", &last_job)
testing.expect(t, shared.is_ok(err), "ready should succeed")
testing.expect(t, strings.contains(out, "Ready"), "ready output should include header")
testing.expect(t, strings.contains(out, "script generated:"), "ready output should include script status")
testing.expect(t, strings.contains(out, "export ready:"), "ready output should include export status")
delete(out)
}
@test
cli_tui_next_recommends_action :: proc(t: ^testing.T) {
prev_deep := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
prev_fal := os.get_env("FAL_API_KEY", context.temp_allocator)
defer {
if len(prev_deep) > 0 { _ = os.set_env("DEEPSEEK_API_KEY", prev_deep) } else { _ = os.unset_env("DEEPSEEK_API_KEY") }
if len(prev_fal) > 0 { _ = os.set_env("FAL_API_KEY", prev_fal) } else { _ = os.unset_env("FAL_API_KEY") }
}
_ = os.unset_env("DEEPSEEK_API_KEY")
_ = os.unset_env("FAL_API_KEY")
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out1, err1 := app.run_tui_command(&controller, "next", &last_job)
testing.expect(t, shared.is_ok(err1), "next should succeed")
testing.expect(t, strings.contains(out1, "generate script local"), "next should recommend local script generation")
delete(out1)
_, out2, err2 := app.run_tui_command(&controller, "generate script local 1", &last_job)
testing.expect(t, shared.is_ok(err2), "generate script local should succeed")
delete(out2)
_, out3, err3 := app.run_tui_command(&controller, "next", &last_job)
testing.expect(t, shared.is_ok(err3), "next should succeed after script")
testing.expect(t, strings.contains(out3, "generate panels local"), "next should recommend local panel generation")
delete(out3)
}
@test
cli_tui_plan_reports_progress :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out1, err1 := app.run_tui_command(&controller, "plan", &last_job)
testing.expect(t, shared.is_ok(err1), "plan should succeed")
testing.expect(t, strings.contains(out1, "Plan"), "plan output should include header")
testing.expect(t, strings.contains(out1, "1) Script"), "plan output should include script step")
delete(out1)
_, out2, err2 := app.run_tui_command(&controller, "generate script local 1", &last_job)
testing.expect(t, shared.is_ok(err2), "local script should succeed")
delete(out2)
_, out3, err3 := app.run_tui_command(&controller, "plan", &last_job)
testing.expect(t, shared.is_ok(err3), "plan should succeed after script")
testing.expect(t, strings.contains(out3, "next:"), "plan output should include next hint")
delete(out3)
}
@test
cli_tui_auto_runs_next_step :: proc(t: ^testing.T) {
prev_deep := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
prev_fal := os.get_env("FAL_API_KEY", context.temp_allocator)
defer {
if len(prev_deep) > 0 { _ = os.set_env("DEEPSEEK_API_KEY", prev_deep) } else { _ = os.unset_env("DEEPSEEK_API_KEY") }
if len(prev_fal) > 0 { _ = os.set_env("FAL_API_KEY", prev_fal) } else { _ = os.unset_env("FAL_API_KEY") }
}
_ = os.unset_env("DEEPSEEK_API_KEY")
_ = os.unset_env("FAL_API_KEY")
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
last_job := 0
_, out, err := app.run_tui_command(&controller, "auto", &last_job)
testing.expect(t, shared.is_ok(err), "auto should succeed")
testing.expect(t, strings.contains(out, "auto ran:"), "auto output should include executed command")
delete(out)
testing.expect(t, len(controller.state.script.pages) > 0, "auto should progress by generating script")
}
@test
cli_tui_auto_all_runs_to_export :: proc(t: ^testing.T) {
prev_deep := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
prev_fal := os.get_env("FAL_API_KEY", context.temp_allocator)
defer {
if len(prev_deep) > 0 { _ = os.set_env("DEEPSEEK_API_KEY", prev_deep) } else { _ = os.unset_env("DEEPSEEK_API_KEY") }
if len(prev_fal) > 0 { _ = os.set_env("FAL_API_KEY", prev_fal) } else { _ = os.unset_env("FAL_API_KEY") }
}
_ = os.unset_env("DEEPSEEK_API_KEY")
_ = os.unset_env("FAL_API_KEY")
tmp_dir, terr := os.make_directory_temp("", "comic-cli-auto-all-*", context.temp_allocator)
if terr != nil {
testing.expect(t, false, "failed to create temp dir")
return
}
defer os.remove_all(tmp_dir)
out_pdf := fmt.aprintf("%s/auto_all.pdf", tmp_dir)
defer delete(out_pdf)
state := core.new_initial_state()
controller := ui.new_controller(state)
defer {
for _, img in controller.state.panel_images {
delete(img.url)
delete(img.prompt)
}
delete(controller.state.panel_images)
controller.state.panel_images = nil
ui.dispose_controller(&controller)
}
last_job := 0
cmd := fmt.aprintf("auto all pdf %s", out_pdf)
defer delete(cmd)
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
testing.expect(t, shared.is_ok(err), "auto all should succeed")
testing.expect(t, strings.contains(out, "auto all exported"), "auto all output should include success")
delete(out)
testing.expect(t, os.exists(out_pdf), "auto all should produce export file")
}
@test
cli_tui_auto_all_local_runs_to_export :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-cli-auto-all-local-*", context.temp_allocator)
if terr != nil {
testing.expect(t, false, "failed to create temp dir")
return
}
defer os.remove_all(tmp_dir)
out_cbz := fmt.aprintf("%s/auto_all_local.cbz", tmp_dir)
defer delete(out_cbz)
state := core.new_initial_state()
controller := ui.new_controller(state)
defer {
for _, img in controller.state.panel_images {
delete(img.url)
delete(img.prompt)
}
delete(controller.state.panel_images)
controller.state.panel_images = nil
ui.dispose_controller(&controller)
}
last_job := 0
cmd := fmt.aprintf("auto all local cbz %s 2", out_cbz)
defer delete(cmd)
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
testing.expect(t, shared.is_ok(err), "auto all local should succeed")
testing.expect(t, strings.contains(out, "auto all local exported"), "auto all local output should include success")
delete(out)
testing.expect(t, os.exists(out_cbz), "auto all local should produce export file")
}
@test
cli_tui_open_and_saveas_aliases :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-cli-alias-*", context.temp_allocator)
if terr != nil {
testing.expect(t, false, "failed to create temp dir")
return
}
defer os.remove_all(tmp_dir)
path := fmt.aprintf("%s/alias.comic.json", tmp_dir)
defer delete(path)
state := core.new_initial_state()
controller := ui.new_controller(state)
controller.state.story_idea = "alias story"
last_job := 0
save_cmd := fmt.aprintf("saveas %s", path)
defer delete(save_cmd)
_, out1, err1 := app.run_tui_command(&controller, save_cmd, &last_job)
testing.expect(t, shared.is_ok(err1), "saveas should succeed")
delete(out1)
controller.state.story_idea = "changed"
open_cmd := fmt.aprintf("open %s", path)
defer delete(open_cmd)
_, out2, err2 := app.run_tui_command(&controller, open_cmd, &last_job)
testing.expect(t, shared.is_ok(err2), "open should succeed")
delete(out2)
testing.expect(t, controller.state.story_idea == "alias story", "open should restore saved state")
if shared.is_ok(err2) {
ui.dispose_controller_owned(&controller)
} else {
ui.dispose_controller(&controller)
}
}