feat(org-templates): add ux-ab-lab + manifest entry + schema smoke test

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) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-24 16:22:14 -07:00
parent ad73a56db1
commit a7eb071e35
2 changed files with 69 additions and 1 deletions

View File

@ -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"}
]
}

View File

@ -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