Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func newCheckAgentIdentityRoles(deps Dependencies) Check {
if priorBlocked(prior, "local.agent-service-detected") {
return Result{
Status: StatusSkip,
Message: "skipped: no `azure.ai.agent` service in " +
Message: "skipped: no `microsoft.foundry` service in " +
"azure.yaml (see check " +
"`local.agent-service-detected`).",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func newCheckAgentStatus(deps Dependencies) Check {
if priorBlocked(prior, "local.agent-service-detected") {
return Result{
Status: StatusSkip,
Message: "skipped: no `azure.ai.agent` service in " +
Message: "skipped: no `microsoft.foundry` service in " +
"azure.yaml (see check " +
"`local.agent-service-detected`).",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,11 @@ type foundryConnectionsProbeFn func(
) ([]string, error)

// newCheckConnections produces Check `remote.connections` (P5.1
// C15). For each `ConnectionResource` declared in any service's
// `agent.manifest.yaml` (collected by the C2 manifest walker), the
// check queries the Foundry project's connection list and verifies a
// connection with the matching name exists. The check Passes when
// every manifest-declared connection has a corresponding entry;
// Fails when one or more are missing.
// C15). For each connection declared in any `microsoft.foundry`
// service's `connections` list in azure.yaml, the check queries the
// Foundry project's connection list and verifies a connection with the
// matching name exists. The check Passes when every declared connection
// has a corresponding entry; Fails when one or more are missing.
//
// # Skip cascade
//
Expand Down Expand Up @@ -135,7 +134,7 @@ func newCheckConnections(deps Dependencies) Check {
if !state.HasConnections {
return Result{
Status: StatusSkip,
Message: "skipped: no connection resources declared in any service's agent.manifest.yaml.",
Message: "skipped: no connection resources declared in any microsoft.foundry service in azure.yaml.",
}
}

Expand Down Expand Up @@ -288,10 +287,10 @@ func classifyConnections(
return Result{
Status: StatusFail,
Message: fmt.Sprintf(
"%d connection(s) referenced by agent.manifest.yaml are missing on project %s: %s",
"%d connection(s) referenced by azure.yaml are missing on project %s: %s",
len(missing), project, sb.String()),
Suggestion: "Run `azd provision` to create the missing connection(s), " +
"or update the agent.manifest.yaml `resources[].name` entries to " +
"or update the `connections[].name` entries in azure.yaml to " +
"match connections that already exist on the Foundry project.",
Details: map[string]any{
"missingConnections": missing,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,19 @@ type Dependencies struct {
}

// NewLocalChecks returns the canonical sequence of local doctor checks
// in execution order. Phase 4.2 covered checks 1-3; Phase 4.3 added
// checks 4-6 (agent service detected, project endpoint set, agent.yaml
// valid). Phase 5 C9 appends check 7 (manual env vars set). Phase 5
// C14 appends check 8 (`local.toolboxes`) which reads per-toolbox MCP
// endpoint env vars; it is local because it does not call ARM /
// Foundry (only the active azd environment).
// in execution order: gRPC/version, azure.yaml present, environment
// selected, agent service detected, project endpoint set, manual env
// vars, and toolbox endpoints. The unified design removed the standalone
// `agent.yaml` file, so the former per-service `local.agent-yaml-valid`
// check no longer applies — structural validity is covered by
// `local.azure-yaml`.
func NewLocalChecks(deps Dependencies) []Check {
return []Check{
newCheckGRPCAndVersion(deps),
newCheckProjectConfig(deps),
newCheckEnvironmentSelected(deps),
newCheckAgentServiceDetected(deps),
newCheckProjectEndpointSet(deps),
newCheckAgentYAMLValid(deps),
newCheckManualEnvVars(deps),
newCheckToolboxes(deps),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ func TestNewLocalChecks_OrderAndIDs(t *testing.T) {
t.Parallel()

checks := NewLocalChecks(Dependencies{})
require.Len(t, checks, 8)
require.Len(t, checks, 7)

want := []struct {
id string
Expand All @@ -455,9 +455,8 @@ func TestNewLocalChecks_OrderAndIDs(t *testing.T) {
{"local.environment-selected", "azd environment selected", false},
{"local.agent-service-detected", "agent service in azure.yaml", false},
{"local.project-endpoint-set", "FOUNDRY_PROJECT_ENDPOINT set", false},
{"local.agent-yaml-valid", "agent.yaml valid (per service)", false},
{"local.manual-env-vars", "manual env vars set", false},
{"local.toolboxes", "Manifest toolboxes have endpoint env vars set", false},
{"local.toolboxes", "Toolboxes have endpoint env vars set", false},
}
for i, w := range want {
require.Equal(t, w.id, checks[i].ID, "index %d", i)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import (
// "manual config values not set" diagnostic.
//
// "Manual" env vars are values referenced by `${...}` syntax inside an
// agent.yaml whose names are NOT declared as outputs of the project's
// infrastructure (Bicep / Terraform). They are operator-supplied:
// third-party API keys, model deployment names, hand-rolled connection
// strings. They have to be set in the active azd environment before
// `azd ai agent run` (local) or `azd deploy` (Azure) can resolve the
// agent.yaml — otherwise the running agent sees the literal `${KEY}`
// agent's `env` block in azure.yaml whose names are NOT declared as
// outputs of the project's infrastructure (Bicep / Terraform). They are
// operator-supplied: third-party API keys, model deployment names,
// hand-rolled connection strings. They have to be set in the active azd
// environment before `azd ai agent run` (local) or `azd deploy` (Azure)
// can resolve them — otherwise the running agent sees the literal `${KEY}`
// string and almost certainly fails on first use.
//
// The classification of "manual" vs "infra" lives in nextstep's
Expand All @@ -42,12 +42,12 @@ import (
// - deps.AzdClient is nil (gRPC channel unavailable). Check
// `local.grpc-extension` will already have failed with the actionable
// error.
// - `local.agent-yaml-valid` failed or was skipped. A broken agent.yaml
// produces an empty MissingManualVars (the classifier can't extract
// references it can't parse), which would mislead the user into
// thinking nothing was missing. This guard transitively covers the
// azure-yaml → agent-service-detected → agent-yaml-valid arm of the
// local-check chain (each step's own skip-cascade propagates here).
// - `local.agent-service-detected` failed or was skipped. With no
// Foundry service there are no agents to extract env references from,
// which would produce an empty MissingManualVars and mislead the user
// into thinking nothing was missing. This guard transitively covers
// the azure-yaml → agent-service-detected arm of the local-check
// chain (each step's own skip-cascade propagates here).
// - `local.environment-selected` failed or was skipped.
// `nextstep.AssembleState` early-exits its `detectMissingVars` block
// when no env is selected (state.go: `if project != nil && envName != ""`).
Expand All @@ -73,16 +73,16 @@ func newCheckManualEnvVars(deps Dependencies) Check {
if deps.AzdClient == nil {
return Result{Status: StatusSkip, Message: "skipped: azd extension not reachable"}
}
if priorBlocked(prior, "local.agent-yaml-valid") {
return Result{Status: StatusSkip, Message: "skipped: agent.yaml check failed or skipped"}
if priorBlocked(prior, "local.agent-service-detected") {
return Result{Status: StatusSkip, Message: "skipped: no microsoft.foundry service detected or upstream check blocked"}
}
if priorBlocked(prior, "local.environment-selected") {
// Without an azd env, AssembleState's detectMissingVars
// block is skipped (state.go:258), so MissingManualVars
// would be empty and the check would falsely Pass.
return Result{
Status: StatusSkip,
Message: "skipped: no azd environment selected (cannot resolve agent.yaml variables)",
Message: "skipped: no azd environment selected (cannot resolve azure.yaml variables)",
}
}

Expand Down Expand Up @@ -132,7 +132,7 @@ func newCheckManualEnvVars(deps Dependencies) Check {
return Result{
Status: StatusFail,
Message: fmt.Sprintf(
"%d manual env var(s) referenced by agent.yaml are not set in the azd environment: %s",
"%d manual env var(s) referenced by azure.yaml are not set in the azd environment: %s",
len(missing), strings.Join(missing, ", ")),
Suggestion: suggestion,
Details: map[string]any{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestCheckManualEnvVars_NoClient_Skips(t *testing.T) {
require.Contains(t, got.Message, "azd extension not reachable")
}

func TestCheckManualEnvVars_PriorAgentYAMLFailed_Skips(t *testing.T) {
func TestCheckManualEnvVars_PriorAgentDetectionFailed_Skips(t *testing.T) {
t.Parallel()

client := newTestAzdClient(t, &fakeProjectServer{}, &fakeEnvironmentServer{})
Expand All @@ -53,21 +53,21 @@ func TestCheckManualEnvVars_PriorAgentYAMLFailed_Skips(t *testing.T) {
// here asserts the cascade short-circuits before the
// assembler is reached.
assembleState: func(context.Context, *azdext.AzdClient) (*nextstep.State, []error) {
t.Fatalf("assembler should not be called when local.agent-yaml-valid failed")
t.Fatalf("assembler should not be called when local.agent-service-detected failed")
return nil, nil
},
})

prior := []Result{{ID: "local.agent-yaml-valid", Status: StatusFail}}
prior := []Result{{ID: "local.agent-service-detected", Status: StatusFail}}
got := check.Fn(t.Context(), Options{}, prior)

require.Equal(t, StatusSkip, got.Status)
require.Contains(t, got.Message, "agent.yaml check failed")
require.Contains(t, got.Message, "no microsoft.foundry service detected")
}

func TestCheckManualEnvVars_PriorAgentYAMLSkipped_AlsoSkips(t *testing.T) {
func TestCheckManualEnvVars_PriorAgentDetectionSkipped_AlsoSkips(t *testing.T) {
// Covers the cascade: a deeper upstream (e.g. azure-yaml) failed,
// agent-yaml-valid was therefore skipped, and this check must
// agent-service-detected was therefore skipped, and this check must
// inherit the skip rather than running on a half-loaded project.
// Without this propagation users would see a misleading
// "no manual env vars are missing" Pass underneath the real bug.
Expand All @@ -82,7 +82,7 @@ func TestCheckManualEnvVars_PriorAgentYAMLSkipped_AlsoSkips(t *testing.T) {
},
})

prior := []Result{{ID: "local.agent-yaml-valid", Status: StatusSkip}}
prior := []Result{{ID: "local.agent-service-detected", Status: StatusSkip}}
got := check.Fn(t.Context(), Options{}, prior)

require.Equal(t, StatusSkip, got.Status)
Expand Down Expand Up @@ -288,11 +288,9 @@ func TestCheckManualEnvVars_NonFatalErrorsButStateOK_Passes(t *testing.T) {
}

func TestNewLocalChecks_IncludesManualEnvVarsLast(t *testing.T) {
// Pin C9's insertion point: the manual-env-vars check must follow
// agent-yaml-valid so its skip-cascade against the upstream chain
// is exercised by the runner's prior-results slice. Locks the
// ordering invariant that the design's "checks 1-7" table relies
// on for failure-cascade coherence.
// Pin the manual-env-vars insertion point: it must follow
// agent-service-detected so its skip-cascade against the upstream
// chain is exercised by the runner's prior-results slice.
t.Parallel()

checks := NewLocalChecks(Dependencies{})
Expand All @@ -304,17 +302,17 @@ func TestNewLocalChecks_IncludesManualEnvVarsLast(t *testing.T) {
}
require.Contains(t, ids, "local.manual-env-vars")

var yamlIdx, manualIdx int = -1, -1
var detectIdx, manualIdx int = -1, -1
for i, id := range ids {
switch id {
case "local.agent-yaml-valid":
yamlIdx = i
case "local.agent-service-detected":
detectIdx = i
case "local.manual-env-vars":
manualIdx = i
}
}
require.NotEqual(t, -1, yamlIdx, "agent-yaml-valid must be in NewLocalChecks")
require.NotEqual(t, -1, detectIdx, "agent-service-detected must be in NewLocalChecks")
require.NotEqual(t, -1, manualIdx, "manual-env-vars must be in NewLocalChecks")
require.Greater(t, manualIdx, yamlIdx,
"manual-env-vars must come after agent-yaml-valid for the skip-cascade")
require.Greater(t, manualIdx, detectIdx,
"manual-env-vars must come after agent-service-detected for the skip-cascade")
}
Loading
Loading