package zerotoheroci import ( "encoding/json" "os" "os/exec" "path/filepath" "regexp" "strings" "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 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' -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' -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 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 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", "micro agent demo", "micro examples", "micro zero-to-hero", "internal/website/docs/guides/no-secret-first-agent.md", "internal/website/docs/guides/your-first-agent.md", "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{ "micro agent demo", "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", "micro agent demo", "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", "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 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", "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", "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 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 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 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 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 = append(os.Environ(), "HOME="+home, "MICRO_AI_API_KEY=", "OPENAI_API_KEY=", "ANTHROPIC_API_KEY=", "GEMINI_API_KEY=", ) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("micro %s failed: %v\n%s", strings.Join(args, " "), err, out) } return string(out) } 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 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 } }