diff --git a/solutions/ess-maker-skills/.github/prompts/setup.prompt.md b/solutions/ess-maker-skills/.github/prompts/setup.prompt.md index 1037f7bfa..f106e9080 100644 --- a/solutions/ess-maker-skills/.github/prompts/setup.prompt.md +++ b/solutions/ess-maker-skills/.github/prompts/setup.prompt.md @@ -9,10 +9,10 @@ Read `src/skills/foundation-setup/SKILL.md` first. Follow its **Command runtime* instructions to establish a working Python invocation before running any Python command. -After reading the foundation skill, write the complete maker-facing progress -checklist below. At the beginning of every subsequent setup turn, write the -same complete checklist again using the latest canonical setup state and -results observed in that invocation. Use the exact ordinary Markdown shape +After reading the foundation skill, use its explicit progress render points. +At the first interactive setup surface in a turn, write the complete +maker-facing progress checklist below using the latest canonical setup state +and results observed in that invocation. Use the exact ordinary Markdown shape defined in the foundation skill: one single-level bullet and one leading status emoji per stage. @@ -23,12 +23,11 @@ status emoji per stage. - {marker} Review the setup handoff Use ✅ for completed, 🔄 for the current stage, ⛔ for a blocked stage, and ⬜ -for pending. Every update is a full snapshot containing all five stages in this -order. After each setup action that changes progress, write the complete -snapshot with the updated statuses. Preserve completed stages, keep pending -stages present, and represent subordinate checks through the status of their -owning stage. Before every maker-facing response, including the final handoff, -synchronize the complete snapshot once more. +for pending. Every rendered update is a full snapshot containing all five +stages in this order. Render it at the first interactive surface in a turn, +when a marker changes, when a blocked state requires maker action, and in the +final handoff. A sequence of setup operations that retains the same markers +continues to its next render point without another progress snapshot. Run setup commands from the current ESS Maker Skills workspace folder. @@ -56,6 +55,10 @@ If the check fails, run: Then rerun the check. +After successful runtime, dependency, and converter checks, run the next setup +operation. When a check requires maker action, state the observed failure and +its single recovery action. + For any command failure, follow the **Command runtime** recovery guidance. Do not route to Dataverse foundation or onboarding playbooks. diff --git a/solutions/ess-maker-skills/scripts/agentbuilder.py b/solutions/ess-maker-skills/scripts/agentbuilder.py index 90564827b..f1db6d876 100644 --- a/solutions/ess-maker-skills/scripts/agentbuilder.py +++ b/solutions/ess-maker-skills/scripts/agentbuilder.py @@ -25,6 +25,8 @@ CLIENT_ID = "417219b4-3a7d-42a2-bdb1-972bd8281a02" DEFAULT_API_VERSION = "2024-10-01" +NATIVE_ALM_API_VERSION = "2022-03-01-preview" +COPILOT_STUDIO_CLIENT_NAME = "CopilotStudio" DEFAULT_TOKEN_CACHE = Path(".local/.agentbuilder_token_cache.bin") DEV_REALM = 0 TEST_REALM = 1 @@ -248,7 +250,7 @@ def list_environments( headers = { "Authorization": f"Bearer {token}", "Accept": "application/json", - "x-ms-client-name": "EssAdk", + "x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME, } url = f"{host}/environmentmanagement/environments" params: dict[str, str] | None = {"api-version": api_version} @@ -585,7 +587,7 @@ def __init__( self.headers = { "Authorization": f"Bearer {token}", "Accept": "application/json", - "x-ms-client-name": "EssAdk", + "x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME, } def list_connections(self, environment_id: str) -> list[dict[str, Any]]: @@ -651,8 +653,7 @@ def __init__( self.headers = { "Authorization": f"Bearer {token}", "Accept": "application/json", - "Content-Type": "application/json", - "x-ms-client-name": "EssAdk", + "x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME, } def _json( @@ -668,11 +669,14 @@ def _json( request_params = {"api-version": self.api_version} if params: request_params.update(params) + request_headers = dict(self.headers) + if body is not None: + request_headers["Content-Type"] = "application/json" response = self.session.request( method, f"{self.host}{path}", params=request_params, - headers=self.headers, + headers=request_headers, json=body, timeout=timeout, ) @@ -773,8 +777,8 @@ def publish_agent( response = self.session.request( "POST", f"{self.host}/copilotstudio/minimalBots/api/{agent_id}/publish", - params={"api-version": self.api_version}, - headers=self.headers, + params={"api-version": NATIVE_ALM_API_VERSION}, + headers={**self.headers, "Content-Type": "application/json"}, json={}, timeout=timeout, allow_redirects=False, @@ -835,6 +839,7 @@ def fetch_components(self, agent_id: str) -> dict[str, Any]: "POST", f"/copilotstudio/minimalBots/api/{agent_id}/components", "Component fetch", + params={"api-version": NATIVE_ALM_API_VERSION}, body={}, timeout=180, ) @@ -855,8 +860,8 @@ def update_bot_entity( return self.session.request( "PUT", f"{self.host}/copilotstudio/minimalBots/api/{agent_id}/components", - params={"api-version": self.api_version}, - headers=self.headers, + params={"api-version": NATIVE_ALM_API_VERSION}, + headers={**self.headers, "Content-Type": "application/json"}, json={"bot": bot, "botComponentChanges": []}, timeout=timeout, allow_redirects=False, @@ -871,9 +876,8 @@ def import_package( ) -> dict[str, Any]: """Import one package without replaying or exposing its response body.""" request_headers = { - name: value - for name, value in self.headers.items() - if name.casefold() != "content-type" + "Authorization": self.headers["Authorization"], + "x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME, } form = ( {"schemaName": replacement_schema_name} @@ -884,11 +888,11 @@ def import_package( response = self.session.request( "POST", f"{self.host}/copilotstudio/minimalBots/alm/import", - params={"api-version": self.api_version}, + params={"api-version": NATIVE_ALM_API_VERSION}, headers=request_headers, files={ "package": ( - package_path.name, + "package.zip", package, "application/zip", ) @@ -941,14 +945,13 @@ def export_package( ) -> None: """Export one native package to a caller-owned path.""" request_headers = { - name: value - for name, value in self.headers.items() - if name.casefold() != "content-type" + "Authorization": self.headers["Authorization"], + "x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME, } response = self.session.request( "POST", f"{self.host}/copilotstudio/minimalBots/alm/{agent_id}/export", - params={"api-version": self.api_version}, + params={"api-version": NATIVE_ALM_API_VERSION}, headers=request_headers, timeout=timeout, allow_redirects=False, @@ -1007,7 +1010,7 @@ def create_agent_from_starter_package( "POST", f"{self.host}/copilotstudio/minimalBots/createFromStarterPackage", params={"api-version": self.api_version}, - headers=self.headers, + headers={**self.headers, "Content-Type": "application/json"}, json={"packageId": package_id}, timeout=timeout, allow_redirects=False, diff --git a/solutions/ess-maker-skills/scripts/publish.py b/solutions/ess-maker-skills/scripts/publish.py index a29e468ba..243e3ad4a 100644 --- a/solutions/ess-maker-skills/scripts/publish.py +++ b/solutions/ess-maker-skills/scripts/publish.py @@ -317,7 +317,11 @@ def _publish_native( print(f" ❌ Publish failed: {error}") return 1 - if result.get("validationPending") is True: + validation_pending = result.get( + "ValidationPending", + result.get("validationPending"), + ) + if validation_pending is True: print( " ✅ Publish request accepted. Copilot Studio validation is " "still running." diff --git a/solutions/ess-maker-skills/scripts/setup_existing_da.py b/solutions/ess-maker-skills/scripts/setup_existing_da.py index a498fcaac..7d1577cd8 100644 --- a/solutions/ess-maker-skills/scripts/setup_existing_da.py +++ b/solutions/ess-maker-skills/scripts/setup_existing_da.py @@ -1171,7 +1171,8 @@ def _confirm_dev_route( realms: dict[str, Any], *, expected_schema_name: str | None, -) -> str: + allow_missing_schema: bool = False, +) -> str | None: route_realm = realms.get("routeRealm") if route_realm not in (0, "dev", "Dev"): raise ExistingDASetupError( @@ -1198,6 +1199,8 @@ def _confirm_dev_route( expected_schema = str(expected_schema_name or "").strip() schema_name = str(agent.get("schemaName") or expected_schema).strip() if not schema_name: + if allow_missing_schema: + return None raise ExistingDASetupError( "Direct agent lookup did not return a schema name." ) @@ -1220,6 +1223,7 @@ def validate_existing_dev_connection( setup_source: str = "existing-dev", require_alm_family: bool = True, expected_schema_name: str | None = None, + allow_missing_schema: bool = False, ) -> dict[str, Any]: """Validate a directly addressable agent as editable Dev identity.""" normalized_setup_source = _validate_setup_source(setup_source) @@ -1248,6 +1252,7 @@ def validate_existing_dev_connection( agent, realms, expected_schema_name=expected_schema_name, + allow_missing_schema=allow_missing_schema, ) family_id = None agent_name = str( @@ -1255,6 +1260,7 @@ def validate_existing_dev_connection( or agent.get("displayName") or agent.get("shortBotName") or schema_name + or normalized_agent_id ) managed_properties = agent.get("managedProperties") is_managed = ( @@ -1404,7 +1410,7 @@ def _validate_changeset_identity( agent_id: str, *, expected_schema_name: str | None = None, -) -> None: +) -> str: bot = changeset.get("bot") if not isinstance(bot, dict): raise ExistingDASetupError("Component fetch did not return bot identity.") @@ -1416,17 +1422,20 @@ def _validate_changeset_identity( raise ExistingDASetupError( "Component fetch returned content for a different agent." ) + fetched_schema = str(bot.get("schemaName") or "").strip() + if not fetched_schema: + raise ExistingDASetupError( + "Component fetch did not return a schema name." + ) expected_schema = str(expected_schema_name or "").strip() - if expected_schema: - fetched_schema = str(bot.get("schemaName") or "").strip() - if not fetched_schema: - raise ExistingDASetupError( - "Component fetch did not return the imported schema name." - ) - if fetched_schema.casefold() != expected_schema.casefold(): - raise ExistingDASetupError( - "Component fetch returned content for a different schema." - ) + if ( + expected_schema + and fetched_schema.casefold() != expected_schema.casefold() + ): + raise ExistingDASetupError( + "Component fetch returned content for a different schema." + ) + return fetched_schema def _materialize_workspace( @@ -1922,9 +1931,19 @@ def attach_existing_dev( agent_id=agent_id, selection_source=selection_source, setup_source=setup_source, - require_alm_family=setup_source != "alm-import", + require_alm_family=False, expected_schema_name=expected_schema_name, + allow_missing_schema=True, ) + normalized_environment_id = connection["environment"]["id"] + normalized_agent_id = connection["agent"]["id"] + prefetched_changeset: dict[str, Any] | None = None + if not connection["agent"]["schemaName"]: + prefetched_changeset = client.fetch_components(normalized_agent_id) + connection["agent"]["schemaName"] = _validate_changeset_identity( + prefetched_changeset, + normalized_agent_id, + ) canonical_state, existing_setup = _validate_setup_target( kit_root, connection, @@ -1962,19 +1981,19 @@ def attach_existing_dev( existing_setup, ) progress_recorded = True - normalized_environment_id = connection["environment"]["id"] - normalized_agent_id = connection["agent"]["id"] schema_name = connection["agent"]["schemaName"] family_id = connection["agent"].get("almFamilyId") agent_name = connection["agent"]["name"] try: - changeset = client.fetch_components(normalized_agent_id) + changeset = ( + prefetched_changeset + if prefetched_changeset is not None + else client.fetch_components(normalized_agent_id) + ) _validate_changeset_identity( changeset, normalized_agent_id, - expected_schema_name=( - schema_name if setup_source == "alm-import" else None - ), + expected_schema_name=expected_schema_name or schema_name, ) except Exception as exc: if progress_recorded: diff --git a/solutions/ess-maker-skills/src/reference/mos-starter-package.md b/solutions/ess-maker-skills/src/reference/mos-starter-package.md index be9f336df..3224048d1 100644 --- a/solutions/ess-maker-skills/src/reference/mos-starter-package.md +++ b/solutions/ess-maker-skills/src/reference/mos-starter-package.md @@ -24,6 +24,7 @@ This reference defines the safety and evidence contract for installing a new Dev | A created starter agent is not automatically opted into ALM | Live-proven | | ALM opt-in uses the full fetched `BotEntity`, adds `configuration.settings["alm.isAlmEnabled"] = true`, and submits an empty `botComponentChanges` list through `PUT /api/{agentId}/components` | Live-proven | | ALM opt-in persisted and advanced the BotEntity version on read-back | Live-proven | +| Direct Dev-route and component validation can materialize a newly created starter agent while published Dev configuration is absent | Live-proven in TEST | | Existing-Dev validation and attachment remain separate and expose service-owned prerequisites | Live-proven | | Direct native ALM import already creates agents from packages declaring `packageType: "templated"` | Live-proven, using the generic import path, not this surface | @@ -52,8 +53,8 @@ Do not describe a pending-validation claim as supported behavior. In particular, 9. End create after emitting a usable returned identity. Preserve catalog revision as `catalogPackageVersion` and the service-returned source template version as `templateVersion`; never conflate them. 10. Invoke ALM enablement only after a successful create response in the confirmed setup flow. Fetch the exact agent first, deep-copy and preserve its full `BotEntity`, change only `alm.isAlmEnabled`, and request no component changes. 11. Verify ALM through a second component fetch. A write response without persisted read-back is not success. -12. Return control after every operation. The existing `setup_existing_da.py attach` command remains the sole Dev validation, projection, and canonical-completion boundary. -13. If attachment reports a service-owned prerequisite, report it and stop. This path never publishes or removes components. +12. Return control after every operation. The existing `setup_existing_da.py attach` command remains the sole Dev validation, projection, and canonical-completion boundary. For a newly created starter agent, pass the create response's schema name and validate the direct Dev route plus fetched component identity without requiring published Dev configuration. +13. If attachment reports a service-owned prerequisite, report it and stop. Publishing is not attachment remediation for this path; foundation setup never publishes or removes components. 14. Keep response bodies, internal classifications, step IDs, and request details as diagnostic evidence. Translate supported facts into plain maker language; never render raw technical evidence as ordinary maker-facing copy. ## Durable command boundary diff --git a/solutions/ess-maker-skills/src/reference/native-alm-import.md b/solutions/ess-maker-skills/src/reference/native-alm-import.md index 781fc3481..2aedbf8f6 100644 --- a/solutions/ess-maker-skills/src/reference/native-alm-import.md +++ b/solutions/ess-maker-skills/src/reference/native-alm-import.md @@ -18,6 +18,7 @@ workspace completion. | Claim | Status | | --- | --- | | Import uses `POST /copilotstudio/minimalBots/alm/import` | Live-proven in TEST | +| Import uses `api-version=2022-03-01-preview` and `x-ms-client-name: CopilotStudio` | Confirmed by the platform ALM reference | | The request uses multipart form data with one binary `package` part | Live-proven in TEST | | Omitting `schemaName` requests create-only behavior | Live-proven through successful create and HTTP 409 collision | | Supplying a directly validated Dev schema requests replacement | Live-proven in TEST | diff --git a/solutions/ess-maker-skills/src/reference/ui-formatting-guidelines.md b/solutions/ess-maker-skills/src/reference/ui-formatting-guidelines.md index cb358599f..e133ab5e9 100644 --- a/solutions/ess-maker-skills/src/reference/ui-formatting-guidelines.md +++ b/solutions/ess-maker-skills/src/reference/ui-formatting-guidelines.md @@ -11,6 +11,25 @@ troubleshooting, and any message that asks the maker to perform UI actions. - Distinguish destinations, UI controls, selected values, and product names. - Use known context instead of asking the maker to translate generic guidance. +## Current-state language + +No historicity means maker-facing text describes only a current observed fact, +a decision the maker must make, an action the maker must take, or a supported +outcome. Authoring rationale and UX meta-intentions remain instruction-only: +response cadence, narration strategy, template selection, render policy, and +commentary about what the conversation will or will not say. + +Resolve a UX concern by defining the positive response at its owning boundary: + +- successful internal work continues to the next defined maker interaction; +- a required decision uses its defined question and choices; +- a blocked operation states the observed blocker and one supported recovery; +- a completed path uses its defined final handoff. + +Terms such as "chatter," "noise," "narration," "render point," "surface," +"template," "maker-facing," and "UX" describe authoring policy. They are not +setup status, evidence, or instructions for the maker. + ## Formatting semantics Use formatting consistently according to what the text represents: @@ -91,6 +110,7 @@ document. - Formatting every noun for emphasis. - Internal implementation terms, state paths, tool names, or placeholders in maker-facing messages. +- Authoring rationale or UX-policy language presented as setup progress. ## Accessibility and resilience diff --git a/solutions/ess-maker-skills/src/skills/foundation-setup/SKILL.md b/solutions/ess-maker-skills/src/skills/foundation-setup/SKILL.md index 5ee6e223d..8695c5e41 100644 --- a/solutions/ess-maker-skills/src/skills/foundation-setup/SKILL.md +++ b/solutions/ess-maker-skills/src/skills/foundation-setup/SKILL.md @@ -9,6 +9,22 @@ Follow `src/reference/ui-formatting-guidelines.md` for every user-facing instruction in this flow. Resolve its examples with the actual environment, agent, product, and connector names before displaying them. +## Setup state sources + +- **Current setup state:** `.local/setup/config.json` +- **Active agent and workspace:** `.local/config.json` +- **Setup evidence:** `.local/setup/agents/{AGENT_ID}/` + +Use the active agent's entry in `.local/setup/config.json` when determining its +setup progress and readiness. Evidence files support that state; they are not a +separate setup record. + +Maker-visible setup text consists of the defined **Message** blocks and +questions, their choices, an observed blocker with its supported recovery, and +the final handoff. Operational sequencing and response-policy prose are +instruction-only. Successful internal operations continue directly to the next +defined maker interaction. + This is the DA-GA `/setup` entry point. It owns only: - maker authentication; @@ -24,7 +40,7 @@ packs, and topics are explicitly outside this skill. ## Maker-facing progress -At setup start and at the beginning of every subsequent setup turn, write the exact maker-facing progress checklist below as a complete snapshot. Every update contains all five stages in this order and uses the same ordinary Markdown shape: one single-level bullet and one leading status emoji per stage. Use the latest canonical setup state read in this invocation and results observed in this invocation to set their statuses. After each setup action that changes progress, write the complete snapshot again. Preserve completed stages, keep pending stages present, and represent subordinate checks through the status of their owning stage. Before every maker-facing response, including the final handoff, synchronize the complete snapshot once more and mark **Review the setup handoff** complete before finishing. Do not expose the eight internal setup-step IDs or show skipped internal records as successful checks. +Write the exact maker-facing progress checklist below as a complete snapshot at these render points: the first interactive setup surface in a turn, a change to any of its five markers, a blocked state that requires maker action, and the final handoff. Every rendered update contains all five stages in this order and uses the same ordinary Markdown shape: one single-level bullet and one leading status emoji per stage. Use the latest canonical setup state read in this invocation and results observed in this invocation to set their statuses. A sequence of setup operations that retains the same markers continues to its next render point without another progress snapshot. Preserve completed stages and keep pending stages present. Report setup-owned FlightChecks in the separate runtime-readiness table defined by the shared existing-agent completion path; a FlightCheck result does not roll back a completed access, identity, agent-establishment, or materialization stage. Mark **Review the setup handoff** complete in the final snapshot. Do not expose the eight internal setup-step IDs or show skipped internal records as successful checks. **Message:** @@ -40,17 +56,17 @@ Here's your ESS agent setup: Use ✅ for completed, 🔄 for the current stage, ⛔ for a blocked stage, and ⬜ for pending. Derive markers only from supplied context, results observed in this invocation, and canonical setup state read in this invocation. Never infer progress from conversation history. -The checklist is a view, not another state model: +The checklist is a view, not another state model. It tracks local workspace setup actions, not the runtime-readiness verdict: - a supplied or selected target completes the first stage; - direct service validation of an exact editable Dev completes the second and third stages for the existing-agent path; - a successful package import with direct Dev validation completes the second and third stages for the supplied-package path; - service inspection of a Prod source completes access and source-identity verification; a directly validated related Dev or successful create-only import completes the editable-Dev stage; - a successful MOS create followed by direct Dev attachment validation completes access, identity, and editable-Dev establishment for the fresh-agent path; -- only `connectionStatus: workspace-ready` with `connectReady: true` completes local workspace materialization; +- `connectionStatus: workspace-ready` with canonical workspace evidence and `SETUP-07` in state `done` completes local workspace materialization, independently of `connectReady`; - reviewing the factual completion report completes the handoff stage in the conversation and does not write another readiness marker. -Before canonical setup begins, mark the first unresolved checklist stage with 🔄 and leave later stages marked ⬜. When canonical state is blocked, mark only the corresponding visible stage with ⛔ and preserve its failure causes in the response. Do not mark a stage complete from a skipped internal setup record. +Before canonical setup begins, mark the first unresolved checklist stage with 🔄 and leave later stages marked ⬜. Mark a checklist stage ⛔ only when the operation named by that stage is itself blocked, and preserve its failure causes in the response. A blocked capacity, connection, or content FlightCheck belongs in the runtime-readiness table and does not change an already completed checklist marker. Do not mark a stage complete from a skipped internal setup record. ## Choose the sign-in account @@ -101,7 +117,11 @@ Establish a working Python invocation before running setup commands. - When local recovery options appear exhausted, explain the external action needed and offer to perform it. -When child guidance shows `python`, substitute the resolved invocation. + After successful runtime and dependency validation, run the next setup + operation. When validation requires maker action, state the observed failure + and its single recovery action. + + When child guidance shows `python`, substitute the resolved invocation. ## Shared workspace choices @@ -150,7 +170,7 @@ python scripts/setup_existing_da.py select-agent \ Parse `DA_ACTIVE_AGENT_JSON:`. Continue setup for that agent when its `connectReady` value is not `true`; otherwise present the completion choices below. This operation changes only local active-agent selection. -After a successful setup handoff, offer exactly these context-appropriate choices: +The final handoff is the sole completion summary. After it, offer exactly these context-appropriate choices: - **Continue customizing this agent** - **Switch to another configured agent** -- only when another configured agent exists. @@ -159,12 +179,22 @@ After a successful setup handoff, offer exactly these context-appropriate choice - **Create and open a new workspace** - **Finish for now** -Do not preselect a choice. **Install another product in this environment** follows `da-mos-starter.md` with the recorded environment and ring. **Finish for now** ends without another operation. +Do not preselect a choice. **Install another product in this environment** begins `da-mos-starter.md` at its first product-installation decision surface with the recorded environment and ring. **Finish for now** ends immediately. Every other selected follow-up begins at that follow-up's first decision surface rather than rendering another completion summary. ## Start Use context supplied with the current setup request and canonical setup state read in this invocation. Do not infer a route or mismatch from conversation history. +Treat requests to create a new agent, install another product, or start with a +fresh agent as explicit fresh-install intent. Resolve that intent before +active-agent resume handling. Read canonical setup state and `.local/config.json` +only to compare the recorded workspace environment with the requested target. +For the same environment, retain every configured agent and continue directly +through `src/skills/foundation-setup/da-mos-starter.md`. For a different +environment, follow **Create and open a new workspace**. This route uses the +environment match as its workspace decision; existing-agent readiness remains +unchanged. + When the current request supplies no agent, environment, package, or fresh-agent intent, read canonical setup state and `.local/config.json`. A usable active local target must identify the agent display name, agent ID, environment ID, service ring or validated API endpoint, and local workspace folder. Treat these values only as routing input; they do not prove current access, realm, or readiness. For a usable local target, ask: @@ -245,13 +275,6 @@ python scripts/emit_capability.py setup When the maker has already supplied a native agent package or explicitly asked to use one, read `src/skills/foundation-setup/da-alm-import.md` and follow it. That skill owns the explicit package handoff and reads the canonical import reference. This is an advanced handoff, not a setup option to advertise or recommend. -When the maker explicitly asks for a fresh installation, read -`src/skills/foundation-setup/da-mos-starter.md` and follow it, even when the -current Developer Kit folder already has setup state. A same-environment -installation stays in this workspace. A different environment uses **Create and -open a new workspace**. Without explicit fresh-agent intent, keep the -existing-Dev path for a maker who already has an agent. - When the request identifies an environment but not an agent or fresh-agent intent, read `src/skills/foundation-setup/da-existing-dev.md` and follow its environment-candidate selection path. When the request does not identify an agent or environment and no usable local target exists, ask: diff --git a/solutions/ess-maker-skills/src/skills/foundation-setup/da-alm-import.md b/solutions/ess-maker-skills/src/skills/foundation-setup/da-alm-import.md index bdd02545d..7934f05a8 100644 --- a/solutions/ess-maker-skills/src/skills/foundation-setup/da-alm-import.md +++ b/solutions/ess-maker-skills/src/skills/foundation-setup/da-alm-import.md @@ -93,10 +93,7 @@ If attachment fails after import `kind: success`, state: was not prepared. No new import is needed.** Show the attachment error and rerun only the attach command after resolving it. -When complete, render the factual completion report from `da-existing-dev.md` -using **Supplied native agent package** as the starting point. Build every other -field from `DA_EXISTING_DEV_SETUP_JSON:` and retain its limits on completion -claims. +After all four FlightChecks have been attempted, render the factual workspace and runtime-readiness report from `da-existing-dev.md` using **Supplied native agent package** as the starting point, including when `connectReady` is false. Build every workspace field from `DA_EXISTING_DEV_SETUP_JSON:` and retain the report's limits on completion claims. ## Handle a collision diff --git a/solutions/ess-maker-skills/src/skills/foundation-setup/da-existing-dev.md b/solutions/ess-maker-skills/src/skills/foundation-setup/da-existing-dev.md index ca23067fc..00581ca91 100644 --- a/solutions/ess-maker-skills/src/skills/foundation-setup/da-existing-dev.md +++ b/solutions/ess-maker-skills/src/skills/foundation-setup/da-existing-dev.md @@ -50,7 +50,7 @@ python scripts/setup_existing_da.py attach \ The access token supplies the tenant identity during initial inspection; do not infer it from the environment ID. -The command validates the exact agent identity and Dev configuration, fetches the authoritative component change set, converts supported authoring components with the Microsoft Object Model serializer, and materializes the local workspace. It persists canonical setup progress for that agent before materialization. Complete the native FlightCheck maintenance below before treating the agent's `connect_ready: true` as current. +The command validates the exact agent identity and direct Dev route, fetches the authoritative component change set, confirms its component identity and schema, converts supported authoring components with the Microsoft Object Model serializer, and materializes the local workspace. It does not require published Dev configuration; publishing is outside foundation setup and is not attachment remediation. It persists canonical setup progress for that agent before materialization when identity is complete. Complete the native FlightCheck maintenance below before treating the agent's `connect_ready: true` as current. If Object Model dependencies are missing, run: @@ -90,7 +90,7 @@ Show candidate display names and ask the maker to choose one. Validate only the ## Maintain native FlightCheck evidence -After every successful `attach` or unchanged existing-workspace resume, run all four setup-owned FlightChecks for the exact agent. +After every successful `attach` or unchanged existing-workspace resume, treat all four setup-owned FlightChecks and their maintenance calls as one presentation unit. Run all four for the exact agent and attempt every check whose prerequisites remain available. Run each checkpoint into its dedicated local evidence folder: @@ -110,7 +110,7 @@ python scripts/setup_existing_da.py maintain-flightcheck --agent-id "{AGENT_ID}" python scripts/setup_existing_da.py maintain-flightcheck --agent-id "{AGENT_ID}" --checkpoint DA-CONTENT-001 --results .local/setup/agents/{AGENT_ID}/flightcheck/DA-CONTENT-001/results.json ``` -Parse every `DA_SETUP_FLIGHTCHECK_JSON:` result. Its `state`, `connectReady`, `activeStep`, and `failureCauses` are the setup verdict. Render the owning setup stage from that verdict and use the matching FlightCheck rows for maker-facing evidence and remediation. +Parse every `DA_SETUP_FLIGHTCHECK_JSON:` result. Its `state`, `connectReady`, `activeStep`, and `failureCauses` are the runtime-readiness verdict. Use the matching FlightCheck rows for maker-facing evidence and remediation. After attachment, attempt every available check before producing the final runtime-readiness table. When maker action is required or an operation prevents later checks from running, state the observed blocker and supported recovery. Do not use a FlightCheck result to roll back a completed maker-facing checklist stage. For `DA-CONN-*`, setup applies these outcomes: @@ -124,11 +124,11 @@ For `DA-CONN-*`, setup applies these outcomes: ## Interpret results -Canonical setup state is authoritative for each agent's setup progress and completion. Setup for the active agent is complete when attachment reports `connectionStatus: workspace-ready`, every step in that agent's canonical record is `done`, and the final `DA_SETUP_FLIGHTCHECK_JSON:` reports `connectReady: true`. +Canonical setup state is authoritative for each agent's setup progress and readiness. Local workspace materialization is complete when attachment reports `connectionStatus: workspace-ready`, canonical workspace evidence is present, and `SETUP-07` is `done`. Runtime readiness is complete only when every step in that agent's canonical record is `done` and the final `DA_SETUP_FLIGHTCHECK_JSON:` reports `connectReady: true`. Canonical state records native environment access, capacity, binding readiness, and baseline content readiness as automated FlightCheck evidence. Only preferred-solution configuration remains skipped because it does not apply to the DA-only path. -When canonical setup state is incomplete, render the stage identified by `active_step` with its state and `failure_causes`. Translate `SETUP-03` to **Establish an editable Dev agent** and `SETUP-07` to **Materialize the local workspace**. Explain the unmet prerequisite in maker language and offer the bounded remediation for that evidence. +Before materialization completes, render an incomplete `SETUP-03` as **Establish an editable Dev agent** and an incomplete `SETUP-07` as **Materialize the local workspace**. After materialization completes, render any blocked capacity, connection, or content step only in the runtime-readiness table. Explain each unmet prerequisite in maker language and offer the bounded remediation supported by that evidence. If content was synced to the local workspace but the returned result is not workspace-ready and supplies no specific failure cause, keep **Materialize the local workspace** current and show: @@ -136,32 +136,39 @@ If content was synced to the local workspace but the returned result is not work Do not invent a cause or run another operation without new maker intent. -On success, build this report only from `DA_EXISTING_DEV_SETUP_JSON:`. Use a friendly environment name only when an authoritative operation returned one; otherwise say `Selected Power Platform environment`. Render empty `unprojectedComponentKinds` as `None` and a missing checkpoint as `Not required`. +After successful materialization and after all four setup-owned FlightChecks have been attempted, build the agent link from `DA_EXISTING_DEV_SETUP_JSON:` and build the runtime-readiness table from the applied FlightCheck results and canonical state. Render both even when `connectReady` is false. -**Message:** +Infer a concise user-friendly product name from the authoritative product or agent display name when its meaning is unambiguous. For example, render `Employee Self-Service IT` as `Employee Self-Service (IT)` and `Employee Self-Service HR` as `Employee Self-Service (HR)`. If a friendly form is not clear, use the authoritative backend display name unchanged. Never use a schema name or agent ID as link text. + +Build the exact agent URL as `{COPILOT_STUDIO_ORIGIN}/environments/{ENVIRONMENT_ID}/bots/{AGENT_ID}/overview`, using the validated Copilot Studio origin for the selected service ring and the exact environment and agent IDs from setup evidence. Never link to the environment's agent-list page. -Your ESS agent workspace is ready. +**Message:** -| Item | Result | -| -------------------------- | ---------------------------------------------------------------------- | -| Editable Dev agent | **{agent display name}** | -| Starting point | Existing editable Dev | -| Target environment | **{friendly environment name or Selected Power Platform environment}** | -| Local workspace | `{workspace folder}` | -| Topics synced | {topic count} | -| Global variables synced | {variable count} | -| Other retained components | {unprojected component summary or None} | -| Local checkpoint | {checkpoint number or Not required} | +Your local workspace is ready for authoring. The remote agent is available at [{USER_FRIENDLY_PRODUCT_NAME}]({ACTUAL_AGENT_URL}) in Microsoft Copilot Studio. -Not performed by foundation setup: +### Runtime readiness -- publishing or promotion; -- connector installation and authentication; -- product-extension configuration; -- server-backed validation of unpublished local changes. +| Check | Status | Details | +| -------------------- | ------------------------------- | ---------------------------------------- | +| Agent access | {agent access status} | {agent access evidence summary} | +| Environment capacity | {environment capacity status} | {environment capacity evidence summary} | +| Connections | {connections status} | {connections evidence summary} | +| Agent content | {agent content status} | {agent content evidence summary} | +| **Overall** | **{overall readiness status}** | **{maker-facing readiness summary}** | **End message.** +Use the same five rows and order in every runtime-readiness table: + +- `Passed` is **✅ Ready**. +- An accepted `DA-CONN-*` `Warning` is **⚠️ Ready with limitation** and retains its warning disclaimer. +- `DA-CONN-*` `Skipped` because the agent declares no logical connection references is **➖ Not required**. +- `NotConfigured` or `Failed` is **⛔ Action required**. +- `Error` or an unavailable check is **⚠️ Check unavailable**. +- A check without current evidence is **⬜ Not checked**. + +Use the most consequential current evidence when a checkpoint has multiple rows: **Action required**, then **Check unavailable**, then **Ready with limitation**, then **Not required**, then **Ready**. When `connectReady` is true and no accepted warning remains, render Overall as **✅ Ready**. When `connectReady` is true with an accepted warning, render it as **⚠️ Ready with limitations**. When `connectReady` is false after materialization, render it as **⚠️ Needs attention** and state that local authoring is ready while the reported runtime prerequisites remain. Do not add inferred warnings or place publishing, connector installation, promotion, product-extension configuration, or non-queryable governance requirements in this table. + This report is a factual handoff, not another readiness gate. If the maker disputes a fact, inspect the underlying operation evidence rather than changing canonical state conversationally. Then present the shared completion choices from `SKILL.md`. Preserve service status, error code, request ID, and local projection-failure evidence for diagnosis. In ordinary maker-facing copy, explain the specific service or conversion failure in plain language without exposing raw technical output. Do not replace it with a generic setup error. @@ -181,6 +188,12 @@ Offer exactly: - **Checkpoint and refresh** - **Keep local files unchanged** +For **Keep local files unchanged**, preserve the managed local files and canonical setup state, then say: + +> Your local files were left unchanged. Setup stopped without refreshing them. + +This choice ends the current setup attempt at the refresh decision. FlightChecks and the final handoff resume after a later unchanged attachment or successful refresh. + Continue only after the maker explicitly selects **Checkpoint and refresh**: ```text diff --git a/solutions/ess-maker-skills/src/skills/foundation-setup/da-mos-starter.md b/solutions/ess-maker-skills/src/skills/foundation-setup/da-mos-starter.md index d57e87fc1..cab80f44b 100644 --- a/solutions/ess-maker-skills/src/skills/foundation-setup/da-mos-starter.md +++ b/solutions/ess-maker-skills/src/skills/foundation-setup/da-mos-starter.md @@ -21,6 +21,22 @@ catalog. When fresh-agent intent and the target environment are known, mark Read canonical setup state and `.local/config.json` when present. Continue in an occupied workspace when its recorded environment is the selected target. If it records a different environment, follow **Create and open a new workspace** in `SKILL.md` and stop this invocation after that handoff. Do not reset a same-environment workspace merely to install another product. +Once the target environment is resolved, use this fixed opening as the first product-installation surface: + +**Message:** + +Here's your ESS agent setup: + +- ✅ Choose the starting point and target environment +- 🔄 Verify access and agent identity +- ⬜ Establish an editable Dev agent +- ⬜ Materialize the local workspace +- ⬜ Review the setup handoff + +Loading entitled products for **{environment name}**... + +**End message.** + ## List the catalog Complete the shared account-selection and authorization steps in `SKILL.md`, then run: @@ -31,18 +47,15 @@ python scripts/setup_mos_starter.py list \ --ring "{RING}" ``` -Parse `DA_MOS_STARTER_PACKAGES_JSON:`. Preserve every service row as operation evidence. Group rows by exact `packageId` and present one picker option per exact ID, using the service-provided name, version, and description. Never show the internal `packageId` to the maker. Do not describe products as remaining, uninstalled, or eligible; the create response is the service-owned decision for the selected package. +Parse `DA_MOS_STARTER_PACKAGES_JSON:`. Preserve every service row as operation evidence. Group rows by exact `packageId` and present one picker option per exact ID, using the service-provided name, version, and description as authoritative inputs. Never show the internal `packageId` to the maker. Do not describe products as remaining, uninstalled, or eligible; the create response is the service-owned decision for the selected package. + +For each picker row, infer a concise user-friendly product name only when the service-provided name or description makes the meaning unambiguous. For example, render `Employee Self-Service IT` as `Employee Self-Service (IT)` and render `Employee Self-Service HR` as `Employee Self-Service (HR)`. If a friendly form is not clear, use the exact service-provided product name unchanged. This display-only inference must not change the underlying `packageId`, backend name, or create request. -Normalize the picker label from the exact service-provided product name: +When listing succeeds, continue the catalog surface with: -| Service product name | Experience | -| -------------------------- | ---------- | -| `Employee Self-Service` | Hub/Core | -| `Employee Self-Service HR` | HR | -| `Employee Self-Service IT` | IT | -| Any other name | Other | +> {PRODUCT_COUNT} entitled products are available. Select the product to create. -Use the host's interactive single-selection control and offer one choice for each exact `packageId`. Do not ask the maker to type a product name. Format each choice as **{experience} -- {product name} {version}** and use `shortDescription`, then `description`, as its supporting text. Omit a blank version or description instead of showing an unresolved value. +Use the host's interactive single-selection control and offer one choice for each exact `packageId`. Do not ask the maker to type a product name. Format each choice as **{friendly product name} {version}** and use `shortDescription`, then `description`, as its supporting text. Omit a blank version or description instead of showing an unresolved value. Retain the selected friendly product name for the final exact-agent link. The successful list proves target access, but not a new agent identity. Keep **Verify access and agent identity** current until create and direct attachment validation succeed. @@ -66,7 +79,7 @@ Offer exactly: - **Choose a different product** - **Cancel setup** -Do not preselect **Create agent**. Run create only after the maker explicitly selects **Create agent** for the displayed product and target. +Do not preselect **Create agent**. After the maker explicitly selects **Create agent** for the displayed product and target, begin the create operation immediately; the confirmation surface already communicates the selected product and environment. ## Create @@ -98,11 +111,12 @@ When the annotations report `outcome: collision`, do not infer which visible age - **Choose an existing agent in this environment** - **Choose a different catalog product** -- **Go back** - **Cancel setup** Do not preselect a choice. For **Choose an existing agent in this environment**, show the returned names, let the maker select one exact agent, and continue through `da-existing-dev.md`. The selected agent is maker-supplied intent, not proof of package identity. This path does not replace an agent. +For **Choose a different catalog product**, present the valid rows from the latest successful catalog result and let the maker select another exact product. Continue through **Confirm the exact product and target** for that selection. A new create request becomes available only after the maker confirms the new product and uses a new client request UUID. + ## Enable ALM After a successful create, say: @@ -120,6 +134,8 @@ python scripts/setup_mos_starter.py enable-alm \ Parse `DA_MOS_STARTER_ALM_ANNOTATIONS_JSON:` and its response body when present, then `DA_MOS_STARTER_ALM_JSON:` on success. A failed read-back may instead emit `DA_MOS_STARTER_ALM_VERIFY_ANNOTATIONS_JSON:`, its response body, or `DA_MOS_STARTER_ALM_VERIFY_JSON:`. Continue only for `outcome: enabled` or `outcome: already-enabled` with `persistedValue: true`. If read-back definitively reports `outcome: verification-failed` and `persistedValue: false`, say, "The follow-up check showed that the agent was not prepared for local editing. Setup has stopped without attaching a workspace." If transport or read-back becomes uncertain, preserve the evidence internally, say that the agent could not be confirmed ready for local editing, and stop. +An enabled or already-enabled result proceeds directly to attachment. + ## Attach After ALM read-back succeeds, run: @@ -129,12 +145,13 @@ python scripts/setup_existing_da.py attach \ --environment-id "{ENVIRONMENT_ID}" \ --ring "{RING}" \ --agent-id "{RETURNED_AGENT_ID}" \ - --setup-source mos-starter + --setup-source mos-starter \ + --expected-schema-name "{RETURNED_SCHEMA_NAME}" ``` -On failure, preserve the command's specific `ERROR:` text and any canonical `active_step` and `failure_causes` as diagnostic evidence. Translate them into the visible setup stage and a plain explanation of the unmet prerequisite as described in `da-existing-dev.md`; never show internal step IDs or raw technical output as ordinary maker copy. On success, parse `DA_EXISTING_DEV_SETUP_JSON:`. When attachment reports `connectionStatus: workspace-ready`, run the agent-scoped native FlightCheck maintenance sequence in `da-existing-dev.md`. Treat setup as complete only when its final `DA_SETUP_FLIGHTCHECK_JSON:` reports `connectReady: true`. If a FlightCheck blocks setup, translate its evidence according to `da-existing-dev.md`. The request-scoped create evidence intentionally remains as an audit note. Do not publish, remove, or replace components from this path. +This attachment validates the returned agent through its direct Dev route and component identity; it does not require published Dev configuration. On failure, preserve the command's specific `ERROR:` text and any canonical `active_step` and `failure_causes` as diagnostic evidence. Translate them into the visible setup stage and a plain explanation of the unmet prerequisite as described in `da-existing-dev.md`; never show internal step IDs or raw technical output as ordinary maker copy. Publishing is outside foundation setup and is not remediation for an attachment failure. On success, parse `DA_EXISTING_DEV_SETUP_JSON:`. When attachment reports `connectionStatus: workspace-ready`, mark the access, identity, editable-agent, and materialization stages complete, then run the four setup-owned FlightChecks as the single presentation unit defined in `da-existing-dev.md`. Complete every check whose prerequisites remain available before producing the factual workspace and runtime-readiness handoff. When an operation requires maker action or prevents later checks from running, state the observed blocker and supported recovery. The request-scoped create evidence intentionally remains as an audit note. Do not publish, remove, or replace components from this path. -After direct attachment validation succeeds, mark **Verify access and agent identity** and **Establish an editable Dev agent** complete. Render the factual completion report from `da-existing-dev.md` using **New entitled MOS product** as the starting point. +After all four FlightChecks have been attempted, render the factual workspace and runtime-readiness report from `da-existing-dev.md` using **New entitled MOS product** as the starting point, including when `connectReady` is false. For every non-created outcome (`pre-dispatch-failure`, `collision`, `rejected`, `malformed-success`, `source-package-mismatch`, or an uncertain response or transport failure), end the create operation. The existing read-only `list` and `setup_existing_da.py validate-agent`/`list-agents` commands remain available for a separately requested inspection. diff --git a/solutions/ess-maker-skills/src/skills/foundation-setup/da-prod-to-dev.md b/solutions/ess-maker-skills/src/skills/foundation-setup/da-prod-to-dev.md index 8e043c720..f525584c0 100644 --- a/solutions/ess-maker-skills/src/skills/foundation-setup/da-prod-to-dev.md +++ b/solutions/ess-maker-skills/src/skills/foundation-setup/da-prod-to-dev.md @@ -169,7 +169,8 @@ python scripts/setup_existing_da.py attach \ --ring "{RING}" \ --api-version "{API_VERSION}" \ --agent-id "{RETURNED_AGENT_ID}" \ - --setup-source prod-to-dev + --setup-source prod-to-dev \ + --expected-schema-name "{RETURNED_SCHEMA_NAME}" ``` -When attachment reports `connectionStatus: workspace-ready`, run the native FlightCheck maintenance sequence in `da-existing-dev.md`. Complete setup only after its final `DA_SETUP_FLIGHTCHECK_JSON:` reports `connectReady: true`, then use the factual report there. Use **Existing Prod agent; related Dev reused** as the starting point for a validated related-Dev path and **Existing Prod agent; new Dev created** after a successful create-only import. Do not claim Prod changed, Dev was published, or promotion was configured. Do not add cross-tenant support, replacement, collision recovery, export receipts, or telemetry. +The earlier source inspection and import or related-Dev validation own ALM-family proof. Attachment validates the returned Dev route and component schema without requiring published Dev configuration. When attachment reports `connectionStatus: workspace-ready`, run the native FlightCheck maintenance sequence in `da-existing-dev.md`. Complete runtime readiness only after its final `DA_SETUP_FLIGHTCHECK_JSON:` reports `connectReady: true`. After all four FlightChecks have been attempted, render the factual workspace and runtime-readiness report there, including when `connectReady` is false. Use **Existing Prod agent; related Dev reused** as the starting point for a validated related-Dev path and **Existing Prod agent; new Dev created** after a successful create-only import. Do not claim Prod changed, Dev was published, or promotion was configured. Do not add cross-tenant support, replacement, collision recovery, export receipts, or telemetry. diff --git a/tests/mocks/agentbuilder_connectivity.py b/tests/mocks/agentbuilder_connectivity.py index 8fbef8d68..9fb6c123d 100644 --- a/tests/mocks/agentbuilder_connectivity.py +++ b/tests/mocks/agentbuilder_connectivity.py @@ -132,7 +132,7 @@ def get_components( "method": responses.POST, "url": ( f"{MOCK_AGENTBUILDER_BASE}/copilotstudio/minimalBots/api/" - f"{MOCK_AGENT_ID}/components?api-version=2024-10-01" + f"{MOCK_AGENT_ID}/components?api-version=2022-03-01-preview" ), "json": payload or components(), "status": 200, diff --git a/tests/scripts/test_agentbuilder.py b/tests/scripts/test_agentbuilder.py index 65ee340ba..0abe73afd 100644 --- a/tests/scripts/test_agentbuilder.py +++ b/tests/scripts/test_agentbuilder.py @@ -124,7 +124,7 @@ def test_connectivity_client_lists_environment_connections() -> None: "headers": { "Authorization": "Bearer fake-token", "Accept": "application/json", - "x-ms-client-name": "EssAdk", + "x-ms-client-name": "CopilotStudio", }, "timeout": 120, } @@ -187,6 +187,12 @@ def request(self, method: str, url: str, **kwargs: Any) -> FakeResponse: return self.responses.pop(0) +def _non_auth_headers(call: dict[str, Any]) -> dict[str, str]: + headers = dict(call["headers"]) + assert headers.pop("Authorization") + return headers + + def test_host_derivation_probes_primary_split_first() -> None: attempted: list[str] = [] @@ -256,6 +262,14 @@ def test_client_uses_only_configured_environment_host() -> None: assert session.calls[3]["params"]["realm"] == 0 assert session.calls[4]["method"] == "POST" assert session.calls[4]["json"] == {} + assert session.calls[4]["params"] == { + "api-version": agentbuilder.NATIVE_ALM_API_VERSION + } + assert _non_auth_headers(session.calls[4]) == { + "Accept": "application/json", + "Content-Type": "application/json", + "x-ms-client-name": "CopilotStudio", + } def test_realm_configuration_rejects_unknown_realm() -> None: @@ -305,11 +319,15 @@ def test_import_package_sends_multipart_create_without_json_content_type( } call = session.calls[0] assert call["method"] == "POST" - assert call["params"] == {"api-version": "2024-10-01"} - assert "Content-Type" not in call["headers"] + assert call["params"] == { + "api-version": agentbuilder.NATIVE_ALM_API_VERSION + } + assert _non_auth_headers(call) == { + "x-ms-client-name": "CopilotStudio", + } assert call["data"] == {} filename, stream, content_type = call["files"]["package"] - assert filename == "agent.zip" + assert filename == "package.zip" assert stream.closed assert content_type == "application/zip" assert "json" not in call @@ -369,11 +387,13 @@ def test_realm_discovery_configuration_and_export_use_native_alm_requests( f"/copilotstudio/minimalBots/alm/{AGENT_ID}/export" ) assert export["params"] == { - "api-version": agentbuilder.DEFAULT_API_VERSION + "api-version": agentbuilder.NATIVE_ALM_API_VERSION } assert export["allow_redirects"] is False assert export["stream"] is True - assert "Content-Type" not in export["headers"] + assert _non_auth_headers(export) == { + "x-ms-client-name": "CopilotStudio", + } assert "POST" not in session.mounts[ "https://" ].max_retries.allowed_methods @@ -558,6 +578,14 @@ def test_lists_ring_environments_with_agentbuilder_token() -> None: call["headers"]["Authorization"] == "Bearer fake-token" for call in session.calls ) + assert all( + _non_auth_headers(call) + == { + "Accept": "application/json", + "x-ms-client-name": "CopilotStudio", + } + for call in session.calls + ) def test_ring_environment_listing_rejects_unsafe_next_link() -> None: @@ -707,6 +735,14 @@ def test_create_agent_from_starter_package_sends_live_proven_post() -> None: assert call["url"].endswith( "/copilotstudio/minimalBots/createFromStarterPackage" ) + assert call["params"] == { + "api-version": agentbuilder.DEFAULT_API_VERSION + } + assert _non_auth_headers(call) == { + "Accept": "application/json", + "Content-Type": "application/json", + "x-ms-client-name": "CopilotStudio", + } assert call["json"] == {"packageId": "pkg-1"} assert call["allow_redirects"] is False assert "POST" not in session.mounts["https://"].max_retries.allowed_methods @@ -809,6 +845,14 @@ def test_update_bot_entity_preserves_bot_and_requests_no_component_changes() -> f"/copilotstudio/minimalBots/api/{AGENT_ID}/components" ) assert call["json"] == {"bot": bot, "botComponentChanges": []} + assert call["params"] == { + "api-version": agentbuilder.NATIVE_ALM_API_VERSION + } + assert _non_auth_headers(call) == { + "Accept": "application/json", + "Content-Type": "application/json", + "x-ms-client-name": "CopilotStudio", + } assert call["allow_redirects"] is False assert "PUT" not in session.mounts["https://"].max_retries.allowed_methods @@ -832,7 +876,14 @@ def test_publish_agent_uses_minimalbot_route_and_empty_json_body() -> None: assert call["url"].endswith( f"/copilotstudio/minimalBots/api/{AGENT_ID}/publish" ) - assert call["params"] == {"api-version": "2024-10-01"} + assert call["params"] == { + "api-version": agentbuilder.NATIVE_ALM_API_VERSION + } + assert _non_auth_headers(call) == { + "Accept": "application/json", + "Content-Type": "application/json", + "x-ms-client-name": "CopilotStudio", + } assert call["json"] == {} assert call["allow_redirects"] is False assert "POST" not in session.mounts["https://"].max_retries.allowed_methods diff --git a/tests/scripts/test_da_command_degradation.py b/tests/scripts/test_da_command_degradation.py index e6ea7eeba..58c3bb642 100644 --- a/tests/scripts/test_da_command_degradation.py +++ b/tests/scripts/test_da_command_degradation.py @@ -27,9 +27,18 @@ def _da_config() -> dict: } +@pytest.mark.parametrize( + ("publish_result", "expected_message"), + ( + ({"ValidationPending": True}, "validation is still running"), + ({"validationPending": False}, "Published"), + ), +) def test_publish_routes_da_ga_to_native_client( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], + publish_result: dict[str, bool], + expected_message: str, ) -> None: calls: list[tuple[str, object]] = [] @@ -39,7 +48,7 @@ def __init__(self, host, token, **kwargs): def publish_agent(self, agent_id): calls.append(("publish", agent_id)) - return {"validationPending": False} + return publish_result monkeypatch.setattr(publish, "load_config", _da_config) monkeypatch.setattr( @@ -101,7 +110,7 @@ def publish_agent(self, agent_id): "publish", "00000000-0000-4000-8000-000000000001", ) - assert "Published" in capsys.readouterr().out + assert expected_message in capsys.readouterr().out def test_publish_preserves_classic_dataverse_route( diff --git a/tests/scripts/test_setup_existing_da.py b/tests/scripts/test_setup_existing_da.py index b5ee0dbe3..c9317b09c 100644 --- a/tests/scripts/test_setup_existing_da.py +++ b/tests/scripts/test_setup_existing_da.py @@ -227,6 +227,7 @@ def __init__( configured_agent_id: str | None = None, route_realm: int | str = 0, include_agent_schema: bool = True, + published_config_available: bool = True, changeset: dict[str, Any] | None = None, ) -> None: self.agent_name = agent_name @@ -237,6 +238,7 @@ def __init__( self.configured_agent_id = configured_agent_id or agent_id self.route_realm = route_realm self.include_agent_schema = include_agent_schema + self.published_config_available = published_config_available self.changeset = changeset or _changeset( agent_id=agent_id, schema_name=schema_name, @@ -276,6 +278,10 @@ def get_realms(self, _agent_id: str) -> dict[str, Any]: def get_dev_configuration(self, _agent_id: str) -> dict[str, Any]: self.configuration_calls += 1 + if not self.published_config_available: + raise setup_existing_da.ExistingDASetupError( + "The agent has no published Dev config. Publish the agent first." + ) return { "realm": self.configuration_realm, "cdsBotId": self.configured_agent_id, @@ -342,6 +348,7 @@ def _write_flightcheck_results( root: Path, checkpoint: str, *statuses: str, + agent_id: str = AGENT_ID, ) -> Path: path = root / f"{checkpoint.replace('*', 'family')}.json" prefix = checkpoint[:-1] if checkpoint.endswith("*") else checkpoint @@ -368,6 +375,14 @@ def _write_flightcheck_results( ), encoding="utf-8", ) + step_id = setup_existing_da.SETUP_FLIGHTCHECK_STEPS[checkpoint] + step_updated_at = _setup_state(root)["agents"][agent_id]["steps"][step_id][ + "updated_at" + ] + step_started = datetime.fromisoformat(step_updated_at).timestamp() + # Do not let host filesystem timestamp precision decide fixture freshness. + fresh_time = max(path.stat().st_mtime, step_started + 1) + os.utime(path, (fresh_time, fresh_time)) return path @@ -719,19 +734,35 @@ def test_attach_rejects_workspace_environment_mismatch( assert set(_setup_state(tmp_path)["agents"]) == {AGENT_ID} -def test_alm_import_attach_materializes_without_published_config( +@pytest.mark.parametrize( + ("setup_source", "selection_source"), + ( + ("existing-dev", "direct-id"), + ("alm-import", "alm-import-result"), + ("prod-to-dev", "prod-to-dev-result"), + ("mos-starter", "mos-starter-result"), + ), +) +def test_attach_materializes_without_published_config( tmp_path: Path, + setup_source: str, + selection_source: str, ) -> None: - client = FakeClient(include_agent_schema=False) + client = FakeClient( + include_agent_schema=False, + published_config_available=False, + ) result = setup_existing_da.attach_existing_dev( client, environment_id=ENVIRONMENT_ID, agent_id=AGENT_ID, kit_root=tmp_path, - setup_source="alm-import", - selection_source="alm-import-result", - expected_schema_name=SCHEMA_NAME, + setup_source=setup_source, + selection_source=selection_source, + expected_schema_name=( + None if setup_source == "existing-dev" else SCHEMA_NAME + ), ) assert result["connectionStatus"] == "workspace-ready" @@ -746,10 +777,40 @@ def test_alm_import_attach_materializes_without_published_config( ) assert metadata["realm"] == "dev" assert metadata["almFamilyId"] is None + assert metadata["setupSource"] == setup_source + + +def test_existing_dev_attach_derives_schema_from_components( + tmp_path: Path, +) -> None: + client = FakeClient(include_agent_schema=False) + + result = setup_existing_da.attach_existing_dev( + client, + environment_id=ENVIRONMENT_ID, + agent_id=AGENT_ID, + kit_root=tmp_path, + ) + + assert result["connectionStatus"] == "workspace-ready" + assert result["schemaName"] == SCHEMA_NAME + assert client.realm_calls == 1 + assert client.configuration_calls == 0 + assert client.fetch_calls == 1 -def test_alm_import_attach_rejects_component_schema_mismatch( +@pytest.mark.parametrize( + ("setup_source", "selection_source"), + ( + ("alm-import", "alm-import-result"), + ("prod-to-dev", "prod-to-dev-result"), + ("mos-starter", "mos-starter-result"), + ), +) +def test_receipt_backed_attach_rejects_component_schema_mismatch( tmp_path: Path, + setup_source: str, + selection_source: str, ) -> None: changeset = _changeset() changeset["bot"]["schemaName"] = "gptagent_different" @@ -764,8 +825,8 @@ def test_alm_import_attach_rejects_component_schema_mismatch( environment_id=ENVIRONMENT_ID, agent_id=AGENT_ID, kit_root=tmp_path, - setup_source="alm-import", - selection_source="alm-import-result", + setup_source=setup_source, + selection_source=selection_source, expected_schema_name=SCHEMA_NAME, ) @@ -803,26 +864,13 @@ def test_alm_import_attach_rejects_non_dev_route( assert not (tmp_path / "workspace").exists() -def test_later_family_discovery_enriches_and_remains_in_setup_state( +def test_publish_independent_attachment_does_not_invent_family_identity( tmp_path: Path, ) -> None: - setup_existing_da.attach_existing_dev( - FakeClient(include_agent_schema=False), - environment_id=ENVIRONMENT_ID, - agent_id=AGENT_ID, - kit_root=tmp_path, - setup_source="alm-import", - expected_schema_name=SCHEMA_NAME, - ) + client = FakeClient(include_agent_schema=False) setup_existing_da.attach_existing_dev( - FakeClient(), - environment_id=ENVIRONMENT_ID, - agent_id=AGENT_ID, - kit_root=tmp_path, - ) - setup_existing_da.attach_existing_dev( - FakeClient(include_agent_schema=False), + client, environment_id=ENVIRONMENT_ID, agent_id=AGENT_ID, kit_root=tmp_path, @@ -831,7 +879,8 @@ def test_later_family_discovery_enriches_and_remains_in_setup_state( ) state = _agent_setup_state(tmp_path) - assert state["agent"]["alm_family_id"] == FAMILY_ID + assert state["agent"]["alm_family_id"] is None + assert client.configuration_calls == 0 def test_dialog_conversion_gap_preserves_evidence_without_ready_state( @@ -1114,6 +1163,11 @@ def test_not_configured_connection_blocks_setup(tmp_path: Path) -> None: payload = json.loads(results_path.read_text(encoding="utf-8")) payload["overall"] = "READY" results_path.write_text(json.dumps(payload), encoding="utf-8") + step_started = datetime.fromisoformat( + _agent_setup_state(tmp_path)["steps"]["SETUP-05"]["updated_at"] + ).timestamp() + fresh_time = max(results_path.stat().st_mtime, step_started + 1) + os.utime(results_path, (fresh_time, fresh_time)) result = setup_existing_da.maintain_setup_flightcheck( tmp_path, @@ -1457,7 +1511,7 @@ def attach(_client: FakeClient, **kwargs: Any) -> dict[str, Any]: ] + ( ["--expected-schema-name", SCHEMA_NAME] - if setup_source == "alm-import" + if setup_source in {"alm-import", "prod-to-dev", "mos-starter"} else [] ) ) @@ -1466,7 +1520,9 @@ def attach(_client: FakeClient, **kwargs: Any) -> dict[str, Any]: assert observed["selection_source"] == selection_source assert observed["setup_source"] == setup_source assert observed["expected_schema_name"] == ( - SCHEMA_NAME if setup_source == "alm-import" else None + SCHEMA_NAME + if setup_source in {"alm-import", "prod-to-dev", "mos-starter"} + else None ) diff --git a/tests/setup/test_da_setup_router.py b/tests/setup/test_da_setup_router.py index 7e211715f..679301c90 100644 --- a/tests/setup/test_da_setup_router.py +++ b/tests/setup/test_da_setup_router.py @@ -30,6 +30,7 @@ _MOS_STARTER_REFERENCE = ( _SOLUTION / "src" / "reference" / "mos-starter-package.md" ) +_UI_FORMATTING = _SOLUTION / "src" / "reference" / "ui-formatting-guidelines.md" _PREPARE_FRESH_WORKSPACE = _SOLUTION / "scripts" / "prepare_fresh_workspace.py" _RESET_LOCAL_WORKSPACE = _SOLUTION / "scripts" / "reset_local_workspace.py" _WORKDAY = _SOLUTION / "src" / "skills" / "setup" / "SKILL.md" @@ -54,6 +55,20 @@ def test_public_setup_routes_to_da_foundation_module() -> None: assert "Do not route to Dataverse foundation or onboarding playbooks" in prompt +def test_foundation_defines_setup_state_sources() -> None: + foundation = _FOUNDATION.read_text(encoding="utf-8") + normalized = " ".join(foundation.split()) + + assert "**Current setup state:** `.local/setup/config.json`" in foundation + assert "**Active agent and workspace:** `.local/config.json`" in foundation + assert "**Setup evidence:** `.local/setup/agents/{AGENT_ID}/`" in foundation + assert ( + "Use the active agent's entry in `.local/setup/config.json` when " + "determining its setup progress and readiness." + ) in normalized + assert "they are not a separate setup record" in normalized + + def test_public_setup_resolves_python_before_bootstrap_commands() -> None: prompt = _SETUP_PROMPT.read_text(encoding="utf-8") foundation = _FOUNDATION.read_text(encoding="utf-8") @@ -95,8 +110,8 @@ def test_public_setup_resolves_python_before_bootstrap_commands() -> None: ) assert "offer to perform it" in normalized_foundation assert prompt.index( - "After reading the foundation skill, write the complete maker-facing " - "progress" + "After reading the foundation skill, use its explicit progress render " + "points" ) < prompt.index("{PYTHON} -m pip install") checklist = "\n".join( ( @@ -109,7 +124,20 @@ def test_public_setup_resolves_python_before_bootstrap_commands() -> None: ) assert checklist in prompt assert checklist in foundation - assert "Before every maker-facing response, including the final handoff" in ( + assert "not the runtime-readiness verdict" in normalized_foundation + assert "does not roll back a completed" in normalized_foundation + assert ( + "`SETUP-07` in state `done` completes local workspace materialization" + in normalized_foundation + ) + assert "At the first interactive setup surface in a turn" in normalized_prompt + assert "when a marker changes" in normalized_prompt + assert "when a blocked state requires maker action" in normalized_prompt + assert "in the final handoff" in normalized_prompt + assert "retains the same markers continues to its next render point" in ( + normalized_prompt + ) + assert "After successful runtime, dependency, and converter checks" in ( normalized_prompt ) assert "one single-level bullet and one leading status emoji per stage" in ( @@ -121,13 +149,10 @@ def test_public_setup_resolves_python_before_bootstrap_commands() -> None: ) assert cwd_instruction in normalized_prompt assert cwd_instruction in normalized_foundation - assert ( - "At setup start and at the beginning of every subsequent setup turn" - in foundation - ) + assert "at these render points" in foundation assert "same ordinary Markdown shape" in foundation assert "native task list" not in foundation - assert "mark **Review the setup handoff** complete before finishing" in foundation + assert "Mark **Review the setup handoff** complete in the final snapshot" in foundation def test_public_setup_does_not_configure_mcp() -> None: @@ -245,6 +270,7 @@ def test_foundation_routes_supported_da_setup_paths() -> None: assert "confirm the `prod` ring with the user" in normalized_import assert "ask the maker only when" in normalized_import assert "`connectReady: true`" in import_text + assert "including when `connectReady` is false" in import_text assert "`setupStatus`" not in import_text assert "`unprojectedDialogCount`" not in import_text assert "Never preselect or recommend **Continue replacement**" in import_text @@ -282,7 +308,7 @@ def test_foundation_routes_supported_da_setup_paths() -> None: ) assert _DA_MOS_STARTER.is_file() assert _MOS_STARTER_REFERENCE.is_file() - assert "explicitly asks for a fresh installation" in normalized + assert "explicit fresh-install intent" in normalized def test_native_setup_skills_pass_resolved_target_fields() -> None: @@ -398,6 +424,9 @@ def test_prod_to_dev_reference_composes_durable_boundaries() -> None: assert "Do not recover a collision or offer replacement" in normalized_conflict assert "setup_existing_da.py attach" in text assert "--setup-source prod-to-dev" in text + assert '--expected-schema-name "{RETURNED_SCHEMA_NAME}"' in text + assert "without requiring published Dev configuration" in normalized + assert "including when `connectReady` is false" in normalized assert "--source-url" not in text assert "--target-url" not in text assert '--environment-id "{SOURCE_ENVIRONMENT_ID}"' in text @@ -414,7 +443,7 @@ def test_prod_to_dev_reference_composes_durable_boundaries() -> None: assert "Create editable Dev agent" in text assert "Do not preselect **Create editable Dev agent**" in text assert "Use related Dev agent" in text - assert "use the factual report there" in normalized + assert "render the factual workspace and runtime-readiness report there" in normalized assert "Existing Prod agent; related Dev reused" in normalized assert "Existing Prod agent; new Dev created" in normalized for historical_text in ( @@ -449,20 +478,23 @@ def test_mos_starter_reference_composes_durable_boundaries() -> None: assert "DA_MOS_STARTER_ALM_VERIFY_JSON:" in text assert "setup_existing_da.py attach" in text assert "--setup-source mos-starter" in text + assert '--expected-schema-name "{RETURNED_SCHEMA_NAME}"' in text assert "--target-url" not in text assert '--environment-id "{ENVIRONMENT_ID}"' in text assert '--ring "{RING}"' in text assert "outcome: created" in text assert "Never show the internal `packageId` to the maker" in normalized - assert "`connectReady: true`" in text + assert "`connectReady: true`" in existing_dev assert "`setupStatus`" not in text assert "Do not ask the maker to classify the product before loading the catalog" in normalized - assert "| `Employee Self-Service` | Hub/Core |" in text - assert "| `Employee Self-Service HR` | HR |" in text - assert "| `Employee Self-Service IT` | IT |" in text + assert "infer a concise user-friendly product name" in normalized + assert "render `Employee Self-Service IT` as `Employee Self-Service (IT)`" in normalized + assert "render `Employee Self-Service HR` as `Employee Self-Service (HR)`" in normalized + assert "use the exact service-provided product name unchanged" in normalized + assert "must not change the underlying `packageId`" in normalized assert "host's interactive single-selection control" in normalized assert "Do not ask the maker to type a product name" in normalized - assert "**{experience} -- {product name} {version}**" in text + assert "**{friendly product name} {version}**" in text assert "Create a new ESS agent" in normalized assert "**{selected product label}**" in text assert "Choose a different product" in text @@ -484,19 +516,45 @@ def test_mos_starter_reference_composes_durable_boundaries() -> None: assert "Present account confirmation once" in normalized_foundation assert "Continue in an occupied workspace" in normalized assert "recorded environment is the selected target" in normalized - assert "explicitly asks for a fresh installation" in normalized_foundation - assert "even when the current Developer Kit folder already has setup state" in ( + assert "create a new agent, install another product, or start with a fresh agent" in ( normalized_foundation ) + assert "Resolve that intent before active-agent resume handling" in ( + normalized_foundation + ) + assert "retain every configured agent and continue directly" in ( + normalized_foundation + ) + assert "existing-agent readiness remains unchanged" in normalized_foundation + assert normalized_foundation.index( + "Resolve that intent before active-agent resume handling" + ) < normalized_foundation.index( + "When the current request supplies no agent, environment, package, or fresh-agent intent" + ) assert "new absolute sibling-folder path" in normalized_foundation assert "Do not preselect **Create agent**" in text - assert "Run create only after the maker explicitly selects" in normalized + assert "After the maker explicitly selects **Create agent**" in normalized assert "exactly one create attempt" not in normalized assert "The command ends after this one attempt" not in normalized assert "Do not invoke create concurrently or automatically" in normalized assert "diagnostic evidence only" in normalized assert "do not explain those internal version concepts to the maker" in normalized assert "The agent was created. Preparing its local authoring workspace" in normalized + assert "{PRODUCT_COUNT} entitled products are available" in text + assert "Loading entitled products for **{environment name}**..." in text + assert text.index("Loading entitled products for **{environment name}**...") < ( + text.index("{PRODUCT_COUNT} entitled products are available") + ) + assert "use this fixed opening as the first product-installation surface" in ( + normalized + ) + assert "begin the create operation immediately" in normalized + assert "An enabled or already-enabled result proceeds directly to attachment" in ( + normalized + ) + assert "single presentation unit defined in `da-existing-dev.md`" in normalized + assert "Complete every check whose prerequisites remain available" in normalized + assert "state the observed blocker and supported recovery" in normalized assert "application lifecycle management" not in normalized assert "**Prepare for local editing**" not in text assert "**Not now**" not in text @@ -506,12 +564,15 @@ def test_mos_starter_reference_composes_durable_boundaries() -> None: assert "workspace is not ready to connect" in existing_dev assert "New entitled MOS product" in text assert "content was synced to your local workspace" in existing_dev - assert "Topics synced" in existing_dev - assert "Global variables synced" in existing_dev - assert "factual completion report from `da-existing-dev.md`" in normalized + assert "Your local workspace is ready for authoring" in existing_dev + assert "[{USER_FRIENDLY_PRODUCT_NAME}]({ACTUAL_AGENT_URL})" in existing_dev + assert "### Runtime readiness" in existing_dev + assert "factual workspace and runtime-readiness report from `da-existing-dev.md`" in normalized assert "Do not infer persona, product, target, or progress" in normalized assert "Never invoke" in normalized and "/connect" in normalized assert "Do not publish, remove, or replace components" in normalized + assert "Publishing is outside foundation setup and is not remediation" in normalized + assert "including when `connectReady` is false" in normalized assert "setup_setup_mos_starter.py" not in text assert "setup_mos_starter.py resolve" not in text assert "setup_mos_starter.py status" not in text @@ -521,6 +582,13 @@ def test_mos_starter_reference_composes_durable_boundaries() -> None: assert "one picker option per exact ID" in normalized assert "Do not describe products as remaining, uninstalled, or eligible" in normalized assert "Choose an existing agent in this environment" in text + assert "Choose a different catalog product" in text + assert "present the valid rows from the latest successful catalog result" in normalized + assert "Continue through **Confirm the exact product and target**" in text + assert "uses a new client request UUID" in normalized + collision_choices = text[text.index("When the annotations report `outcome: collision`") :] + collision_choices = collision_choices[: collision_choices.index("## Enable ALM")] + assert "**Go back**" not in collision_choices assert "does not identify the corresponding agent" in reference assert "createFromStarterPackage" in reference @@ -609,6 +677,23 @@ def test_foundation_uses_maker_facing_progress_without_duplicate_state() -> None ): assert stage in text assert "The checklist is a view, not another state model" in normalized + assert "first interactive setup surface in a turn" in normalized + assert "a change to any of its five markers" in normalized + assert "a blocked state that requires maker action" in normalized + assert "A sequence of setup operations that retains the same markers" in normalized + assert "The final handoff is the sole completion summary" in normalized + assert "**Finish for now** ends immediately" in normalized + assert "first decision surface rather than rendering another completion summary" in ( + normalized + ) + assert "After successful runtime and dependency validation" in normalized + assert "Maker-visible setup text consists of the defined **Message** blocks" in ( + normalized + ) + assert "Operational sequencing and response-policy prose are instruction-only" in ( + normalized + ) + assert "Successful internal operations continue directly" in normalized assert "Never infer progress from conversation history" in normalized assert "Do not mark a stage complete from a skipped internal setup record" in ( normalized @@ -642,7 +727,7 @@ def test_alm_import_uses_shared_progress_and_completion_handoff() -> None: assert "Verify access and agent identity" in normalized assert "Establish an editable Dev agent" in normalized assert "Agent package imported and verified as an editable Dev agent" in normalized - assert "factual completion report from `da-existing-dev.md`" in normalized + assert "factual workspace and runtime-readiness report from `da-existing-dev.md`" in normalized assert "Supplied native agent package" in text assert "another readiness" not in text.casefold() @@ -670,6 +755,7 @@ def test_alm_import_collision_and_retry_require_separate_choices() -> None: def test_existing_da_dev_path_never_routes_through_dataverse() -> None: text = _DA_EXISTING_DEV.read_text(encoding="utf-8") + normalized = " ".join(text.split()) for command in ( "setup_existing_da.py list-agents", @@ -692,11 +778,43 @@ def test_existing_da_dev_path_never_routes_through_dataverse() -> None: assert "Validate only the selected candidate" in text assert "before authentication or remote agent validation" in text assert "Do not run `validate-agent` immediately before `attach`" in text - assert "Your ESS agent workspace is ready." in text - assert "| Starting point | Existing editable Dev |" in " ".join(text.split()) - assert "Not performed by foundation setup" in text + assert "Your local workspace is ready for authoring." in text + assert "The remote agent is available at" in text + assert "[{USER_FRIENDLY_PRODUCT_NAME}]({ACTUAL_AGENT_URL})" in text + assert "| Item" not in text + assert "| Starting point" not in text + assert ( + "{COPILOT_STUDIO_ORIGIN}/environments/{ENVIRONMENT_ID}/bots/" + "{AGENT_ID}/overview" + ) in text + assert "Never link to the environment's agent-list page" in text + assert "use the authoritative backend display name unchanged" in normalized + assert "### Runtime readiness" in text + readiness_table = "\n".join( + ( + "| Check | Status | Details |", + "| -------------------- | ------------------------------- | ---------------------------------------- |", + "| Agent access | {agent access status} | {agent access evidence summary} |", + "| Environment capacity | {environment capacity status} | {environment capacity evidence summary} |", + "| Connections | {connections status} | {connections evidence summary} |", + "| Agent content | {agent content status} | {agent content evidence summary} |", + "| **Overall** | **{overall readiness status}** | **{maker-facing readiness summary}** |", + ) + ) + assert readiness_table in text assert "Checkpoint and refresh" in text assert "Keep local files unchanged" in text + assert "preserve the managed local files and canonical setup state" in normalized + assert ( + "Your local files were left unchanged. Setup stopped without refreshing them." + in text + ) + assert "ends the current setup attempt at the refresh decision" in normalized + assert "resume after a later unchanged attachment or successful refresh" in ( + normalized + ) + assert "does not require published Dev configuration" in normalized + assert "publishing is outside foundation setup" in normalized def test_existing_dev_completion_remains_evidence_driven() -> None: @@ -705,8 +823,22 @@ def test_existing_dev_completion_remains_evidence_driven() -> None: assert "`connectionStatus: workspace-ready`" in text assert "`connectReady: true`" in text - assert "Canonical setup state is authoritative for each agent's setup progress and completion" in normalized - assert "`state`, `connectReady`, `activeStep`, and `failureCauses` are the setup verdict" in normalized + assert "Canonical setup state is authoritative for each agent's setup progress and readiness" in normalized + assert "`state`, `connectReady`, `activeStep`, and `failureCauses` are the runtime-readiness verdict" in normalized + assert "treat all four setup-owned FlightChecks and their maintenance calls as one presentation unit" in normalized + assert "attempt every available check before producing the final runtime-readiness table" in normalized + assert "Render both even when `connectReady` is false" in normalized + for readiness_status in ( + "**✅ Ready**", + "**⚠️ Ready with limitation**", + "**➖ Not required**", + "**⛔ Action required**", + "**⚠️ Check unavailable**", + "**⬜ Not checked**", + ): + assert readiness_status in text + assert "When `connectReady` is false after materialization" in normalized + assert "local authoring is ready while the reported runtime prerequisites remain" in normalized assert "Present **Connection required**" in normalized assert "a factual handoff, not another readiness gate" in normalized assert "changing canonical state conversationally" in normalized @@ -720,6 +852,30 @@ def test_existing_dev_completion_remains_evidence_driven() -> None: assert "canonical setup state, or conversation history" in normalized +def test_ui_guidance_keeps_ux_meta_intentions_out_of_maker_copy() -> None: + text = _UI_FORMATTING.read_text(encoding="utf-8") + normalized = " ".join(text.split()) + + assert "No historicity means maker-facing text describes only" in normalized + assert "a current observed fact" in normalized + assert "a decision the maker must make" in normalized + assert "an action the maker must take" in normalized + assert "a supported outcome" in normalized + assert "Authoring rationale and UX meta-intentions remain instruction-only" in ( + normalized + ) + assert "successful internal work continues to the next defined maker interaction" in ( + normalized + ) + assert "a blocked operation states the observed blocker and one supported recovery" in ( + normalized + ) + assert '"chatter," "noise," "narration," "render point," "surface,"' in text + assert "Authoring rationale or UX-policy language presented as setup progress" in ( + normalized + ) + + def test_foundation_router_paths_resolve() -> None: referenced = set(_PATH_RE.findall(_FOUNDATION.read_text(encoding="utf-8"))) missing = [path for path in sorted(referenced) if not (_SOLUTION / path).is_file()]