package zerotoheroci import ( "encoding/json" "os" "os/exec" "path/filepath" "regexp" "strings" "sync" "testing" "time" goagent "go-micro.dev/v6/agent" "go-micro.dev/v6/store" ) func TestZeroToHeroReferenceDocs(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md")) for _, want := range []string{ "make harness", "make zero-to-hero-transcript", "make inner-loop", "go test ./cmd/micro/cli/new -run TestZeroToOne -count=1", "go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1", "go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1", "go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1", "go test ./examples/first-agent -run TestRunFirstAgent -count=1", "go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1", "./internal/harness/zero-to-hero-ci/run.sh", "micro zero-to-hero", "go run ./internal/harness/agent-flow", "make provider-conformance-mock", "internal/harness/plan-delegate", "internal/harness/universe", } { if !strings.Contains(guide, want) { t.Fatalf("0→hero guide missing %q", want) } } runScript := readFile(t, filepath.Join(root, "internal", "harness", "zero-to-hero-ci", "run.sh")) for _, want := range []string{ "go test ./cmd/micro/cli/new -run TestZeroToOne -count=1", "go test ./cmd/micro -run 'TestFirstAgentWalkthroughCLIBoundaries|TestExamplesWayfindingIndexStaysLinked|TestExamplesCommandPointsAtWayfindingIndex|TestZeroToHeroCLIBoundaries|TestZeroToHeroCommandPrintsMaintainedNoSecretPath' -count=1", "go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1", "go test ./examples/first-agent -run TestRunFirstAgent -count=1", "go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1", } { if !strings.Contains(runScript, want) { t.Fatalf("0→hero CI run script missing lifecycle command %q", want) } } for _, want := range []string{ "scaffold:", "run/chat/inspect:", "deploy dry-run:", "chat/inspect:", "first-agent app:", "0→hero app:", "flow history:", } { if !strings.Contains(runScript, want) { t.Fatalf("0→hero CI run script missing debuggable boundary label %q", want) } } readme := readFile(t, filepath.Join(root, "README.md")) if !strings.Contains(readme, "internal/website/docs/guides/zero-to-hero.md") { t.Fatal("README does not point to the canonical 0→hero guide") } if !strings.Contains(readme, "make zero-to-hero-transcript") { t.Fatal("README does not expose the focused ordered 0→hero transcript contract") } if !strings.Contains(readme, "make inner-loop") { t.Fatal("README does not expose the focused CLI inner-loop contract") } nav := readFile(t, filepath.Join(root, "internal", "website", "_data", "navigation.yml")) if !strings.Contains(nav, "0→hero Reference") || !strings.Contains(nav, "/docs/guides/zero-to-hero.html") { t.Fatal("website navigation does not expose the canonical 0→hero guide") } } func TestZeroToHeroTranscriptTargetStaysOrdered(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) makefile := readFile(t, filepath.Join(root, "Makefile")) if !strings.Contains(makefile, "zero-to-hero-transcript:") { t.Fatal("Makefile missing focused zero-to-hero-transcript target") } if !strings.Contains(makefile, "./internal/harness/zero-to-hero-ci/run.sh") { t.Fatal("zero-to-hero-transcript target must run the maintained CI transcript") } runScript := readFile(t, filepath.Join(root, "internal", "harness", "zero-to-hero-ci", "run.sh")) assertOrderedMarkers(t, "0→hero CI transcript", runScript, []string{ `run_step "scaffold: 0→1 service contract"`, `run_step "run/chat/inspect: first-agent CLI boundaries"`, `run_step "chat/inspect: no-secret first-agent transcript and docs"`, `run_step "first-agent app: runnable provider-free example"`, `run_step "0→hero app: support lifecycle smoke"`, `run_step "flow history: deterministic services → agents → workflows harnesses"`, `run_step "deploy dry-run: configured target plan"`, }) guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md")) if !strings.Contains(guide, "make zero-to-hero-transcript") { t.Fatal("0→hero guide must point developers at the focused ordered transcript target") } } func TestZeroToHeroDeployDryRunCommandSmoke(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) absRoot, err := filepath.Abs(root) if err != nil { t.Fatalf("resolve repository root: %v", err) } bin := filepath.Join(t.TempDir(), "micro") build := exec.Command("go", "build", "-o", bin, "./cmd/micro") build.Dir = absRoot if out, err := build.CombinedOutput(); err != nil { t.Fatalf("build micro CLI for deploy dry-run smoke: %v\n%s", err, out) } workspace := t.TempDir() writeFile(t, filepath.Join(workspace, "micro.mu"), `service api path ./api deploy prod ssh deploy@prod.example.com path /srv/micro `) if err := os.Mkdir(filepath.Join(workspace, "api"), 0o755); err != nil { t.Fatalf("create service dir: %v", err) } cmd := exec.Command(bin, "deploy", "--dry-run", "prod") cmd.Dir = workspace cmd.Env = append(os.Environ(), "MICRO_CONFIG_FILE="+filepath.Join(workspace, "micro.mu")) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("documented deploy dry-run command failed: %v\n%s", err, out) } got := string(out) for _, want := range []string{ "micro deploy --dry-run", "Target", "deploy@prod.example.com", "Remote path", "/srv/micro", "Services", "api", "No SSH, rsync, systemd, or remote deployment was performed.", } { if !strings.Contains(got, want) { t.Fatalf("deploy dry-run output missing %q:\n%s", want, got) } } guide := readFile(t, filepath.Join(absRoot, "internal", "website", "docs", "guides", "zero-to-hero.md")) if !strings.Contains(guide, "micro deploy --dry-run prod") { t.Fatal("0→hero guide must document the same deploy dry-run command covered by CI") } } func TestGuidesNavigationLeadsWithDoing(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) nav := readFile(t, filepath.Join(root, "internal", "website", "_data", "navigation.yml")) orderedGuides := []string{ "/docs/guides/no-secret-first-agent.html", "/docs/guides/your-first-agent.html", "/docs/guides/zero-to-hero.html", "/docs/guides/plan-delegate.html", "/docs/guides/agent-guardrails.html", "/docs/guides/agents-and-workflows.html", "/docs/guides/agent-patterns.html", "/docs/guides/agent-harness.html", "/docs/guides/agent-loops.html", } last := -1 for _, guide := range orderedGuides { idx := strings.Index(nav, guide) if idx == -1 { t.Fatalf("website navigation does not expose %s", guide) } if idx < last { t.Fatalf("website navigation should lead with hands-on guides; %s appeared out of order", guide) } last = idx doc := strings.TrimPrefix(strings.TrimSuffix(guide, ".html"), "/docs/") + ".md" if _, err := os.Stat(filepath.Join(root, "internal", "website", "docs", doc)); err != nil { t.Fatalf("navigation links to missing guide %s: %v", guide, err) } } } func TestYourFirstAgentTutorialSmoke(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) absRoot, err := filepath.Abs(root) if err != nil { t.Fatalf("resolve repository root: %v", err) } guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "your-first-agent.md")) for _, want := range []string{ "go test ./internal/harness/zero-to-hero-ci -run TestYourFirstAgentTutorialSmoke -count=1", "micro agent preflight", "mkdir first-agent", "go mod init example.com/first-agent", "go get go-micro.dev/v6@v6", "micro run", "micro call task TaskService.Create", "micro call task TaskService.List", "micro chat assistant", "micro inspect agent assistant", } { if !strings.Contains(guide, want) { t.Fatalf("Your First Agent guide missing copy/paste boundary %q", want) } } mainGo := extractFirstAgentMain(t, guide) workspace := t.TempDir() writeFile(t, filepath.Join(workspace, "go.mod"), "module example.com/first-agent\n\ngo 1.24\n\nrequire go-micro.dev/v6 v6.0.0\n\nreplace go-micro.dev/v6 => "+absRoot+"\n") writeFile(t, filepath.Join(workspace, "main.go"), mainGo) runInWorkspace(t, workspace, "go", "mod", "tidy") runInWorkspace(t, workspace, "go", "test", "./...") } func extractFirstAgentMain(t *testing.T, guide string) string { t.Helper() start := strings.Index(guide, "Add `main.go`:") if start == -1 { t.Fatal("Your First Agent guide is missing the main.go section") } rest := guide[start:] open := strings.Index(rest, "```go") if open == -1 { t.Fatal("Your First Agent guide is missing a Go code fence for main.go") } rest = rest[open+len("```go"):] close := strings.Index(rest, "```") if close == -1 { t.Fatal("Your First Agent guide main.go code fence is not closed") } return strings.TrimSpace(rest[:close]) + "\n" } func writeFile(t *testing.T, name, contents string) { t.Helper() if err := os.WriteFile(name, []byte(contents), 0o644); err != nil { t.Fatalf("write %s: %v", name, err) } } func runInWorkspace(t *testing.T, workspace, name string, args ...string) { t.Helper() cmd := exec.Command(name, args...) cmd.Dir = workspace out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("Your First Agent tutorial command %q does not pass from a clean workspace: %v\n%s", strings.Join(append([]string{name}, args...), " "), err, out) } } func TestArchitectureDocsAlignWithAgentHarnessLifecycle(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) doc := readFile(t, filepath.Join(root, "internal", "website", "docs", "architecture.md")) for _, want := range []string{ "services → agents → workflows lifecycle", "## Service substrate", "## Agent harness", "## Workflows", "## Interop gateways", "`model` / `ai.Model`", "`store` / memory", "`ai.Tools`", "`agent`", "`flow`", "`micro mcp`", "`micro a2a`", "[AI Integration](ai-integration.html)", "[Your First Agent](guides/your-first-agent.html)", "[0→hero Reference](guides/zero-to-hero.html)", } { if !strings.Contains(doc, want) { t.Fatalf("architecture doc missing lifecycle marker %q", want) } } assertOrderedMarkers(t, "architecture lifecycle", doc, []string{ "## Service substrate", "## Agent harness", "## Workflows", "## Interop gateways", "## Developer path", }) } func TestFirstAgentWayfindingDocs(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) checks := []struct { name string file string heading string links []string }{ { name: "README first-agent on-ramp", file: filepath.Join(root, "README.md"), heading: "### First agent on-ramp", links: []string{ "internal/website/docs/guides/install-troubleshooting.md", "make docs-wayfinding", "micro agent demo", "micro agent quickcheck", "micro examples", "micro zero-to-hero", "internal/website/docs/guides/no-secret-first-agent.md", "internal/website/docs/guides/your-first-agent.md", "micro chat", "internal/website/docs/guides/debugging-agents.md", "micro inspect agent ", "internal/website/docs/guides/zero-to-hero.md", }, }, { name: "README examples list", file: filepath.Join(root, "README.md"), heading: "## Examples", links: []string{ "examples/README.md", "examples/first-agent/", }, }, { name: "repository examples index", file: filepath.Join(root, "examples", "README.md"), heading: "## Recommended first-agent path", links: []string{ "./first-agent/", "./support/", }, }, { name: "repository examples wayfinding index", file: filepath.Join(root, "examples", "INDEX.md"), heading: "## Recommended adoption path", links: []string{ "./hello-world/", "./first-agent/", "./support/", }, }, { name: "micro README first-agent on-ramp", file: filepath.Join(root, "cmd", "micro", "README.md"), heading: "## First agent on-ramp", links: []string{ "make docs-wayfinding", "micro agent demo", "micro agent quickcheck", "micro examples", "micro zero-to-hero", }, }, { name: "website examples index", file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"), heading: "## Start here", links: []string{ "https://github.com/micro/go-micro/tree/master/examples/first-agent", "../guides/no-secret-first-agent.html", "../guides/your-first-agent.html", "../guides/debugging-agents.html", "../guides/zero-to-hero.html", }, }, { name: "website getting-started on-ramp", file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"), heading: "### First-agent on-ramp", links: []string{ "guides/install-troubleshooting.html", "make docs-wayfinding", "micro agent demo", "micro agent quickcheck", "micro examples", "micro zero-to-hero", "https://github.com/micro/go-micro/blob/master/examples/INDEX.md", "https://github.com/micro/go-micro/tree/master/examples/support", "https://github.com/micro/go-micro/tree/master/examples/first-agent", "guides/no-secret-first-agent.html", "guides/your-first-agent.html", "micro chat", "guides/debugging-agents.html", "micro inspect agent ", "guides/zero-to-hero.html", }, }, { name: "website quickstart next steps", file: filepath.Join(root, "internal", "website", "docs", "quickstart.md"), heading: "## Next Steps", links: []string{ "guides/install-troubleshooting.html", "micro agent demo", "micro agent quickcheck", "micro examples", "micro zero-to-hero", "https://github.com/micro/go-micro/blob/master/examples/INDEX.md", "https://github.com/micro/go-micro/tree/master/examples/support", "https://github.com/micro/go-micro/tree/master/examples/first-agent", "guides/no-secret-first-agent.html", "guides/your-first-agent.html", "micro chat", "guides/debugging-agents.html", "micro inspect agent ", "guides/zero-to-hero.html", }, }, { name: "website docs index learn more", file: filepath.Join(root, "internal", "website", "docs", "index.md"), heading: "## Learn More", links: []string{ "getting-started.html", "https://github.com/micro/go-micro/blob/master/examples/INDEX.md", "https://github.com/micro/go-micro/tree/master/examples/support", "guides/no-secret-first-agent.html", "guides/your-first-agent.html", "micro chat", "guides/debugging-agents.html", "micro inspect agent ", "guides/zero-to-hero.html", }, }, } for _, check := range checks { t.Run(check.name, func(t *testing.T) { doc := firstMarkdownSection(t, readFile(t, check.file), check.heading) last := -1 for _, link := range check.links { idx := strings.Index(doc, link) if idx == -1 { t.Fatalf("%s missing first-agent wayfinding link %q; keep the no-secret → first-agent → debugging → 0→hero path discoverable", check.name, link) } assertWayfindingTargetExists(t, root, check.file, link) if idx < last { t.Fatalf("%s link %q appeared out of order; expected no-secret → first-agent → debugging → 0→hero", check.name, link) } last = idx } }) } } func TestFirstAgentWayfindingCanonicalTrailStaysInSync(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) onRampTrail := []string{ "micro agent demo", "micro agent quickcheck", "micro examples", "micro zero-to-hero", "examples/INDEX.md", "examples/first-agent", "examples/support", "no-secret-first-agent", "your-first-agent", "micro chat", "debugging-agents", "micro inspect agent ", "zero-to-hero", } checks := []struct { name string file string heading string markers []string }{ { name: "README first-agent on-ramp", file: filepath.Join(root, "README.md"), heading: "### First agent on-ramp", markers: onRampTrail, }, { name: "website docs index first-agent path", file: filepath.Join(root, "internal", "website", "docs", "index.md"), heading: "## Learn More", markers: onRampTrail, }, { name: "website getting-started first-agent on-ramp", file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"), heading: "### First-agent on-ramp", markers: onRampTrail, }, { name: "website quickstart next steps", file: filepath.Join(root, "internal", "website", "docs", "quickstart.md"), heading: "## Next Steps", markers: onRampTrail, }, { name: "examples map recommended adoption path", file: filepath.Join(root, "examples", "INDEX.md"), heading: "", markers: []string{ "micro examples", "micro zero-to-hero", "examples/INDEX.md", "examples/first-agent", "examples/support", "zero-to-hero", }, }, { name: "no-secret first-agent guide next steps", file: filepath.Join(root, "internal", "website", "docs", "guides", "no-secret-first-agent.md"), heading: "", markers: []string{ "micro agent demo", "examples/first-agent", "examples/support", "your-first-agent", "micro chat", "debugging-agents", "micro inspect agent ", }, }, { name: "0→hero guide related examples", file: filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md"), heading: "", markers: []string{ "micro zero-to-hero", "examples/first-agent", "examples/support", "micro inspect agent ", "zero-to-hero", }, }, } for _, check := range checks { t.Run(check.name, func(t *testing.T) { section := readFile(t, check.file) if check.heading != "" { section = firstMarkdownSection(t, section, check.heading) } for _, marker := range check.markers { if !containsWayfindingMarker(section, marker) { t.Fatalf("%s missing canonical first-agent wayfinding marker %q; keep README, website, examples, no-secret, and 0→hero surfaces aligned", check.name, marker) } } }) } } func TestFirstAgentWayfindingLinkTargetsResolve(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) checks := []struct { name string file string heading string }{ { name: "README first-agent on-ramp", file: filepath.Join(root, "README.md"), heading: "### First agent on-ramp", }, { name: "README examples list", file: filepath.Join(root, "README.md"), heading: "## Examples", }, { name: "repository examples index", file: filepath.Join(root, "examples", "README.md"), heading: "## Recommended first-agent path", }, { name: "repository examples wayfinding index", file: filepath.Join(root, "examples", "INDEX.md"), heading: "## Recommended adoption path", }, { name: "website examples index", file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"), heading: "## Start here", }, { name: "website getting-started on-ramp", file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"), heading: "### First-agent on-ramp", }, { name: "website quickstart next steps", file: filepath.Join(root, "internal", "website", "docs", "quickstart.md"), heading: "## Next Steps", }, { name: "website docs index learn more", file: filepath.Join(root, "internal", "website", "docs", "index.md"), heading: "## Learn More", }, } for _, check := range checks { t.Run(check.name, func(t *testing.T) { section := firstMarkdownSection(t, readFile(t, check.file), check.heading) links := markdownLinks(section) if len(links) == 0 { t.Fatalf("%s has no Markdown links in %q", check.name, check.heading) } for _, link := range links { assertWayfindingTargetExists(t, root, check.file, link) } }) } } func TestFirstAgentGuideChainDocumentsRequiredNextSteps(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) checks := []struct { name string file string markers []string }{ { name: "no-secret transcript hands off to live build and debug", file: filepath.Join(root, "internal", "website", "docs", "guides", "no-secret-first-agent.md"), markers: []string{ "micro agent demo", "go run ./examples/first-agent", "go run ./examples/support", "go test ./examples/first-agent -run TestRunFirstAgent -count=1", "go test ./examples/support -run TestRunSupportMockSmoke -count=1", "make harness", "micro agent preflight", "micro run", "micro chat assistant", "micro inspect agent assistant", "Debugging your agent", "debugging-agents.html", }, }, { name: "your-first-agent keeps no-secret, preflight, doctor, inspect, and debug nearby", file: filepath.Join(root, "internal", "website", "docs", "guides", "your-first-agent.md"), markers: []string{ "no-secret-first-agent.html", "go run ./examples/support", "micro agent preflight", "micro agent doctor", "micro run", "micro chat assistant", "micro inspect agent assistant", "debugging-agents.html", "zero-to-hero.html", }, }, { name: "debugging guide starts at install/preflight and preserves inspect/history recovery", file: filepath.Join(root, "internal", "website", "docs", "guides", "debugging-agents.md"), markers: []string{ "install-troubleshooting.html", "micro agent preflight", "micro agent doctor", "micro run", "micro chat", "micro inspect agent support", "micro agent history", "go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1", }, }, { name: "zero-to-hero guide exposes the provider-free contract commands", file: filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md"), markers: []string{ "go test ./internal/harness/zero-to-hero-ci -run TestFirstAgentWayfinding -count=1", "micro zero-to-hero", "go run ./examples/first-agent", "go run ./examples/support", "make harness", "go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1", "go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1", "make provider-conformance-mock", }, }, } for _, check := range checks { t.Run(check.name, func(t *testing.T) { doc := readFile(t, check.file) for _, marker := range check.markers { if !strings.Contains(doc, marker) { t.Fatalf("%s missing required first-agent next-step marker %q", check.name, marker) } if firstAgentMarkerIsLink(marker) { assertWayfindingTargetExists(t, root, check.file, marker) } } }) } } func firstAgentMarkerIsLink(marker string) bool { return strings.HasSuffix(marker, ".html") || strings.HasSuffix(marker, ".md") || strings.HasPrefix(marker, "./") || strings.HasPrefix(marker, "../") || strings.HasPrefix(marker, "https://github.com/micro/go-micro/") } func TestFirstAgentLifecycleCommandOrderIsDocumented(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) checks := []struct { name string file string heading string markers []string }{ { name: "0→hero guide lifecycle", file: filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md"), heading: "## What the contract covers", markers: []string{"micro new", "micro run", "micro chat", "micro inspect agent", "micro deploy --dry-run"}, }, { name: "CLI docs lifecycle", file: filepath.Join(root, "cmd", "micro", "cli", "cli.go"), heading: "const docsWayfinding", markers: []string{"micro agent demo", "micro run", "micro chat", "micro inspect agent", "deploy dry-run"}, }, { name: "scaffold next steps", file: filepath.Join(root, "cmd", "micro", "cli", "new", "new.go"), heading: "func printNextSteps", markers: []string{"go run .", "micro chat", "micro inspect agent", "micro agent demo", "micro docs"}, }, } for _, check := range checks { t.Run(check.name, func(t *testing.T) { doc := readFile(t, check.file) if check.heading != "" { start := strings.Index(doc, check.heading) if start == -1 { t.Fatalf("%s missing %q boundary", check.name, check.heading) } doc = doc[start:] } assertOrderedMarkers(t, check.name, doc, check.markers) }) } } func TestExamplesIndexesPreserveLifecycleMap(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) checks := []struct { name string file string heading string want []string ordered []string }{ { name: "repository examples lifecycle map", file: filepath.Join(root, "examples", "README.md"), heading: "## Recommended first-agent path", want: []string{ "hello-world", "0→1", "first-agent", "support", "services", "agents", "workflows", "Debugging and observability", }, ordered: []string{"1. First service", "2. First agent", "3. First workflow"}, }, { name: "website examples lifecycle map", file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"), heading: "## Start here", want: []string{ "examples/hello-world", "0→1", "examples/first-agent", "examples/support", "services", "agents", "workflows", "debugging-agents.html", }, ordered: []string{"0→1 service", "Provider-free first agent", "0→hero lifecycle"}, }, } for _, check := range checks { t.Run(check.name, func(t *testing.T) { section := firstMarkdownSection(t, readFile(t, check.file), check.heading) for _, want := range check.want { if !strings.Contains(section, want) { t.Fatalf("%s missing lifecycle map marker %q", check.name, want) } } last := -1 for _, marker := range check.ordered { idx := strings.Index(section, marker) if idx == -1 { t.Fatalf("%s missing ordered example marker %q", check.name, marker) } if idx < last { t.Fatalf("%s marker %q appeared out of order; keep examples flowing hello-world/0→1 → first-agent → support/0→hero", check.name, marker) } last = idx } }) } } func TestGettingStartedDocsLeadWithNoSecretFirstRun(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) checks := []struct { name string file string section string want []string }{ { name: "README quick start", file: filepath.Join(root, "README.md"), section: "## Quick Start", want: []string{ "install troubleshooting guide", "### Fastest start — no API key", "micro new helloworld", "micro run", "curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call", "### First agent on-ramp", "micro agent demo", "### Generate from a prompt — with an LLM key", }, }, { name: "CLI README", file: filepath.Join(root, "cmd", "micro", "README.md"), section: "## Create a service", want: []string{ "## Create a service", "micro new helloworld", "## Run the service", "micro run", "micro agent demo", }, }, { name: "website getting started", file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"), section: "Install troubleshooting", want: []string{ "Install troubleshooting", "## Quick Start: Scaffold, Run, Call", "micro new helloworld", "micro run", "curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call", "### First-agent on-ramp", "micro agent demo", "## Generate from a Prompt — with an LLM key", }, }, { name: "website quickstart", file: filepath.Join(root, "internal", "website", "docs", "quickstart.md"), section: "## Create Your First Service", want: []string{ "micro new helloworld", "micro run", "curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call", "## Next Steps", "micro agent demo", "micro agent quickcheck", "micro zero-to-hero", "guides/no-secret-first-agent.html", "guides/debugging-agents.html", "micro inspect agent ", "guides/zero-to-hero.html", }, }, } for _, check := range checks { t.Run(check.name, func(t *testing.T) { doc := readFile(t, check.file) if check.section != "" { start := strings.Index(doc, check.section) if start == -1 { t.Fatalf("%s missing %q section", check.name, check.section) } doc = doc[start:] } last := -1 for _, want := range check.want { idx := strings.Index(doc, want) if idx == -1 { t.Fatalf("%s missing no-secret first-run marker %q", check.name, want) } if idx < last { t.Fatalf("%s marker %q appeared out of order; keep install/scaffold/run/call before provider-backed generation", check.name, want) } last = idx } }) } } func TestNoSecretFirstAgentTranscript(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "no-secret-first-agent.md")) for _, want := range []string{ "micro agent demo", "go run ./examples/first-agent", "go test ./examples/first-agent -run TestRunFirstAgent -count=1", "go run ./examples/support", "go test ./examples/support -run TestRunSupportMockSmoke -count=1", "make harness", "micro agent preflight", "micro run", "micro chat assistant", "micro inspect agent assistant", "go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1", "No-secret first-agent transcript", } { if !strings.Contains(guide, want) { t.Fatalf("no-secret first-agent transcript missing %q", want) } } debugCheckpoint := firstMarkdownSection(t, guide, "## Debug transcript checkpoint") for _, want := range []string{ `micro chat assistant --prompt "Triage ticket-1 for Alice"`, "micro inspect agent assistant --limit 1", "micro agent history assistant", "status, event count, last event", "Debugging your agent", "debugging-agents.html", } { if !strings.Contains(debugCheckpoint, want) { t.Fatalf("no-secret debug transcript checkpoint missing %q", want) } } debuggingGuide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "debugging-agents.md")) for _, want := range []string{ "Provider-free quickcheck", "go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1", "micro inspect agent assistant --limit 1", "micro inspect agent --status done", "micro agent history assistant", } { if !strings.Contains(debuggingGuide, want) { t.Fatalf("debugging guide missing provider-free quickcheck marker %q", want) } } harnessReadme := readFile(t, filepath.Join(root, "internal", "harness", "zero-to-hero-ci", "README.md")) if !strings.Contains(harnessReadme, "go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1") { t.Fatal("0→hero harness README does not expose the agent debugging quickcheck command") } readme := readFile(t, filepath.Join(root, "README.md")) if !strings.Contains(readme, "internal/website/docs/guides/no-secret-first-agent.md") { t.Fatal("README does not point to the no-secret first-agent transcript") } firstAgent := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "your-first-agent.md")) if !strings.Contains(firstAgent, "no-secret-first-agent.html") { t.Fatal("Your First Agent guide does not point to the no-secret transcript") } } func TestNoSecretFirstAgentDebuggingSmoke(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) home := t.TempDir() storeDir := filepath.Join(home, "micro", "store") st := store.NewFileStore(store.DirOption(storeDir)) seedNoSecretAgentDebuggingState(t, st) if err := st.Close(); err != nil { t.Fatalf("close seeded store: %v", err) } micro := buildMicroBinary(t, root) for _, tc := range []struct { name string args []string want []string }{ { name: "demo advertises provider-free debug path", args: []string{"agent", "demo"}, want: []string{"No-secret first-agent demo", "provider-free", "run history", "micro inspect agent "}, }, { name: "inspect shows seeded run history", args: []string{"inspect", "agent", "assistant", "--limit", "1"}, want: []string{`Agent "assistant" runs`, "run-debug-smoke", "status=done", "events=3", "last=done", "trace=trace-debug-"}, }, { name: "inspect filters documented statuses", args: []string{"inspect", "agent", "--status", "done", "--json", "assistant"}, want: []string{"run-debug-smoke", `"status": "done"`, `"trace_id": "trace-debug-smoke"`}, }, { name: "agent history shows memory and run index", args: []string{"agent", "history", "assistant"}, want: []string{"user:", "Triage ticket-1", "assistant:", "ticket-1 is ready", "Runs:", "run-debug-smoke", "status=done"}, }, } { t.Run(tc.name, func(t *testing.T) { out := runMicroCLIWithHome(t, micro, home, tc.args...) for _, want := range tc.want { if !strings.Contains(out, want) { t.Fatalf("micro %s output missing %q:\n%s", strings.Join(tc.args, " "), want, out) } } }) } } func TestFirstAgentCLIChatInspectFixture(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) absRoot, err := filepath.Abs(root) if err != nil { t.Fatalf("resolve repository root: %v", err) } workspace := t.TempDir() home := filepath.Join(workspace, "home") if err := os.MkdirAll(home, 0o755); err != nil { t.Fatalf("create fixture home: %v", err) } writeFile(t, filepath.Join(workspace, "go.mod"), "module example.com/first-agent-cli-fixture\n\ngo 1.24\n\nrequire go-micro.dev/v6 v6.0.0\n\nreplace go-micro.dev/v6 => "+filepath.ToSlash(absRoot)+"\n") writeFile(t, filepath.Join(workspace, "main.go"), firstAgentCLIFixtureSource()) runInWorkspace(t, workspace, "go", "mod", "tidy") micro := buildMicroBinary(t, absRoot) fixtureBin := filepath.Join(workspace, "first-agent-cli-fixture") runInWorkspace(t, workspace, "go", "build", "-o", fixtureBin, ".") fixture := exec.Command(fixtureBin) fixture.Dir = workspace fixture.Env = append(os.Environ(), "HOME="+home, "MICRO_AI_API_KEY=", "OPENAI_API_KEY=", "ANTHROPIC_API_KEY=", "GEMINI_API_KEY=", ) var fixtureOut lockedBuffer fixture.Stdout = &fixtureOut fixture.Stderr = &fixtureOut if err := fixture.Start(); err != nil { t.Fatalf("start first-agent CLI fixture: %v\n%s", err, fixtureOut.String()) } defer func() { if fixture.Process != nil { _ = fixture.Process.Signal(os.Interrupt) _ = fixture.Process.Kill() } }() waitForCLIOutput(t, &fixtureOut, "first-agent fixture ready", 15*time.Second) waitForRegisteredAgent(t, micro, home, "assistant", &fixtureOut, 15*time.Second) chat := runMicroCLIWithHome(t, micro, home, "chat", "--prompt", "Summarize my first-agent next steps", "assistant") for _, want := range []string{"assistant:", "install the CLI", "run a service", "chat with an agent"} { if !strings.Contains(chat, want) { t.Fatalf("micro chat assistant output missing %q:\n%s\nfixture output:\n%s", want, chat, fixtureOut.String()) } } stopFixture(t, fixture) inspect := runMicroCLIWithHome(t, micro, home, "inspect", "agent", "assistant", "--limit", "1") for _, want := range []string{`Agent "assistant" runs`, "status=done", "last=done"} { if !strings.Contains(inspect, want) { t.Fatalf("micro inspect agent assistant output missing %q:\n%s\nfixture output:\n%s", want, inspect, fixtureOut.String()) } } } func stopFixture(t *testing.T, cmd *exec.Cmd) { t.Helper() if cmd.Process == nil { return } done := make(chan error, 1) _ = cmd.Process.Signal(os.Interrupt) go func() { done <- cmd.Wait() }() select { case <-done: case <-time.After(2 * time.Second): _ = cmd.Process.Kill() <-done } } func waitForCLIOutput(t *testing.T, out *lockedBuffer, want string, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if strings.Contains(out.String(), want) { return } time.Sleep(100 * time.Millisecond) } t.Fatalf("timed out waiting for fixture output %q; got:\n%s", want, out.String()) } func waitForRegisteredAgent(t *testing.T, micro, home, agent string, fixtureOut *lockedBuffer, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) var lastOut []byte var lastErr error for time.Now().Before(deadline) { cmd := exec.Command(micro, "agent", "list") cmd.Env = microCLIEnv(home) lastOut, lastErr = cmd.CombinedOutput() if lastErr == nil && strings.Contains(string(lastOut), agent) { return } time.Sleep(100 * time.Millisecond) } t.Fatalf("timed out waiting for registered agent %q; last micro agent list error: %v\nlast output:\n%s\nfixture output:\n%s", agent, lastErr, lastOut, fixtureOut.String()) } type lockedBuffer struct { mu sync.Mutex b strings.Builder } func (b *lockedBuffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() return b.b.Write(p) } func (b *lockedBuffer) String() string { b.mu.Lock() defer b.mu.Unlock() return b.b.String() } func firstAgentCLIFixtureSource() string { return `package main import ( "context" "fmt" "os" "os/signal" "path/filepath" "strings" "syscall" "go-micro.dev/v6/agent" "go-micro.dev/v6/ai" "go-micro.dev/v6/service" "go-micro.dev/v6/store" ) type NotesService struct{} type ListNotesRequest struct{} type ListNotesResponse struct { Notes []string ` + "`json:\"notes\" description:\"Notes the assistant can summarize\"`" + ` } func (s *NotesService) List(ctx context.Context, req *ListNotesRequest, rsp *ListNotesResponse) error { rsp.Notes = []string{"Install the CLI", "Run a service", "Chat with an agent"} return nil } type mockModel struct{ opts ai.Options } func newMock(opts ...ai.Option) ai.Model { m := &mockModel{}; _ = m.Init(opts...); return m } func (m *mockModel) Init(opts ...ai.Option) error { for _, o := range opts { o(&m.opts) }; return nil } func (m *mockModel) Options() ai.Options { return m.opts } func (m *mockModel) String() string { return "first-agent-cli-fixture" } func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) { return nil, fmt.Errorf("stream unsupported") } func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) { for _, tool := range req.Tools { if strings.Contains(tool.Name, "List") && m.opts.ToolHandler != nil { m.opts.ToolHandler(ctx, ai.ToolCall{ID:"list-notes", Name: tool.Name, Input: map[string]any{}}); break } } return &ai.Response{Answer: "assistant: your first agent should install the CLI, run a service, then chat with an agent."}, nil } func main() { ai.Register("first-agent-cli-fixture", newMock) home, _ := os.UserHomeDir() st := store.NewFileStore(store.DirOption(filepath.Join(home, "micro", "store"))) defer st.Close() svc := service.New(service.Name("notes"), service.Address("127.0.0.1:0")) if err := svc.Handle(&NotesService{}); err != nil { panic(err) } go func() { if err := svc.Run(); err != nil { fmt.Println(err); os.Exit(1) } }() defer svc.Server().Stop() a := agent.New(agent.Name("assistant"), agent.Address("127.0.0.1:0"), agent.Services("notes"), agent.Provider("first-agent-cli-fixture"), agent.WithStore(st)) go func() { if err := a.Run(); err != nil { fmt.Println(err); os.Exit(1) } }() defer a.Stop() fmt.Println("first-agent fixture ready") stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop } ` } func seedNoSecretAgentDebuggingState(t *testing.T, st store.Store) { t.Helper() scoped := store.Scope(st, "agent", "assistant") runID := "run-debug-smoke" events := []goagent.RunEvent{ {Time: time.Unix(1700000000, 0), RunID: runID, Agent: "assistant", TraceID: "trace-debug-smoke", Kind: "run", Name: "ask"}, {Time: time.Unix(1700000001, 0), RunID: runID, Agent: "assistant", TraceID: "trace-debug-smoke", Kind: "model", Provider: "mock", Model: "first-agent-mock"}, {Time: time.Unix(1700000002, 0), RunID: runID, Agent: "assistant", TraceID: "trace-debug-smoke", Kind: "done", Name: "answer"}, } for _, event := range events { b, err := json.Marshal(event) if err != nil { t.Fatal(err) } key := "runs/" + event.RunID + "/" + event.Time.Format("20060102150405.000000000") + "-" + event.Kind if err := scoped.Write(&store.Record{Key: key, Value: b}); err != nil { t.Fatalf("seed run event: %v", err) } } mem := goagent.NewMemory(scoped, "history", 10) mem.Add("user", "Triage ticket-1 for Alice") mem.Add("assistant", "ticket-1 is ready for Alice without provider secrets") } func buildMicroBinary(t *testing.T, root string) string { t.Helper() bin := filepath.Join(t.TempDir(), "micro") cmd := exec.Command("go", "build", "-o", bin, "./cmd/micro") cmd.Dir = root out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("build micro CLI failed: %v\n%s", err, out) } return bin } func runMicroCLIWithHome(t *testing.T, micro, home string, args ...string) string { t.Helper() cmd := exec.Command(micro, args...) cmd.Env = microCLIEnv(home) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("micro %s failed: %v\n%s", strings.Join(args, " "), err, out) } return string(out) } func microCLIEnv(home string) []string { return append(os.Environ(), "HOME="+home, "MICRO_AI_API_KEY=", "OPENAI_API_KEY=", "ANTHROPIC_API_KEY=", "GEMINI_API_KEY=", ) } func TestFirstAgentWayfindingTargetsExist(t *testing.T) { root := filepath.Clean(filepath.Join("..", "..", "..")) for _, target := range []string{ "examples/README.md", "examples/first-agent/README.md", "internal/website/docs/examples/index.md", "internal/website/docs/guides/no-secret-first-agent.md", "internal/website/docs/guides/your-first-agent.md", "internal/website/docs/guides/debugging-agents.md", "internal/website/docs/guides/zero-to-hero.md", } { if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(target))); err != nil { t.Fatalf("first-agent wayfinding target %s disappeared: %v", target, err) } } } func firstMarkdownSection(t *testing.T, doc, heading string) string { t.Helper() start := strings.Index(doc, heading) if start == -1 { t.Fatalf("missing %q section", heading) } section := doc[start+len(heading):] if next := strings.Index(section, "\n##"); next != -1 { section = section[:next] } return section } func readFile(t *testing.T, name string) string { t.Helper() data, err := os.ReadFile(name) if err != nil { t.Fatalf("read %s: %v", name, err) } return string(data) } var markdownLinkRE = regexp.MustCompile(`\[[^\]]+\]\(([^)#?]+)(?:[#?][^)]*)?\)`) func markdownLinks(section string) []string { matches := markdownLinkRE.FindAllStringSubmatch(section, -1) links := make([]string, 0, len(matches)) for _, match := range matches { if len(match) > 1 { links = append(links, match[1]) } } return links } func containsWayfindingMarker(section, marker string) bool { if strings.Contains(section, marker) { return true } switch marker { case "examples/INDEX.md": return strings.Contains(section, "examples/INDEX.md") || strings.Contains(section, "Recommended adoption path") || strings.Contains(section, "examples wayfinding index") || strings.Contains(section, "Examples wayfinding index") || strings.Contains(section, "./INDEX.md") case "examples/first-agent": return strings.Contains(section, "examples/first-agent") || strings.Contains(section, "./first-agent") case "examples/support": return strings.Contains(section, "examples/support") || strings.Contains(section, "./support") case "no-secret-first-agent": return strings.Contains(section, "no-secret-first-agent") || strings.Contains(section, "No-secret First Agent") case "your-first-agent": return strings.Contains(section, "your-first-agent") || strings.Contains(section, "Your First Agent") case "debugging-agents": return strings.Contains(section, "debugging-agents") || strings.Contains(section, "Debugging your agent") case "zero-to-hero": return strings.Contains(section, "zero-to-hero") || strings.Contains(section, "0→hero") default: if strings.HasPrefix(marker, "micro inspect agent ") { return strings.Contains(section, "micro inspect agent ") } return false } } func assertWayfindingTargetExists(t *testing.T, root, sourceFile, link string) { t.Helper() if !strings.Contains(link, "/") && !strings.Contains(link, ".") { return } if strings.HasPrefix(link, "http://") || strings.HasPrefix(link, "https://") { switch { case strings.HasPrefix(link, "https://go-micro.dev/docs/"): link = strings.TrimPrefix(link, "https://go-micro.dev/docs/") link = filepath.ToSlash(filepath.Join("internal", "website", "docs", strings.TrimSuffix(link, ".html")+".md")) case strings.HasPrefix(link, "https://github.com/micro/go-micro/tree/master/"): link = strings.TrimPrefix(link, "https://github.com/micro/go-micro/tree/master/") default: return } } else if strings.HasSuffix(link, ".html") { sourceDir := filepath.Dir(sourceFile) websiteDocs := filepath.Join(root, "internal", "website", "docs") resolved := filepath.Clean(filepath.Join(sourceDir, filepath.FromSlash(link))) if rel, err := filepath.Rel(websiteDocs, resolved); err == nil && !strings.HasPrefix(rel, "..") { link = filepath.ToSlash(filepath.Join("internal", "website", "docs", strings.TrimSuffix(rel, ".html")+".md")) } } else if strings.HasPrefix(link, ".") { target := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), filepath.FromSlash(link))) if _, err := os.Stat(target); err != nil { t.Fatalf("first-agent wayfinding link %q in %s resolves to missing target %s: %v", link, sourceFile, target, err) } return } target := filepath.Join(root, filepath.FromSlash(link)) if _, err := os.Stat(target); err != nil { t.Fatalf("first-agent wayfinding link %q in %s resolves to missing target %s: %v", link, sourceFile, target, err) } } func assertOrderedMarkers(t *testing.T, name, doc string, markers []string) { t.Helper() last := -1 for _, marker := range markers { idx := strings.Index(doc, marker) if idx == -1 { t.Fatalf("%s missing lifecycle command marker %q", name, marker) } if idx < last { t.Fatalf("%s marker %q appeared out of order; keep scaffold → run → chat → inspect → deploy discoverable", name, marker) } last = idx } }