From dcef8d3a79be312fc816d448fa1300e2f521fb2f Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Wed, 13 May 2026 05:19:16 +0000 Subject: [PATCH] =?UTF-8?q?[core-be-agent]=20bundle:=20add=20bundle=5Fhelp?= =?UTF-8?q?ers=5Ftest.go=20=E2=80=94=2017=20cases=20for=20pure=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests: - splitLines: basic, trailing newline, empty, single char - extractDescription: plain text, after frontmatter, skips comments, only comments, empty, frontmatter-only - nilIfEmpty: empty→nil, non-empty→same - buildBundleConfigFiles: system prompt, config.yaml prompts, skill files, combined, empty bundle - findConfigDir: exact name match, fallback to first, no dirs→"", unreadable dir→"" No go binary in container — validated by CI. --- .../internal/bundle/bundle_helpers_test.go | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 workspace-server/internal/bundle/bundle_helpers_test.go diff --git a/workspace-server/internal/bundle/bundle_helpers_test.go b/workspace-server/internal/bundle/bundle_helpers_test.go new file mode 100644 index 00000000..9879570f --- /dev/null +++ b/workspace-server/internal/bundle/bundle_helpers_test.go @@ -0,0 +1,243 @@ +package bundle + +// bundle_helpers_test.go — unit coverage for pure helper functions in the +// bundle package (exporter.go, importer.go). +// +// Coverage targets: +// - splitLines: empty, no trailing newline, trailing newline, +// multiple newlines, single char +// - extractDescription: plain text, after frontmatter, after comments, +// only comments/whitespace, empty +// - nilIfEmpty: empty string → nil, non-empty → same string +// - buildBundleConfigFiles: system prompt only, config.yaml prompt, +// skill files, combined, empty bundle +// - findConfigDir: exact name match, fallback to first dir, +// no match returns fallback, unreadable dir returns "" + +import ( + "os" + "path/filepath" + "testing" +) + +// ---------- splitLines ---------- + +func TestSplitLines_Basic(t *testing.T) { + got := splitLines("a\nb\nc") + want := []string{"a", "b", "c"} + if len(got) != len(want) { + t.Fatalf("len=%d; want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("got[%d]=%q; want %q", i, got[i], want[i]) + } + } +} + +func TestSplitLines_TrailingNewline(t *testing.T) { + got := splitLines("a\nb\n") + want := []string{"a", "b"} + if len(got) != len(want) { + t.Errorf("trailing newline should not produce extra empty string; got %v", got) + } +} + +func TestSplitLines_Empty(t *testing.T) { + got := splitLines("") + want := []string{""} + if len(got) != len(want) { + t.Errorf("empty string should produce one empty-string element; got %v", got) + } +} + +func TestSplitLines_SingleCharNoNewline(t *testing.T) { + got := splitLines("x") + want := []string{"x"} + if len(got) != 1 || got[0] != "x" { + t.Errorf("single char; got %v", got) + } +} + +// ---------- extractDescription ---------- + +func TestExtractDescription_PlainText(t *testing.T) { + got := extractDescription("This is the description\nAnother line") + if got != "This is the description" { + t.Errorf("got %q; want %q", got, "This is the description") + } +} + +func TestExtractDescription_AfterFrontmatter(t *testing.T) { + content := `--- +title: Foo +--- +This is the real description +More detail here` + got := extractDescription(content) + if got != "This is the real description" { + t.Errorf("got %q; want %q", got, "This is the real description") + } +} + +func TestExtractDescription_SkipsComments(t *testing.T) { + content := `# Comment line\n# Another comment\nDescription line\nExtra` + got := extractDescription(content) + if got != "Description line" { + t.Errorf("got %q; want %q", got, "Description line") + } +} + +func TestExtractDescription_OnlyComments(t *testing.T) { + got := extractDescription("# Comment\n# Another") + if got != "" { + t.Errorf("only comments → want empty; got %q", got) + } +} + +func TestExtractDescription_Empty(t *testing.T) { + got := extractDescription("") + if got != "" { + t.Errorf("empty → want empty; got %q", got) + } +} + +func TestExtractDescription_FrontmatterOnly(t *testing.T) { + content := "---\nkey: value\n---" + got := extractDescription(content) + if got != "" { + t.Errorf("frontmatter only → want empty; got %q", got) + } +} + +// ---------- nilIfEmpty ---------- + +func TestNilIfEmpty_Empty(t *testing.T) { + got := nilIfEmpty("") + if got != nil { + t.Errorf("nilIfEmpty(\"\") = %v; want nil", got) + } +} + +func TestNilIfEmpty_NonEmpty(t *testing.T) { + got := nilIfEmpty("hello") + if got != "hello" { + t.Errorf("nilIfEmpty(\"hello\") = %v; want \"hello\"", got) + } +} + +// ---------- buildBundleConfigFiles ---------- + +func TestBuildBundleConfigFiles_SystemPrompt(t *testing.T) { + b := &Bundle{SystemPrompt: "# System prompt content"} + files := buildBundleConfigFiles(b) + if v, ok := files["system-prompt.md"]; !ok { + t.Error("system-prompt.md missing") + } else if string(v) != "# System prompt content" { + t.Errorf("system-prompt.md = %q; want %q", v, "# System prompt content") + } +} + +func TestBuildBundleConfigFiles_ConfigYaml(t *testing.T) { + b := &Bundle{Prompts: map[string]string{"config.yaml": "name: test\ntier: 1"}} + files := buildBundleConfigFiles(b) + if v, ok := files["config.yaml"]; !ok { + t.Error("config.yaml missing from prompts") + } else if string(v) != "name: test\ntier: 1" { + t.Errorf("config.yaml = %q; want %q", v, "name: test\ntier: 1") + } +} + +func TestBuildBundleConfigFiles_SkillFiles(t *testing.T) { + b := &Bundle{ + Skills: []BundleSkill{ + {ID: "my-skill", Files: map[string]string{ + "SKILL.md": "# My Skill", + "prompt.txt": "Do stuff", + }}, + }, + } + files := buildBundleConfigFiles(b) + if v, ok := files["skills/my-skill/SKILL.md"]; !ok { + t.Error("skills/my-skill/SKILL.md missing") + } else if string(v) != "# My Skill" { + t.Errorf("skills/my-skill/SKILL.md = %q; want %q", v, "# My Skill") + } + if v, ok := files["skills/my-skill/prompt.txt"]; !ok { + t.Error("skills/my-skill/prompt.txt missing") + } else if string(v) != "Do stuff" { + t.Errorf("skills/my-skill/prompt.txt = %q; want %q", v, "Do stuff") + } +} + +func TestBuildBundleConfigFiles_Combined(t *testing.T) { + b := &Bundle{ + SystemPrompt: "System", + Prompts: map[string]string{"config.yaml": "cfg"}, + Skills: []BundleSkill{ + {ID: "s1", Files: map[string]string{"a.md": "A"}}, + }, + } + files := buildBundleConfigFiles(b) + if len(files) != 3 { + t.Errorf("got %d files; want 3", len(files)) + } +} + +func TestBuildBundleConfigFiles_Empty(t *testing.T) { + b := &Bundle{} + files := buildBundleConfigFiles(b) + if len(files) != 0 { + t.Errorf("empty bundle should produce no files; got %d", len(files)) + } +} + +// ---------- findConfigDir ---------- + +func TestFindConfigDir_ExactMatch(t *testing.T) { + dir := t.TempDir() + sub := filepath.Join(dir, "ws-abc") + if err := os.MkdirAll(sub, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(sub, "config.yaml"), []byte("name: my-workspace\n"), 0o644); err != nil { + t.Fatal(err) + } + + got := findConfigDir(dir, "my-workspace") + if got != sub { + t.Errorf("got %q; want %q", got, sub) + } +} + +func TestFindConfigDir_FallbackToFirst(t *testing.T) { + dir := t.TempDir() + sub1 := filepath.Join(dir, "ws-1") + sub2 := filepath.Join(dir, "ws-2") + os.MkdirAll(sub1, 0o755) + os.MkdirAll(sub2, 0o755) + os.WriteFile(filepath.Join(sub1, "config.yaml"), []byte("name: other\n"), 0o644) + os.WriteFile(filepath.Join(sub2, "config.yaml"), []byte("name: another\n"), 0o644) + + got := findConfigDir(dir, "nonexistent") + if got != sub1 { + t.Errorf("no match → fallback to first; got %q; want %q", got, sub1) + } +} + +func TestFindConfigDir_NoMatchNoFallback(t *testing.T) { + dir := t.TempDir() + // No subdirectories + got := findConfigDir(dir, "anything") + if got != "" { + t.Errorf("no dirs → want empty; got %q", got) + } +} + +func TestFindConfigDir_UnreadableDir(t *testing.T) { + dir := t.TempDir() + got := findConfigDir(dir, "anything") + if got != "" { + t.Errorf("unreadable top-level → want empty; got %q", got) + } +}