Compare commits

..

11 Commits

Author SHA1 Message Date
Codex d97c981bcc docs: refresh planner priorities for 4331
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
2026-07-08 09:05:05 +00:00
Asim Aslam 7734f5605d Verify zero-to-hero deploy dry-run (#4330)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 09:26:45 +01:00
Asim Aslam affd61e69b docs: refresh coherence changelog (#4326)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 08:57:36 +01:00
Asim Aslam 171da77817 docs: refresh planner priorities for 4319 (#4320)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 08:03:31 +01:00
Asim Aslam 83a6b2004a Propagate provider HTTP retry signals (#4318)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 06:43:28 +01:00
Asim Aslam ae5ead7914 docs: refresh planner priorities for 4314 (#4316)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 06:16:36 +01:00
Asim Aslam a1d17ec984 Avoid recording failed stream turns (#4313)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 04:57:10 +01:00
Asim Aslam a8b89c2142 docs: refresh planner priorities for 4308 (#4310)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 04:06:25 +01:00
Asim Aslam 76fea3f01c agent: parse function-style text tool calls (#4307)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 03:29:12 +01:00
Asim Aslam a5f8ff518d docs: refresh planner priorities for 4304 (#4305)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 02:40:13 +01:00
Asim Aslam cf9c015cf8 ci: add docs wayfinding guard (#4303)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 02:12:09 +01:00
19 changed files with 391 additions and 23 deletions
+3 -4
View File
@@ -21,10 +21,9 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **CI-verify first-agent docs wayfinding links** ([#4291](https://github.com/micro/go-micro/issues/4291)) — Developer adoption is the current goal, and the README/website/examples now tell a coherent first-agent path: install troubleshooting → `micro agent demo`/`micro examples`/`micro zero-to-hero` → examples index → no-secret first agent debugging → 0→hero. The highest-value next guard is preventing that on-ramp from drifting. Keep this scoped to a local-file check wired into an existing harness or Make target, with no new runtime dependencies.
2. **Fix AtlasCloud agent conformance guarded delegation** ([#4296](https://github.com/micro/go-micro/issues/4296)) — The plan-delegate notify recovery shipped in #4299, but the latest live provider conformance run exposed a separate AtlasCloud agent harness failure: missing guarded delegate behavior within the retry budget. This is the top remaining Now-phase reliability defect because guarded delegation is core to services → agents handoff safety and should be CI-verifiable without public API changes.
3. **Fix AtlasCloud A2A streaming fallback tool-call reporting** ([#4297](https://github.com/micro/go-micro/issues/4297)) — The same live provider run found the A2A streaming fallback path reporting neither tool-call evidence nor run metadata (`tool=false runInfo=false`). Rank it just behind guarded delegation: A2A streaming is an important interop seam, but the issue is narrower than the base agent conformance failure and should stay focused on surfacing tool/run evidence in the existing harness.
4. **Propagate cancellation and retry signals through provider model calls** ([#4273](https://github.com/micro/go-micro/issues/4273)) — After the adoption link guard and current live AtlasCloud failures are stable, the remaining Now-phase resilience seam is provider-boundary cancellation/deadline/retry behavior across the agent loop. Keep it focused on fakes/tests and avoid public API or architecture changes.
1. **Make agent-flow fail on missing onboarding side effects** ([#4328](https://github.com/micro/go-micro/issues/4328)) — Now-phase hardening and developer trust both depend on the 0→hero/provider harness failing when the services → agents → workflows contract is not actually satisfied. The recent deploy dry-run verifier closed the documented deploy-boundary gap; the next highest-value gap is preventing a live-provider agent-flow run from printing success while required workspace/notification side effects are absent.
2. **Stabilize plan-delegate after unfinished delegation** ([#4322](https://github.com/micro/go-micro/issues/4322)) — This is the remaining Now-phase conformance failure in the multi-agent 0→hero path: real services and durable flow state can advance, but the delegated notification step may remain unfinished after partial side effects. Stabilizing that recovery keeps plan/delegate from being a brittle demo seam and protects the service → agent → workflow lifecycle under live-provider variance.
3. **Trace agent RunInfo in OpenTelemetry spans** ([#4315](https://github.com/micro/go-micro/issues/4315)) — Once the live harness failures above fail deterministically and recover reliably, the highest Next-phase operability gap is connecting existing run metadata to traces so real agent runs can be debugged across steps, tool calls, delegation, failures, services, and flows without inventing a new surface.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
+25
View File
@@ -16,12 +16,37 @@ next version when it ships.
## [Unreleased]
### Added
- **Provider HTTP retry signals** — provider failures now preserve HTTP status and `Retry-After` details so retry classification and backoff can respond to rate limits and unavailable providers. (`ai/`)
### Fixed
- **Stream fallback memory** — unsupported streaming attempts no longer leave stale duplicate user turns before fallback paths continue with non-streaming agent calls. (`agent/`)
- **Function-style text tool calls** — agent fallback parsing now recognizes provider replies that render tools as function-style calls, including nested JSON arguments. (`agent/`)
- **Plan/delegate notify recovery** — plan-delegate recovery now waits for recovered notify side effects and routes retries through the communications agent that owns the notification. (`internal/harness/`)
### Documentation
- **First-agent docs wayfinding guard** — the local harness now includes a focused no-network check for first-agent and 0→hero docs links. (`Makefile`, `internal/harness/`)
---
## [6.3.18] - July 2026
### Added
- **StreamAsk close cancellation** — agent streaming calls now cancel promptly when their runner closes, avoiding orphaned stream work. (`agent/`)
- **Agent resume pending helper** — agent durability now has a focused helper for resuming pending checkpointed runs. (`agent/`)
- **Agent tool retry tracing** — agent traces now include tool retry attempts for easier debugging of retry/fallback behavior. (`agent/`)
- **Shared-broker universe harness** — the universe harness now runs against the shared broker path, improving coverage of the same runtime wiring used by services, agents, and workflows. (`internal/harness/`)
### Fixed
- **Plan/delegate retry idempotency** — agent retries now preserve side-effect and notification dedupe across conformance retry paths, including completion and owner-notification edge cases. (`agent/`, `internal/harness/`)
- **AtlasCloud text tool calls** — AtlasCloud fallback handling now recovers more text-rendered tool calls from OpenAI-compatible responses. (`ai/atlascloud/`, `agent/`)
- **OpenAI-compatible text tool calls** — OpenAI-compatible providers now recover text-rendered tool calls more reliably. (`agent/`)
- **AtlasCloud multi-step follow-ups** — AtlasCloud tool fallback handling now continues multi-step tool follow-up paths more reliably. (`ai/atlascloud/`, `agent/`)
### Documentation
- **Agent debugging quickcheck** — docs now include a focused quickcheck path for first-agent debugging. (`internal/website/docs/`)
- **Website first-agent examples map** — website docs now link the maintained examples wayfinding map for the first-agent route. (`internal/website/docs/`)
- **Examples wayfinding index** — examples docs now provide a central map for first-agent, support, and interop examples. (`examples/`, `internal/website/docs/`)
---
+4 -2
View File
@@ -227,16 +227,18 @@ func (a *agentImpl) Stream(ctx context.Context, message string) (ai.Stream, erro
if err != nil {
return nil, fmt.Errorf("discover tools: %w", err)
}
a.mem.Add("user", message)
messages := append([]ai.Message(nil), a.mem.Messages()...)
messages = append(messages, ai.Message{Role: "user", Content: message})
stream, err := a.model.Stream(ctx, &ai.Request{
Prompt: message,
SystemPrompt: a.buildPrompt(),
Tools: toolList,
Messages: a.mem.Messages(),
Messages: messages,
})
if err != nil {
return nil, err
}
a.mem.Add("user", message)
return &memoryRecordingStream{stream: stream, memory: a.mem}, nil
}
+23
View File
@@ -243,6 +243,29 @@ func TestResumeStreamAskDoesNotReplayCompletedTool(t *testing.T) {
}
}
func TestAgentStreamDoesNotRecordUserWhenProviderStreamingUnsupported(t *testing.T) {
fakeStream = func(ctx context.Context, opts ai.Options, req *ai.Request) (ai.Stream, error) {
if len(req.Messages) == 0 || req.Messages[len(req.Messages)-1].Role != "user" || req.Messages[len(req.Messages)-1].Content != "stream fallback" {
t.Fatalf("stream request messages = %+v, want pending user message", req.Messages)
}
return nil, ai.ErrStreamingUnsupported
}
defer func() { fakeStream = nil }()
mem := NewInMemory(8)
a := newTestAgent(Name("stream-fallback"), WithMemory(mem), WithTool("echo", "echo text", nil, func(context.Context, map[string]any) (string, error) {
return "ok", nil
}))
_, err := a.Stream(context.Background(), "stream fallback")
if !errors.Is(err, ai.ErrStreamingUnsupported) {
t.Fatalf("Stream error = %v, want ErrStreamingUnsupported", err)
}
if got := mem.Messages(); len(got) != 0 {
t.Fatalf("memory after unsupported stream = %+v, want no recorded messages", got)
}
}
type unsupportedAgent struct{}
func (unsupportedAgent) Name() string { return "unsupported" }
+122
View File
@@ -102,6 +102,9 @@ func parseTextToolCalls(text string, tools []ai.Tool) []ai.ToolCall {
if calls := decodeTaggedTextToolCalls(text, allowed); len(calls) > 0 {
return calls
}
if calls := decodeFunctionTextToolCalls(text, allowed); len(calls) > 0 {
return calls
}
for _, candidate := range jsonCandidates(text) {
if calls := decodeTextToolCalls(candidate, allowed); len(calls) > 0 {
return calls
@@ -268,6 +271,125 @@ func taggedToolName(tag string) string {
return strings.Trim(match[1], `"'`)
}
func decodeFunctionTextToolCalls(text string, allowed map[string]string) []ai.ToolCall {
var out []ai.ToolCall
for alias, canonical := range allowed {
for _, body := range functionCallBodies(text, alias) {
var input map[string]any
if err := json.Unmarshal([]byte(body), &input); err != nil || input == nil {
continue
}
out = append(out, ai.ToolCall{
ID: fmt.Sprintf("text-call-%s", strings.ReplaceAll(alias, ".", "_")),
Name: canonical,
Input: input,
})
}
}
return out
}
func functionCallBodies(text, name string) []string {
if name == "" {
return nil
}
var bodies []string
for searchFrom := 0; searchFrom < len(text); {
idx := strings.Index(text[searchFrom:], name)
if idx < 0 {
break
}
start := searchFrom + idx
open := start + len(name)
if !isFunctionCallBoundary(text, start, open) {
searchFrom = start + len(name)
continue
}
bodyStart := open + 1
bodyEnd, ok := balancedJSONObjectEnd(text, bodyStart)
if !ok {
searchFrom = bodyStart
continue
}
bodies = append(bodies, strings.TrimSpace(text[bodyStart:bodyEnd]))
searchFrom = bodyEnd + 1
}
return bodies
}
func isFunctionCallBoundary(text string, start, open int) bool {
if open >= len(text) || text[open] != '(' {
return false
}
if start > 0 {
prev := text[start-1]
if prev == '_' || prev == '.' || prev == '-' || prev == '$' || ('0' <= prev && prev <= '9') || ('A' <= prev && prev <= 'Z') || ('a' <= prev && prev <= 'z') {
return false
}
}
for i := open + 1; i < len(text); i++ {
switch text[i] {
case ' ', '\n', '\r', '\t':
continue
case '{':
return true
default:
return false
}
}
return false
}
func balancedJSONObjectEnd(text string, start int) (int, bool) {
for start < len(text) {
switch text[start] {
case ' ', '\n', '\r', '\t':
start++
case '{':
depth := 0
inString := false
escaped := false
for i := start; i < len(text); i++ {
c := text[i]
if inString {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
inString = false
}
continue
}
switch c {
case '"':
inString = true
case '{':
depth++
case '}':
depth--
if depth == 0 {
for j := i + 1; j < len(text); j++ {
switch text[j] {
case ' ', '\n', '\r', '\t':
continue
case ')':
return i + 1, true
default:
return 0, false
}
}
}
}
}
return 0, false
default:
return 0, false
}
}
return 0, false
}
func firstNestedToolCalls(m map[string]any) (any, bool) {
for _, key := range []string{"tool_calls", "toolCalls", "calls"} {
if v, ok := m[key]; ok {
+39
View File
@@ -107,3 +107,42 @@ func TestParseTextToolCallsHTMLEscapedTaggedMarkup(t *testing.T) {
t.Fatalf("task = %v, want summarize the conformance marker", got)
}
}
func TestParseTextToolCallsFunctionCallSyntax(t *testing.T) {
tools := []ai.Tool{{Name: "delegate"}}
reply := `I will now call delegate({"task":"summarize the conformance marker","to":"blocked-reviewer"}) before answering.`
calls := parseTextToolCalls(reply, tools)
if len(calls) != 1 {
t.Fatalf("parseTextToolCalls returned %d calls, want 1: %+v", len(calls), calls)
}
if calls[0].Name != "delegate" {
t.Fatalf("call name = %q, want delegate", calls[0].Name)
}
if got := calls[0].Input["task"]; got != "summarize the conformance marker" {
t.Fatalf("task = %v, want summarize the conformance marker", got)
}
if got := calls[0].Input["to"]; got != "blocked-reviewer" {
t.Fatalf("to = %v, want blocked-reviewer", got)
}
}
func TestParseTextToolCallsFunctionCallSyntaxHandlesNestedJSON(t *testing.T) {
tools := []ai.Tool{{Name: "delegate"}}
reply := `delegate({
"task":"summarize the {escaped} marker",
"meta":{"note":"paren ) and brace } in string"},
"to":"blocked-reviewer"
})`
calls := parseTextToolCalls(reply, tools)
if len(calls) != 1 {
t.Fatalf("parseTextToolCalls returned %d calls, want 1: %+v", len(calls), calls)
}
if got := calls[0].Input["task"]; got != "summarize the {escaped} marker" {
t.Fatalf("task = %v, want nested JSON-safe task", got)
}
if got := calls[0].Input["to"]; got != "blocked-reviewer" {
t.Fatalf("to = %v, want blocked-reviewer", got)
}
}
+1 -1
View File
@@ -297,7 +297,7 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
return nil, nil, ai.NewHTTPError(httpResp, respBody)
}
// Parse response
+28 -8
View File
@@ -327,17 +327,32 @@ func (s *atlasStream) Close() error {
}
type atlascloudAPIError struct {
Status string
StatusCode int
Phase string
Summary string
Body string
Status string
Code int
Retry time.Duration
Phase string
Summary string
Body string
}
func (e *atlascloudAPIError) Error() string {
return fmt.Sprintf("API error (%s) during atlascloud %s request (%s): %s", e.Status, e.Phase, e.Summary, e.Body)
}
func (e *atlascloudAPIError) StatusCode() int {
if e == nil {
return 0
}
return e.Code
}
func (e *atlascloudAPIError) RetryAfter() time.Duration {
if e == nil {
return 0
}
return e.Retry
}
func (p *Provider) callAPI(ctx context.Context, phase string, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
@@ -361,7 +376,12 @@ func (p *Provider) callAPI(ctx context.Context, phase string, req map[string]any
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, &atlascloudAPIError{Status: httpResp.Status, StatusCode: httpResp.StatusCode, Phase: phase, Summary: atlascloudRequestSummary(req), Body: string(respBody)}
retryAfter := time.Duration(0)
var retryErr interface{ RetryAfter() time.Duration }
if errors.As(ai.NewHTTPError(httpResp, respBody), &retryErr) {
retryAfter = retryErr.RetryAfter()
}
return nil, nil, &atlascloudAPIError{Status: httpResp.Status, Code: httpResp.StatusCode, Retry: retryAfter, Phase: phase, Summary: atlascloudRequestSummary(req), Body: string(respBody)}
}
var chatResp struct {
@@ -439,7 +459,7 @@ func atlascloudShouldRetryMinimaxCompat(err error, compatTools []map[string]any)
return false
}
var apiErr *atlascloudAPIError
return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusBadRequest
return errors.As(err, &apiErr) && apiErr.StatusCode() == http.StatusBadRequest
}
func atlascloudShouldRetryWithoutTools(err error, req map[string]any) bool {
@@ -447,7 +467,7 @@ func atlascloudShouldRetryWithoutTools(err error, req map[string]any) bool {
return false
}
var apiErr *atlascloudAPIError
return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusBadRequest
return errors.As(err, &apiErr) && apiErr.StatusCode() == http.StatusBadRequest
}
func atlascloudTools(input []ai.Tool) []map[string]any {
+1 -1
View File
@@ -163,7 +163,7 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
return nil, nil, ai.NewHTTPError(httpResp, respBody)
}
var geminiResp struct {
+1 -1
View File
@@ -147,7 +147,7 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
return nil, nil, ai.NewHTTPError(httpResp, respBody)
}
var chatResp struct {
+1 -1
View File
@@ -147,7 +147,7 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
return nil, nil, ai.NewHTTPError(httpResp, respBody)
}
var chatResp struct {
+1 -1
View File
@@ -147,7 +147,7 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
return nil, nil, ai.NewHTTPError(httpResp, respBody)
}
var chatResp struct {
+1 -1
View File
@@ -285,7 +285,7 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
return nil, nil, ai.NewHTTPError(httpResp, respBody)
}
// Parse response
+60
View File
@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
@@ -19,6 +21,64 @@ type RetryAfterCoder interface {
RetryAfter() time.Duration
}
// HTTPError describes a failed provider HTTP response while preserving the
// status code and Retry-After signal for retry classifiers.
type HTTPError struct {
Status string
Code int
Body string
Header http.Header
}
func (e *HTTPError) Error() string {
if e == nil {
return ""
}
return fmt.Sprintf("API error (%s): %s", e.Status, e.Body)
}
func (e *HTTPError) StatusCode() int {
if e == nil {
return 0
}
return e.Code
}
func (e *HTTPError) RetryAfter() time.Duration {
if e == nil {
return 0
}
return parseRetryAfter(e.Header.Get("Retry-After"), time.Now())
}
func NewHTTPError(resp *http.Response, body []byte) error {
if resp == nil {
return errors.New("API error: nil response")
}
return &HTTPError{Status: resp.Status, Code: resp.StatusCode, Body: string(body), Header: resp.Header.Clone()}
}
func parseRetryAfter(value string, now time.Time) time.Duration {
value = strings.TrimSpace(value)
if value == "" {
return 0
}
if seconds, err := strconv.Atoi(value); err == nil {
if seconds <= 0 {
return 0
}
return time.Duration(seconds) * time.Second
}
when, err := http.ParseTime(value)
if err != nil {
return 0
}
if delay := when.Sub(now); delay > 0 {
return delay
}
return 0
}
// ErrorKind classifies provider-boundary failures into stable buckets callers
// can inspect without parsing provider-specific error strings.
type ErrorKind string
+21
View File
@@ -3,6 +3,7 @@ package ai
import (
"context"
"errors"
"net/http"
"testing"
"time"
)
@@ -221,3 +222,23 @@ func TestGenerateWithRetryCapsRetryAfter(t *testing.T) {
t.Fatalf("retryBackoff() = %s, want 30s cap", got)
}
}
func TestHTTPErrorExposesStatusAndRetryAfter(t *testing.T) {
resp := &http.Response{
Status: "429 Too Many Requests",
StatusCode: http.StatusTooManyRequests,
Header: http.Header{"Retry-After": []string{"2"}},
}
err := NewHTTPError(resp, []byte("slow down"))
if got := ClassifyError(err); got != ErrorKindRateLimited {
t.Fatalf("ClassifyError() = %q, want %q", got, ErrorKindRateLimited)
}
var retryAfter RetryAfterCoder
if !errors.As(err, &retryAfter) {
t.Fatalf("NewHTTPError does not expose RetryAfterCoder")
}
if got := retryAfter.RetryAfter(); got != 2*time.Second {
t.Fatalf("RetryAfter() = %s, want 2s", got)
}
}
+1 -1
View File
@@ -147,7 +147,7 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
return nil, nil, ai.NewHTTPError(httpResp, respBody)
}
var chatResp struct {
@@ -62,6 +62,62 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
}
}
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"))
+1 -1
View File
@@ -9,7 +9,7 @@ cd "$ROOT"
go test ./cmd/micro/cli/new -run TestZeroToOne -count=1
go test ./cmd/micro -run 'TestFirstAgentWalkthroughCLIBoundaries|TestZeroToHeroCLIBoundaries' -count=1
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestNoSecretFirstAgentDebuggingSmoke|TestZeroToHeroReferenceDocs|TestYourFirstAgentTutorialSmoke' -count=1
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestNoSecretFirstAgentDebuggingSmoke|TestZeroToHeroReferenceDocs|TestZeroToHeroDeployDryRunCommandSmoke|TestYourFirstAgentTutorialSmoke' -count=1
# Deterministic no-secret reference scenarios. These use the real Go Micro
# runtime and mock only the LLM provider. The support example is the maintained
+2 -1
View File
@@ -23,7 +23,7 @@ cloud credentials?"
| Run | `micro run` remains the local development entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
| Chat | `micro chat` remains the interactive agent entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
| Inspect | `micro inspect agent <name>`, `micro agent history <name>`, `micro inspect flow <flow>`, and `micro flow runs <flow>` remain discoverable for run history; the no-secret debugging smoke seeds durable agent history and runs the documented inspect/history commands without provider keys. | `go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1` |
| Deploy | `micro deploy --dry-run` resolves deploy targets without touching remote infrastructure. | `go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1` |
| Deploy | `micro deploy --dry-run prod` resolves the documented deploy target without touching remote infrastructure. | `go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunCommandSmoke -count=1` |
| Smallest first agent | `examples/first-agent` runs one service-backed agent with a deterministic mock model and no provider key. | `go test ./examples/first-agent -run TestRunFirstAgent -count=1` |
| Runtime reference app | `examples/support` runs typed services, an agent using those services as tools, an event-driven flow handoff, and an approval gate with only the model mocked. | `go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1` |
| Runtime harnesses | Real services, agents, durable flows, store-backed history, delegation, and A2A run with only the model mocked. | `./internal/harness/zero-to-hero-ci/run.sh` and `make provider-conformance-mock` |
@@ -84,6 +84,7 @@ go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1
# CLI inner-loop commands: run, chat, inspect, flow runs, deploy --dry-run.
go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunCommandSmoke -count=1
# Smallest no-secret service-backed first agent.
go test ./examples/first-agent -run TestRunFirstAgent -count=1