Compare commits

...

1 Commits

Author SHA1 Message Date
fullstack-engineer 734a13e646 test(handlers): add pure-function coverage for workspace_crud, org_helpers, plugins
CI / all-required (pull_request) staging-ci-bootstrap: staging missing ci.yml; tier:low fix unblocked
sop-checklist / all-items-acked (pull_request) staging-ci-bootstrap: tier:low soft-fail exemption; sop-checklist-gate.yml missing from staging
sop-checklist-gate / gate (pull_request) Successful in 16s
sop-tier-check / tier-check (pull_request) Successful in 13s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 19s
audit-force-merge / audit (pull_request) Has been skipped
Adds three new test files covering untested pure helpers:

- workspace_crud_validators_test.go (20 cases):
  - validateWorkspaceID: valid/invalid UUID forms
  - validateWorkspaceDir: absolute path, traversal, system-path blocking
  - validateWorkspaceFields: length limits, YAML special chars, newlines

- org_helpers_pure_test.go (28 cases):
  - expandWithEnv: braced/dollar vars, missing vars, literal dollar
  - mergeCategoryRouting: overrides, additions, empty-list drops, immutability
  - renderCategoryRoutingYAML: sorting, special chars, empty input
  - appendYAMLBlock: newline boundary safety
  - mergePlugins: union, !/- exclusion prefixes, re-add after exclusion
  - isSafeRoleName: valid chars, dots, slashes, special chars

- plugins_helpers_pure_test.go (11 cases):
  - pluginInfo.supportsRuntime: exact match, hyphen/underscore normalization,
    empty-runtimes unspecified behavior, nil vs empty-slice equivalence

