From 540309f5e0c479c5b992eb8094eba99bbd4ff83e Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 18:38:39 +0000 Subject: [PATCH] test(lib): add AX-7 coverage for workspace helpers Co-Authored-By: Virgil --- docs/RFC.md | 1 + pkg/lib/lib_example_test.go | 6 + pkg/lib/lib_test.go | 297 ++++++++++++++++++++++++++++++++++++ 3 files changed, 304 insertions(+) diff --git a/docs/RFC.md b/docs/RFC.md index ca98c4a..373eba1 100644 --- a/docs/RFC.md +++ b/docs/RFC.md @@ -423,6 +423,7 @@ Every exported function MUST have a usage-example comment: ## Changelog +- 2026-03-30: `pkg/lib.WorkspaceFile` now has direct Good/Bad/Ugly coverage and an example companion, closing the last workspace-template helper gap in `pkg/lib`. - 2026-03-30: `version.go` now has an example companion, closing the last build-relevant source file without example coverage. - 2026-03-30: `pkg/agentic/commands_workspace.go` now has a matching example companion, closing the last agentic source file without example coverage. - 2026-03-30: plan files and review queue rate-limit state now use `WriteAtomic`, keeping JSON state writes aligned with the AX safe-write convention. diff --git a/pkg/lib/lib_example_test.go b/pkg/lib/lib_example_test.go index 8e32fe3..c11cb61 100644 --- a/pkg/lib/lib_example_test.go +++ b/pkg/lib/lib_example_test.go @@ -60,6 +60,12 @@ func ExampleTemplate() { // Output: true } +func ExampleWorkspaceFile() { + r := WorkspaceFile("default", "CODEX.md.tmpl") + core.Println(r.OK) + // Output: true +} + func ExampleMountData() { c := core.New() MountData(c) diff --git a/pkg/lib/lib_test.go b/pkg/lib/lib_test.go index 8f637b8..81eaa13 100644 --- a/pkg/lib/lib_test.go +++ b/pkg/lib/lib_test.go @@ -3,7 +3,9 @@ package lib import ( + "embed" "runtime" + "sync" "testing" core "dappco.re/go/core" @@ -11,6 +13,46 @@ import ( var testFs = (&core.Fs{}).NewUnrestricted() +func breakLibMountForTest(t *testing.T) { + t.Helper() + + originalPromptFiles := promptFiles + promptFiles = embed.FS{} + mountOnce = sync.Once{} + mountResult = core.Result{} + data = nil + promptFS = nil + taskFS = nil + flowFS = nil + personaFS = nil + workspaceFS = nil + + t.Cleanup(func() { + promptFiles = originalPromptFiles + mountOnce = sync.Once{} + mountResult = core.Result{} + data = nil + promptFS = nil + taskFS = nil + flowFS = nil + personaFS = nil + workspaceFS = nil + }) +} + +func corruptLibMountForTest(t *testing.T) { + t.Helper() + + MountData(core.New()) + data = nil + + t.Cleanup(func() { + mountOnce = sync.Once{} + mountResult = core.Result{} + data = nil + }) +} + // --- Prompt --- func TestLib_Prompt_Good(t *testing.T) { @@ -30,6 +72,13 @@ func TestLib_Prompt_Bad(t *testing.T) { } } +func TestLib_Prompt_Ugly(t *testing.T) { + r := Prompt("../coding") + if r.OK { + t.Error("Prompt('../coding') should return !OK") + } +} + // --- Task --- func TestLib_Task_Good(t *testing.T) { @@ -59,6 +108,13 @@ func TestLib_Task_Bad(t *testing.T) { } } +func TestLib_Task_Ugly(t *testing.T) { + r := Task("../bug-fix") + if r.OK { + t.Error("Task('../bug-fix') should return !OK") + } +} + // --- TaskBundle --- func TestLib_TaskBundle_Good(t *testing.T) { @@ -82,6 +138,13 @@ func TestLib_TaskBundle_Bad(t *testing.T) { } } +func TestLib_TaskBundle_Ugly(t *testing.T) { + r := TaskBundle("../code/review") + if r.OK { + t.Error("TaskBundle('../code/review') should return !OK") + } +} + // --- Flow --- func TestLib_Flow_Good(t *testing.T) { @@ -94,6 +157,20 @@ func TestLib_Flow_Good(t *testing.T) { } } +func TestLib_Flow_Bad(t *testing.T) { + r := Flow("nonexistent-flow") + if r.OK { + t.Error("Flow('nonexistent-flow') should return !OK") + } +} + +func TestLib_Flow_Ugly(t *testing.T) { + r := Flow("../go") + if r.OK { + t.Error("Flow('../go') should return !OK") + } +} + // --- Persona --- func TestLib_Persona_Good(t *testing.T) { @@ -110,6 +187,20 @@ func TestLib_Persona_Good(t *testing.T) { } } +func TestLib_Persona_Bad(t *testing.T) { + r := Persona("nonexistent-persona") + if r.OK { + t.Error("Persona('nonexistent-persona') should return !OK") + } +} + +func TestLib_Persona_Ugly(t *testing.T) { + r := Persona("../secops/developer") + if r.OK { + t.Error("Persona('../secops/developer') should return !OK") + } +} + // --- Template --- func TestLib_Template_Good(t *testing.T) { @@ -136,6 +227,80 @@ func TestLib_Template_Bad(t *testing.T) { } } +func TestLib_Template_Ugly(t *testing.T) { + r := Template("../coding") + if r.OK { + t.Error("Template('../coding') should return !OK") + } +} + +// --- WorkspaceFile --- + +func TestLib_WorkspaceFile_Good(t *testing.T) { + r := WorkspaceFile("default", "CODEX.md.tmpl") + if !r.OK { + t.Fatal("WorkspaceFile('default', 'CODEX.md.tmpl') returned !OK") + } + if r.Value.(string) == "" { + t.Error("WorkspaceFile('default', 'CODEX.md.tmpl') returned empty string") + } +} + +func TestLib_WorkspaceFile_Bad(t *testing.T) { + r := WorkspaceFile("missing-template", "CODEX.md.tmpl") + if r.OK { + t.Error("WorkspaceFile('missing-template', 'CODEX.md.tmpl') should return !OK") + } +} + +func TestLib_WorkspaceFile_Ugly(t *testing.T) { + r := WorkspaceFile("default", "../CODEX.md.tmpl") + if r.OK { + t.Error("WorkspaceFile('default', '../CODEX.md.tmpl') should return !OK") + } +} + +// --- MountData --- + +func TestLib_MountData_Good(t *testing.T) { + c := core.New() + MountData(c) + + r := c.Data().ReadString("prompts/coding.md") + if !r.OK { + t.Fatal("MountData() did not register prompt data") + } +} + +func TestLib_MountData_Bad(t *testing.T) { + breakLibMountForTest(t) + + c := core.New() + MountData(c) + + r := c.Data().ReadString("prompts/coding.md") + if r.OK { + t.Error("MountData() should not register prompt data when mounting fails") + } +} + +func TestLib_MountData_Ugly(t *testing.T) { + corruptLibMountForTest(t) + + panicked := false + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + MountData(nil) + }() + if !panicked { + t.Fatal("MountData(nil) should panic") + } +} + // --- List Functions --- func TestLib_ListPrompts_Good(t *testing.T) { @@ -145,6 +310,31 @@ func TestLib_ListPrompts_Good(t *testing.T) { } } +func TestLib_ListPrompts_Bad(t *testing.T) { + breakLibMountForTest(t) + + if prompts := ListPrompts(); prompts != nil { + t.Error("ListPrompts() should return nil when mounting fails") + } +} + +func TestLib_ListPrompts_Ugly(t *testing.T) { + corruptLibMountForTest(t) + + panicked := false + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = ListPrompts() + }() + if !panicked { + t.Fatal("ListPrompts() should panic when embedded state is corrupted") + } +} + func TestLib_ListTasks_Good(t *testing.T) { tasks := ListTasks() if len(tasks) == 0 { @@ -162,6 +352,31 @@ func TestLib_ListTasks_Good(t *testing.T) { } } +func TestLib_ListTasks_Bad(t *testing.T) { + breakLibMountForTest(t) + + if tasks := ListTasks(); tasks != nil { + t.Error("ListTasks() should return nil when mounting fails") + } +} + +func TestLib_ListTasks_Ugly(t *testing.T) { + corruptLibMountForTest(t) + + panicked := false + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = ListTasks() + }() + if !panicked { + t.Fatal("ListTasks() should panic when embedded state is corrupted") + } +} + func TestLib_ListPersonas_Good(t *testing.T) { personas := ListPersonas() if len(personas) == 0 { @@ -179,6 +394,31 @@ func TestLib_ListPersonas_Good(t *testing.T) { } } +func TestLib_ListPersonas_Bad(t *testing.T) { + breakLibMountForTest(t) + + if personas := ListPersonas(); personas != nil { + t.Error("ListPersonas() should return nil when mounting fails") + } +} + +func TestLib_ListPersonas_Ugly(t *testing.T) { + corruptLibMountForTest(t) + + panicked := false + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = ListPersonas() + }() + if !panicked { + t.Fatal("ListPersonas() should panic when embedded state is corrupted") + } +} + func TestLib_ListFlows_Good(t *testing.T) { flows := ListFlows() if len(flows) == 0 { @@ -186,6 +426,31 @@ func TestLib_ListFlows_Good(t *testing.T) { } } +func TestLib_ListFlows_Bad(t *testing.T) { + breakLibMountForTest(t) + + if flows := ListFlows(); flows != nil { + t.Error("ListFlows() should return nil when mounting fails") + } +} + +func TestLib_ListFlows_Ugly(t *testing.T) { + corruptLibMountForTest(t) + + panicked := false + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = ListFlows() + }() + if !panicked { + t.Fatal("ListFlows() should panic when embedded state is corrupted") + } +} + func TestLib_ListWorkspaces_Good(t *testing.T) { workspaces := ListWorkspaces() if len(workspaces) == 0 { @@ -193,6 +458,31 @@ func TestLib_ListWorkspaces_Good(t *testing.T) { } } +func TestLib_ListWorkspaces_Bad(t *testing.T) { + breakLibMountForTest(t) + + if workspaces := ListWorkspaces(); workspaces != nil { + t.Error("ListWorkspaces() should return nil when mounting fails") + } +} + +func TestLib_ListWorkspaces_Ugly(t *testing.T) { + corruptLibMountForTest(t) + + panicked := false + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = ListWorkspaces() + }() + if !panicked { + t.Fatal("ListWorkspaces() should panic when embedded state is corrupted") + } +} + // --- ExtractWorkspace --- func TestLib_ExtractWorkspace_Good(t *testing.T) { @@ -266,6 +556,13 @@ func TestLib_ExtractWorkspace_Bad(t *testing.T) { } } +func TestLib_ExtractWorkspace_Ugly(t *testing.T) { + err := ExtractWorkspace("default", t.TempDir(), nil) + if err == nil { + t.Fatal("ExtractWorkspace should fail when template data is nil") + } +} + func TestLib_ExtractWorkspace_Good_AXConventions(t *testing.T) { dir := t.TempDir() data := &WorkspaceData{Repo: "test-repo", Task: "align AX docs"}