From a7eb071e3530c08b89ed8492bec4fb30dd359ae4 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Fri, 24 Apr 2026 16:22:14 -0700 Subject: [PATCH] feat(org-templates): add ux-ab-lab + manifest entry + schema smoke test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the UX A/B Lab org template — a 7-agent cell for rapid landing-page variant generation. The template is also the first consumer of the new any_of env schema (ANTHROPIC_API_KEY OR CLAUDE_CODE_OAUTH_TOKEN), so it doubles as an end-to-end fixture for that feature. Canvas tree (all claude-code / sonnet): Design Director ├── UX Researcher ├── Visual Designer ├── React Engineer ├── Deploy Engineer ├── A11y + SEO Auditor ← WCAG AA + canonical/noindex gate └── Perf Auditor ← Core Web Vitals gate Template files live in their own standalone repo (Molecule-AI/molecule-ai-org-template-ux-ab-lab, to be published); this change adds the manifest.json entry so fresh clones + CI populate the template via scripts/clone-manifest.sh. Tests: - TestOrgTemplate_ClaudeAnyOfAuthPreflight — parses the exact required_env / recommended_env shape the template ships with via inline YAML (not on-disk, since org-templates/ is gitignored in this monorepo) and verifies either member alternative satisfies the preflight. SEO safety built into the auditor's system prompt: - One canonical variant; all others canonicalise to it. - noindex, follow on non-canonical variants. - Sitemap contains only the canonical URL. - No robots.txt disallow (blocked pages can't emit canonical). Co-Authored-By: Claude Opus 4.7 (1M context) --- manifest.json | 3 +- .../internal/handlers/org_test.go | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 1bba24ad..72f37404 100644 --- a/manifest.json +++ b/manifest.json @@ -39,6 +39,7 @@ {"name": "free-beats-all", "repo": "Molecule-AI/molecule-ai-org-template-free-beats-all", "ref": "main"}, {"name": "medo-smoke", "repo": "Molecule-AI/molecule-ai-org-template-medo-smoke", "ref": "main"}, {"name": "molecule-worker-gemini", "repo": "Molecule-AI/molecule-ai-org-template-molecule-worker-gemini", "ref": "main"}, - {"name": "reno-stars", "repo": "Molecule-AI/molecule-ai-org-template-reno-stars", "ref": "main"} + {"name": "reno-stars", "repo": "Molecule-AI/molecule-ai-org-template-reno-stars", "ref": "main"}, + {"name": "ux-ab-lab", "repo": "Molecule-AI/molecule-ai-org-template-ux-ab-lab", "ref": "main"} ] } diff --git a/workspace-server/internal/handlers/org_test.go b/workspace-server/internal/handlers/org_test.go index 41d3b1bf..19dbece9 100644 --- a/workspace-server/internal/handlers/org_test.go +++ b/workspace-server/internal/handlers/org_test.go @@ -829,6 +829,73 @@ func TestCollectOrgEnv_RejectsInvalidNames(t *testing.T) { } } +// TestOrgTemplate_ClaudeAnyOfAuthPreflight exercises the shape the +// ux-ab-lab template ships with: a single any-of group at the org +// level covering ANTHROPIC_API_KEY vs. CLAUDE_CODE_OAUTH_TOKEN, plus +// two strict recommended entries (SERPER_API_KEY, VERCEL_TOKEN). +// Proves the end-to-end YAML → OrgTemplate → collectOrgEnv → IsSatisfied +// pipeline works for the canonical "Claude sub OR API key" pattern +// without depending on the on-disk template file (org-templates/ is +// populated by the clone-manifest, not tracked in this monorepo). +func TestOrgTemplate_ClaudeAnyOfAuthPreflight(t *testing.T) { + src := ` +name: UX A/B Lab +required_env: + - any_of: + - ANTHROPIC_API_KEY + - CLAUDE_CODE_OAUTH_TOKEN +recommended_env: + - SERPER_API_KEY + - VERCEL_TOKEN +workspaces: + - name: Design Director + children: + - name: UX Researcher + - name: Visual Designer + - name: React Engineer + - name: Deploy Engineer + - name: A11y + SEO Auditor + - name: Perf Auditor +` + var tmpl OrgTemplate + if err := yaml.Unmarshal([]byte(src), &tmpl); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(tmpl.Workspaces) != 1 || len(tmpl.Workspaces[0].Children) != 6 { + t.Fatalf("expected 1 root with 6 children, got shape %+v", tmpl.Workspaces) + } + + required, recommended := collectOrgEnv(&tmpl) + if len(required) != 1 { + t.Fatalf("expected 1 required requirement (the any-of group), got %d: %v", len(required), reqNames(required)) + } + if required[0].Name != "" { + t.Errorf("expected any-of group, got strict name %q", required[0].Name) + } + wantMembers := []string{"ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"} + got := append([]string(nil), required[0].AnyOf...) + sort.Strings(got) + if !stringSlicesEqual(got, wantMembers) { + t.Errorf("any-of members mismatch: got %v, want %v", got, wantMembers) + } + + // Either member should independently satisfy the group. + if !required[0].IsSatisfied(map[string]struct{}{"ANTHROPIC_API_KEY": {}}) { + t.Errorf("ANTHROPIC_API_KEY alone should satisfy the group") + } + if !required[0].IsSatisfied(map[string]struct{}{"CLAUDE_CODE_OAUTH_TOKEN": {}}) { + t.Errorf("CLAUDE_CODE_OAUTH_TOKEN alone should satisfy the group") + } + if required[0].IsSatisfied(map[string]struct{}{"OPENAI_API_KEY": {}}) { + t.Errorf("unrelated key should NOT satisfy the group") + } + + wantRec := []string{"SERPER_API_KEY", "VERCEL_TOKEN"} + if !stringSlicesEqual(reqNames(recommended), wantRec) { + t.Errorf("recommended mismatch: got %v, want %v", reqNames(recommended), wantRec) + } +} + // TestEnvRequirement_UnmarshalYAML proves the on-disk YAML shape // (scalar OR `{any_of: [...]}` block) round-trips into EnvRequirement // correctly. The preflight pipeline reads user-authored org.yaml