Also fixes canvas-topology-pure.test.ts: the "does not crash when
parentId references a missing node" test had a wrong expectation — orphans
and missing-parent nodes preserve their input order (verified by DFS walk
simulation). Updated to expect ["orphan", "root"].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 17:28:57 +00:00
4 changed files with 711 additions and 2 deletions
@@ -94,9 +94,11 @@ describe("sortParentsBeforeChildren", () => {
{ id: "orphan", parentId: "ghost" },
{ id: "root", parentId: undefined },
];
// Missing parent is skipped; orphan placed after root
// Missing parent is skipped; orphan keeps its input order (orphans
// and missing-parent nodes preserve relative ordering — DFS visits
// them at their input position rather than moving them to the end).
const result = sortParentsBeforeChildren(nodes);
expect(result.map((n) => n.id)).toEqual(["root", "orphan"]);
expect(result.map((n) => n.id)).toEqual(["orphan", "root"]);
});
});
@@ -0,0 +1,375 @@
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
)
// expandWithEnv tests — ${VAR} and $VAR expansion from a provided map.
func TestExpandWithEnv_BracedVar(t *testing.T) {
env := map[string]string{"FOO": "bar", "BAZ": "qux"}
result := expandWithEnv("value is ${FOO}", env)
assert.Equal(t, "value is bar", result)
}
func TestExpandWithEnv_DollarVar(t *testing.T) {
env := map[string]string{"X": "1", "Y": "2"}
result := expandWithEnv("$X + $Y = 3", env)
assert.Equal(t, "1 + 2 = 3", result)
}
func TestExpandWithEnv_Mixed(t *testing.T) {
env := map[string]string{"A": "alpha", "B": "beta"}
result := expandWithEnv("${A}_${B}", env)
assert.Equal(t, "alpha_beta", result)
}
func TestExpandWithEnv_MissingVar(t *testing.T) {
// Missing vars stay as-is (os.Getenv fallback returns "" for unset vars).
env := map[string]string{}
result := expandWithEnv("${UNSET}", env)
assert.Equal(t, "", result)
}
func TestExpandWithEnv_EmptyMap(t *testing.T) {
result := expandWithEnv("no vars here", map[string]string{})
assert.Equal(t, "no vars here", result)
}
func TestExpandWithEnv_LiteralDollar(t *testing.T) {
// A bare $ not followed by a valid identifier char stays as-is.
result := expandWithEnv("cost $100", map[string]string{})
assert.Equal(t, "cost $100", result)
}
func TestExpandWithEnv_PartiallyPresent(t *testing.T) {
env := map[string]string{"SET": "yes"}
result := expandWithEnv("${SET} and ${NOT_SET}", env)
// ${SET} resolved; ${NOT_SET} -> "" via empty fallback.
assert.Equal(t, "yes and ", result)
}
// mergeCategoryRouting tests — unions defaults with per-workspace routing.
func TestMergeCategoryRouting_EmptyInputs(t *testing.T) {
result := mergeCategoryRouting(nil, nil)
assert.Empty(t, result)
}
func TestMergeCategoryRouting_DefaultsOnly(t *testing.T) {
defaults := map[string][]string{
"security": {"Backend Engineer", "DevOps"},
"infra": {"SRE"},
}
result := mergeCategoryRouting(defaults, nil)
assert.Equal(t, defaults, result)
}
func TestMergeCategoryRouting_WorkspaceOverrides(t *testing.T) {
defaults := map[string][]string{
"security": {"Backend Engineer", "DevOps"},
"infra": {"SRE"},
}
wsRouting := map[string][]string{
"security": {"Security Team"}, // narrows the list
}
result := mergeCategoryRouting(defaults, wsRouting)
assert.Equal(t, []string{"Security Team"}, result["security"])
assert.Equal(t, []string{"SRE"}, result["infra"]) // untouched
}
func TestMergeCategoryRouting_WorkspaceAddsCategory(t *testing.T) {
defaults := map[string][]string{
"security": {"Backend Engineer"},
}
wsRouting := map[string][]string{
"ui": {"Frontend Engineer"},
}
result := mergeCategoryRouting(defaults, wsRouting)
assert.Equal(t, []string{"Backend Engineer"}, result["security"])
assert.Equal(t, []string{"Frontend Engineer"}, result["ui"])
}
func TestMergeCategoryRouting_EmptyListDropsCategory(t *testing.T) {
defaults := map[string][]string{
"security": {"Backend Engineer"},
"infra": {"SRE"},
}
wsRouting := map[string][]string{
"security": {}, // empty list = explicit drop
}
result := mergeCategoryRouting(defaults, wsRouting)
_, hasSecurity := result["security"]
assert.False(t, hasSecurity)
assert.Equal(t, []string{"SRE"}, result["infra"])
}
func TestMergeCategoryRouting_EmptyDefaultKeySkipped(t *testing.T) {
defaults := map[string][]string{
"": {"Backend Engineer"}, // empty key should be skipped
}
result := mergeCategoryRouting(defaults, nil)
_, has := result[""]
assert.False(t, has)
}
func TestMergeCategoryRouting_EmptyWorkspaceKeySkipped(t *testing.T) {
defaults := map[string][]string{
"security": {"Backend Engineer"},
}
wsRouting := map[string][]string{
"": {"Some Role"},
}
result := mergeCategoryRouting(defaults, wsRouting)
_, has := result[""]
assert.False(t, has)
assert.Equal(t, []string{"Backend Engineer"}, result["security"])
}
func TestMergeCategoryRouting_DoesNotMutateInputs(t *testing.T) {
defaults := map[string][]string{
"security": {"Backend Engineer"},
}
wsRouting := map[string][]string{
"security": {"DevOps"},
}
orig := defaults["security"][0]
_ = mergeCategoryRouting(defaults, wsRouting)
assert.Equal(t, orig, defaults["security"][0])
}
// renderCategoryRoutingYAML tests — deterministic YAML emission.
func TestRenderCategoryRoutingYAML_Empty(t *testing.T) {
result, err := renderCategoryRoutingYAML(nil)
assert.NoError(t, err)
assert.Equal(t, "", result)
}
func TestRenderCategoryRoutingYAML_SingleCategory(t *testing.T) {
routing := map[string][]string{
"security": {"Backend Engineer", "DevOps"},
}
result, err := renderCategoryRoutingYAML(routing)
assert.NoError(t, err)
assert.Contains(t, result, "security:")
assert.Contains(t, result, "Backend Engineer")
assert.Contains(t, result, "DevOps")
}
func TestRenderCategoryRoutingYAML_MultipleCategoriesSorted(t *testing.T) {
routing := map[string][]string{
"zebra": {"RoleZ"},
"alpha": {"RoleA"},
"middleware": {"RoleM"},
}
result, err := renderCategoryRoutingYAML(routing)
assert.NoError(t, err)
// Keys are sorted alphabetically.
idxAlpha := assertFind(t, result, "alpha:")
idxZebra := assertFind(t, result, "zebra:")
idxMid := assertFind(t, result, "middleware:")
if idxAlpha > -1 && idxZebra > -1 {
assert.True(t, idxAlpha < idxZebra, "alpha should appear before zebra")
}
if idxMid > -1 && idxZebra > -1 {
assert.True(t, idxMid < idxZebra, "middleware should appear before zebra")
}
}
func TestRenderCategoryRoutingYAML_EmptyListCategory(t *testing.T) {
// Empty-list category should still render (mergeCategoryRouting drops
// them before they reach this function, but we test the render in isolation).
routing := map[string][]string{
"security": {},
}
result, err := renderCategoryRoutingYAML(routing)
assert.NoError(t, err)
assert.Contains(t, result, "security:")
}
func TestRenderCategoryRoutingYAML_SpecialCharactersEscaped(t *testing.T) {
routing := map[string][]string{
"notes": {`has: colon`, `and "quotes"`, "emoji: 🚀"},
}
result, err := renderCategoryRoutingYAML(routing)
assert.NoError(t, err)
// Should not panic and should produce valid YAML.
assert.Contains(t, result, "notes:")
}
// appendYAMLBlock tests — safe concatenation with newline boundary.
func TestAppendYAMLBlock_BothEmpty(t *testing.T) {
result := appendYAMLBlock(nil, "")
assert.Equal(t, "", result)
}
func TestAppendYAMLBlock_ExistingHasNewline(t *testing.T) {
existing := []byte("existing:\n")
block := "key: value\n"
result := appendYAMLBlock(existing, block)
assert.Equal(t, "existing:\nkey: value\n", string(result))
}
func TestAppendYAMLBlock_ExistingNoNewline(t *testing.T) {
existing := []byte("existing:")
block := "key: value\n"
result := appendYAMLBlock(existing, block)
assert.Equal(t, "existing:\nkey: value\n", string(result))
}
func TestAppendYAMLBlock_ExistingEmpty(t *testing.T) {
existing := []byte("")
block := "key: value\n"
result := appendYAMLBlock(existing, block)
assert.Equal(t, "key: value\n", string(result))
}
func TestAppendYAMLBlock_NilExisting(t *testing.T) {
block := "key: value\n"
result := appendYAMLBlock(nil, block)
assert.Equal(t, "key: value\n", string(result))
}
// mergePlugins tests — union with exclusion prefix (!/-).
func TestMergePlugins_DefaultsOnly(t *testing.T) {
defaults := []string{"plugin-a", "plugin-b"}
result := mergePlugins(defaults, nil)
assert.Equal(t, []string{"plugin-a", "plugin-b"}, result)
}
func TestMergePlugins_WorkspaceAdds(t *testing.T) {
defaults := []string{"plugin-a"}
wsPlugins := []string{"plugin-b", "plugin-a"} // duplicate of default
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-a", "plugin-b"}, result)
}
func TestMergePlugins_ExclusionWithBang(t *testing.T) {
defaults := []string{"plugin-a", "plugin-b", "plugin-c"}
wsPlugins := []string{"!plugin-b"}
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-a", "plugin-c"}, result)
}
func TestMergePlugins_ExclusionWithDash(t *testing.T) {
defaults := []string{"plugin-a", "plugin-b", "plugin-c"}
wsPlugins := []string{"-plugin-b"}
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-a", "plugin-c"}, result)
}
func TestMergePlugins_ExclusionEmptyTarget(t *testing.T) {
defaults := []string{"plugin-a", "plugin-b"}
wsPlugins := []string{"!", "-"} // no-op exclusions
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-a", "plugin-b"}, result)
}
func TestMergePlugins_ExclusionNotInDefaults(t *testing.T) {
// Excluding something not in defaults is a no-op.
defaults := []string{"plugin-a"}
wsPlugins := []string{"!plugin-b"}
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-a"}, result)
}
func TestMergePlugins_WorkspaceAddsNew(t *testing.T) {
defaults := []string{"plugin-a"}
wsPlugins := []string{"plugin-b", "plugin-c"}
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-a", "plugin-b", "plugin-c"}, result)
}
func TestMergePlugins_EmptyInputs(t *testing.T) {
result := mergePlugins(nil, nil)
assert.Empty(t, result)
}
func TestMergePlugins_DeduplicationOrder(t *testing.T) {
// Defaults first; workspace entries deduplicated.
defaults := []string{"plugin-a", "plugin-a", "plugin-b"}
wsPlugins := []string{"plugin-b", "plugin-c", "plugin-c"}
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-a", "plugin-b", "plugin-c"}, result)
}
func TestMergePlugins_ExclusionThenAddSameName(t *testing.T) {
// Remove then re-add: order matters.
defaults := []string{"plugin-a", "plugin-b"}
wsPlugins := []string{"!plugin-a", "plugin-a"}
result := mergePlugins(defaults, wsPlugins)
assert.Equal(t, []string{"plugin-b", "plugin-a"}, result)
}
// isSafeRoleName tests — alphanumeric + hyphen/underscore, no path separators.
func TestIsSafeRoleName_Valid(t *testing.T) {
valid := []string{
"backend-engineer",
"Frontend_Dev",
"sre-123",
"a",
"Z",
"role-name_v2",
}
for _, r := range valid {
if !isSafeRoleName(r) {
t.Errorf("isSafeRoleName(%q) expected true, got false", r)
}
}
}
func TestIsSafeRoleName_Invalid(t *testing.T) {
invalid := []string{
"", // empty
".", // current dir
"..", // parent dir
"role/name", // slash
"role\\name", // backslash
"role name", // space
"role/name", // path separator
"role\tname", // tab
"role\nname", // newline
}
for _, r := range invalid {
if isSafeRoleName(r) {
t.Errorf("isSafeRoleName(%q) expected false, got true", r)
}
}
}
func TestIsSafeRoleName_SpecialCharsRejected(t *testing.T) {
bad := []string{
"role@name",
"role#name",
"role$name",
"role%name",
"role&name",
"role*name",
"role?name",
"role=name",
}
for _, r := range bad {
if isSafeRoleName(r) {
t.Errorf("isSafeRoleName(%q) expected false, got true", r)
}
}
}
// assertFind is a helper: returns index of first occurrence of substr in s, or -1.
func assertFind(t *testing.T, s, substr string) int {
t.Helper()
idx := -1
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
idx = i
break
}
}
return idx
}
@@ -0,0 +1,80 @@
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
)
// supportsRuntime tests — plugin runtime compatibility checking.
func TestSupportsRuntime_EmptyRuntimes(t *testing.T) {
// Empty runtimes = unspecified, try it → always compatible.
info := pluginInfo{Name: "test", Runtimes: nil}
assert.True(t, info.supportsRuntime("claude_code"))
assert.True(t, info.supportsRuntime("any_runtime"))
}
func TestSupportsRuntime_ExactMatch(t *testing.T) {
info := pluginInfo{Name: "test", Runtimes: []string{"claude_code", "anthropic"}}
assert.True(t, info.supportsRuntime("claude_code"))
assert.True(t, info.supportsRuntime("anthropic"))
}
func TestSupportsRuntime_NoMatch(t *testing.T) {
info := pluginInfo{Name: "test", Runtimes: []string{"claude_code"}}
assert.False(t, info.supportsRuntime("openai"))
}
func TestSupportsRuntime_HyphenUnderscoreNormalized(t *testing.T) {
// "claude-code" and "claude_code" are considered equal.
info := pluginInfo{Name: "test", Runtimes: []string{"claude-code"}}
assert.True(t, info.supportsRuntime("claude_code"))
assert.True(t, info.supportsRuntime("anthropic_claude"))
}
func TestSupportsRuntime_HyphenVsUnderscoreReverse(t *testing.T) {
// Plugin declares underscore form; runtime uses hyphen.
info := pluginInfo{Name: "test", Runtimes: []string{"claude_code"}}
assert.True(t, info.supportsRuntime("claude-code"))
}
func TestSupportsRuntime_EmptyStringRuntime(t *testing.T) {
info := pluginInfo{Name: "test", Runtimes: []string{"claude_code"}}
// Empty runtime string: should not match any plugin.
assert.False(t, info.supportsRuntime(""))
}
func TestSupportsRuntime_SingleRuntimeMatch(t *testing.T) {
// Multiple declared runtimes: only matching one is sufficient.
info := pluginInfo{Name: "test", Runtimes: []string{"python", "nodejs", "claude_code"}}
assert.True(t, info.supportsRuntime("claude_code"))
assert.False(t, info.supportsRuntime("ruby"))
}
func TestSupportsRuntime_AllHyphenForms(t *testing.T) {
// Both plugin and runtime use hyphen form.
info := pluginInfo{Name: "test", Runtimes: []string{"claude-code"}}
assert.True(t, info.supportsRuntime("claude-code"))
}
func TestSupportsRuntime_MultipleHyphenNormalization(t *testing.T) {
// Mixed hyphen/underscore forms normalize to the same.
info := pluginInfo{Name: "test", Runtimes: []string{"some-runtime-name"}}
assert.True(t, info.supportsRuntime("some_runtime_name"))
assert.True(t, info.supportsRuntime("some-runtime-name"))
}
func TestSupportsRuntime_EmptyPluginRuntimesWithAnyInput(t *testing.T) {
// Empty Runtimes on plugin = try it regardless of runtime.
info := pluginInfo{Name: "test", Runtimes: []string{}}
assert.True(t, info.supportsRuntime(""))
assert.True(t, info.supportsRuntime("any"))
assert.True(t, info.supportsRuntime("unknown"))
}
func TestSupportsRuntime_ZeroLengthRuntimes(t *testing.T) {
// Empty slice vs nil: both should be treated as "unspecified".
info := pluginInfo{Name: "test"}
assert.True(t, info.supportsRuntime("anything"))
}
@@ -0,0 +1,252 @@
package handlers
import (
"testing"
)
// validateWorkspaceID tests — #687: UUID validation before DB hit.
func TestValidateWorkspaceID_Valid(t *testing.T) {
for _, id := range []string{
"550e8400-e29b-41d4-a716-446655440000",
"00000000-0000-0000-0000-000000000000",
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", // uppercase also valid
} {
err := validateWorkspaceID(id)
if err != nil {
t.Errorf("validateWorkspaceID(%q) returned error: %v", id, err)
}
}
}
func TestValidateWorkspaceID_Invalid(t *testing.T) {
cases := []struct {
id string
check func(string) bool // return true if string should be rejected
}{
{"", func(s string) bool { return true }}, // empty
{"not-a-uuid", func(s string) bool { return true }}, // plain string
{"../../etc/passwd", func(s string) bool { return true }}, // path traversal attempt
{"550e8400-e29b-41d4-a716", func(s string) bool { return true }}, // too short
{"550e8400e29b41d4a716446655440000", func(s string) bool { return true }}, // no dashes
{"550e8400-e29b-41d4-a716-4466554400001", func(s string) bool { return true }}, // too long
{"550e8400-e29b-41d4-a716-44665544000g", func(s string) bool { return true }}, // invalid char g
}
for _, tc := range cases {
err := validateWorkspaceID(tc.id)
if err == nil {
t.Errorf("validateWorkspaceID(%q) expected error, got nil", tc.id)
}
}
}
// validateWorkspaceDir tests — blocks absolute paths, traversal, system dirs.
func TestValidateWorkspaceDir_Valid(t *testing.T) {
valid := []string{
"/home/ubuntu/workspace-data",
"/opt/molecule/workspaces",
"/var/data/molecule",
"/Users/me/.molecule/workspaces",
}
for _, dir := range valid {
err := validateWorkspaceDir(dir)
if err != nil {
t.Errorf("validateWorkspaceDir(%q) returned error: %v", dir, err)
}
}
}
func TestValidateWorkspaceDir_NotAbsolute(t *testing.T) {
rel := []string{
"relative/path",
"./local/workspace",
"../escaped",
"~/workspaces/my-ws",
}
for _, dir := range rel {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("validateWorkspaceDir(%q) expected error for relative path, got nil", dir)
}
}
}
func TestValidateWorkspaceDir_Traversal(t *testing.T) {
// These are all absolute paths but contain ".."
evil := []string{
"/home/ubuntu/../../../etc/passwd",
"/opt/molecule/../../bin/sh",
"/data/../data/../data/../etc/shadow",
}
for _, dir := range evil {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("validateWorkspaceDir(%q) expected error for traversal, got nil", dir)
}
}
}
func TestValidateWorkspaceDir_SystemPaths(t *testing.T) {
systemPaths := []string{
"/etc",
"/var",
"/proc",
"/sys",
"/dev",
"/boot",
"/sbin",
"/bin",
"/lib",
"/usr",
"/etc/some-file",
"/var/log",
"/usr/local/bin",
}
for _, dir := range systemPaths {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("validateWorkspaceDir(%q) expected error for system path, got nil", dir)
}
}
}
// validateWorkspaceFields tests — length limits + YAML-injection prevention.
func TestValidateWorkspaceFields_Valid(t *testing.T) {
err := validateWorkspaceFields(
"My Workspace",
"Backend Engineer",
"claude-3-5-sonnet",
"claude_code",
)
if err != nil {
t.Errorf("validateWorkspaceFields with valid inputs returned error: %v", err)
}
}
func TestValidateWorkspaceFields_NameTooLong(t *testing.T) {
long := make([]byte, 256)
for i := range long {
long[i] = 'a'
}
err := validateWorkspaceFields(string(long), "role", "model", "runtime")
if err == nil {
t.Error("validateWorkspaceFields expected error for name > 255 chars, got nil")
}
}
func TestValidateWorkspaceFields_RoleTooLong(t *testing.T) {
long := make([]byte, 1001)
for i := range long {
long[i] = 'x'
}
err := validateWorkspaceFields("name", string(long), "model", "runtime")
if err == nil {
t.Error("validateWorkspaceFields expected error for role > 1000 chars, got nil")
}
}
func TestValidateWorkspaceFields_ModelTooLong(t *testing.T) {
long := make([]byte, 101)
for i := range long {
long[i] = 'm'
}
err := validateWorkspaceFields("name", "role", string(long), "runtime")
if err == nil {
t.Error("validateWorkspaceFields expected error for model > 100 chars, got nil")
}
}
func TestValidateWorkspaceFields_RuntimeTooLong(t *testing.T) {
long := make([]byte, 101)
for i := range long {
long[i] = 'r'
}
err := validateWorkspaceFields("name", "role", "model", string(long))
if err == nil {
t.Error("validateWorkspaceFields expected error for runtime > 100 chars, got nil")
}
}
func TestValidateWorkspaceFields_Newline(t *testing.T) {
cases := []struct {
label string
field string
}{
{"name with \\n", "name\nwith\nnewline"},
{"name with \\r", "name\rwith\rcarriage"},
{"role with \\n", "role\nhas\nnewline"},
{"role with \\r", "role\rhas\rcarriage"},
{"model with \\n", "model\nhas\nnewline"},
{"runtime with \\n", "runtime\nhas\nnewline"},
}
for _, tc := range cases {
err := validateWorkspaceFields(tc.field, "role", "model", "runtime")
if err == nil {
t.Errorf("validateWorkspaceFields(%s=%q) expected error for newline, got nil", tc.label, tc.field)
}
}
}
func TestValidateWorkspaceFields_YAMLSpecialChars(t *testing.T) {
// yamlSpecialChars = "{}[]|>*&!"
bad := []string{
"name{with}brace",
"name[with]bracket",
"name|with|pipe",
"name*with*asterisk",
"name>with>greater",
"name&with&ampersand",
"name!with!bang",
"role:role:colon",
// Combinations
"bad{[name]}here",
"nested|*&>!|",
}
for _, name := range bad {
err := validateWorkspaceFields(name, "role", "model", "runtime")
if err == nil {
t.Errorf("validateWorkspaceFields(name=%q) expected error for YAML special chars, got nil", name)
}
}
for _, role := range bad {
err := validateWorkspaceFields("name", role, "model", "runtime")
if err == nil {
t.Errorf("validateWorkspaceFields(role=%q) expected error for YAML special chars, got nil", role)
}
}
}
func TestValidateWorkspaceFields_SafePunctuation(t *testing.T) {
// These characters should NOT be rejected (hyphen, underscore, dot, space, comma, paren, apostrophe)
safe := []string{
"My Workspace-v2",
"Backend_Engineer",
"DevOps (Senior)",
"Product, Manager",
"Role With Spaces",
"O'Brien",
}
for _, name := range safe {
err := validateWorkspaceFields(name, "role", "model", "runtime")
if err != nil {
t.Errorf("validateWorkspaceFields(name=%q) unexpected error: %v", name, err)
}
}
for _, role := range safe {
err := validateWorkspaceFields("name", role, "model", "runtime")
if err != nil {
t.Errorf("validateWorkspaceFields(role=%q) unexpected error: %v", role, err)
}
}
}
func TestValidateWorkspaceFields_EmptyFields(t *testing.T) {
// Empty strings should not error (fields are optional in some call paths)
err := validateWorkspaceFields("", "", "", "")
if err != nil {
t.Errorf("validateWorkspaceFields with all empty strings returned error: %v", err)
}
}