Skip to content

Expose IInteractionService to polyglot app hosts#17959

Open
sebastienros wants to merge 20 commits into
mainfrom
sebastienros/polyglot-interaction-service
Open

Expose IInteractionService to polyglot app hosts#17959
sebastienros wants to merge 20 commits into
mainfrom
sebastienros/polyglot-interaction-service

Conversation

@sebastienros

@sebastienros sebastienros commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Description

Aspire's IInteractionService (prompts, message boxes, notifications, and dynamic inputs) was only reachable from C# app hosts. Polyglot app hosts (TypeScript, Python, Go, Java, Rust) had no way to drive user interactions even though other services are already exposed through the ATS callback-service-provider surface. This PR brings the interaction service to polyglot app hosts.

The core challenge is that the natural way to model interaction inputs as DTOs breaks down for inputs that carry callbacks (dynamic option loading, dialog validation): delegates are not serializable across the ATS/JSON-RPC boundary. Modeling them as DTO fields produces a runtime serialization crash.

Approach

The work was done in the order the design required:

  • Expose ServiceProvider from ExecuteCommandContext so polyglot commands have an entry point to resolve services.
  • Add an ATS-first InteractionExports surface (a tailored internal API rather than a 1:1 DTO mapping of the native types). Inputs are created server-side through factory methods returning an opaque InteractionInputBuilder handle, so callbacks never have to cross the wire as data. Callbacks (dynamic loading, dialog validation) are passed as method parameters and registered as callback handles; the engine marshals a curated context back to the client.
  • Implement the full interaction surface: promptConfirmation, promptMessageBox, promptNotification, promptInput, promptInputs, input factories (createTextInput/createSecretInput/createBooleanInput/createNumberInput/createChoiceInput), withChoiceOptions/withValue/withDynamicLoading, and the inputs-dialog validationCallback.

Notable details for reviewers

  • Non-serializable callbacks stripped from results. InteractionInput.DynamicLoading holds a Func and is sanitized out of interaction results at runtime (ToResultInput), and the property is [AspireExportIgnore]'d so it no longer appears on the polyglot read-back surface (it was always null on the wire). This is the fix for the original "Serialization of System.Func is not supported" crash.
  • By-name input access everywhere. Input collections (the validation context, command arguments, and the multi-input prompt result) expose name-based accessors (value/get for optional lookup, requiredValue/required to throw when missing), so callers read ctx.inputs().value("color") instead of scanning an array. To make this work on the prompt result, InputsInteractionResult is modeled as a handle ([AspireExport(ExposeProperties = true)]) so it surfaces the same InteractionInputCollection (result.inputs()) and a result.canceled() accessor, reusing the proven validation/argument-collection pattern.
  • validationCallback lives on the options object (alongside primaryButtonText, etc.), mirroring the C# InputsDialogInteractionOptions shape, now that callbacks can be carried on DTOs.
  • Choice options are ordered. Choices are an array of { value, label } entries so option order is preserved across the wire.
  • Validation callbacks reuse the already-curated InputsDialogValidationContext (its raw IServiceProvider is [AspireExportIgnore]'d). The shared InputsDialogInteractionOptions.Default singleton is never mutated.
  • withCommand test bench. The polyglot bench apps in tests/PolyglotAppHosts exercise the full surface (including a dynamically-loaded options command and dialog validation) in all four languages, providing per-member coverage.
  • Generated artifacts. ats.txt and the five language snapshots are regenerated. Per repo convention, generated-shape behavior is validated through the polyglot bench apps rather than unit tests asserting code shape.

Manual end-to-end validation was done by building custom app hosts and driving the dashboard in a browser: input validation (required/reserved errors keep the dialog open, valid input closes it) and dynamic options (alwaysLoadOnStart populates choices, dependsOnInputs reloads them on dependency change) both work with no serialization or RPC errors.

Fixes # (issue)

TypeScript examples

The interaction service is resolved from the command context's service provider. All examples come from the polyglot test bench (tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts).

Resolve the service from a command callback:

await container.withCommand("greet", "Greet", async (ctx) => {
    const interactionService = await ctx.serviceProvider().getInteractionService();
    if (!(await interactionService.isAvailable())) {
        return { success: true, message: "Interaction service is not available." };
    }
    // ...
});

Message-style prompts (confirmation, message box, notification):

const confirmation = await interactionService.promptConfirmation("Confirm", "Proceed?", {
    primaryButtonText: "Yes", secondaryButtonText: "No", showSecondaryButton: true
});

const notification = await interactionService.promptNotification("Heads up", "Something happened.", {
    intent: MessageIntent.Warning, linkText: "Learn more", linkUrl: "https://aspire.dev"
});

Create inputs through factory methods (each returns an opaque server-side builder handle). Choices are an ordered array of { value, label } entries:

const name = await interactionService.createTextInput("name", {
    label: "Name", required: true, placeholder: "Jane Doe", maxLength: 64
});
const password = await interactionService.createSecretInput("password", { required: true });
const enabled = await interactionService.createBooleanInput("enabled", { value: "true" });
const count = await interactionService.createNumberInput("count", { value: "1" });
const color = await interactionService.createChoiceInput("color", {
    choices: [{ value: "r", label: "Red" }, { value: "g", label: "Green" }],
    options: { allowCustomChoice: true }
});

// Builder methods chain off the handle.
const greeting = await interactionService.createTextInput("greeting").withValue("hello");
const size = await interactionService
    .createChoiceInput("size")
    .withChoiceOptions([{ value: "s", label: "Small" }, { value: "l", label: "Large" }]);

Dynamically loaded options. The callback runs server-side and reads other inputs through the load context, so no delegate is ever serialized:

const region = await interactionService.createChoiceInput("region", {
    choices: [{ value: "us", label: "United States" }, { value: "eu", label: "Europe" }]
});

const zone = await interactionService
    .createChoiceInput("zone")
    .withDynamicLoading(async (loadContext) => {
        const selectedRegion = await loadContext.getInputValue("region");
        await loadContext.setChoiceOptions(selectedRegion === "eu"
            ? [{ value: "eu-west", label: "EU West" }, { value: "eu-north", label: "EU North" }]
            : [{ value: "us-east", label: "US East" }, { value: "us-west", label: "US West" }]);
    }, { alwaysLoadOnStart: true, dependsOnInputs: ["region"] });

const result = await interactionService.promptInputs(
    "Pick a zone",
    "Choose a region, then pick a zone from the dynamically loaded options.",
    [region, zone]);

// The result is a handle: read cancellation and look up submitted values by name.
if (!(await result.canceled())) {
    const selectedZone = await result.inputs().value("zone");
}

Single and multi-input prompts with a dialog validation callback. The callback lives on the options argument (next to primaryButtonText) and runs server-side, reading submitted values by name:

const single = await interactionService.promptInput(
    "Single input", "Enter a value.",
    interactionService.createTextInput("solo"),
    {
        primaryButtonText: "Save",
        validationCallback: async (validationContext) => {
            const solo = await validationContext.inputs().value("solo");
            if (!solo) {
                await validationContext.addValidationError("solo", "A value is required.");
            }
        }
    });

const soloValue = single.input?.value;

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

sebastienros and others added 6 commits June 5, 2026 08:54
Add an ATS-first polyglot surface for the interaction service so app hosts in
TypeScript, Python, Go, Rust, and Java can prompt users from command callbacks.

- Expose ExecuteCommandContext.ServiceProvider (serviceProvider()) so command
  callbacks can resolve services, including the new getInteractionService().
- Register IInteractionService as an ATS handle and add InteractionExports with
  prompt methods, input factories (as IInteractionService extensions so the
  scanner keeps the input name and emits them as service methods), an
  InteractionInputBuilder handle, and a curated InteractionInputLoadContext for
  dynamic loading that never exposes the raw IServiceProvider.
- Model inputs via a builder/handle instead of a DTO because input options can
  carry non-serializable callbacks (dynamic loading).
- Add a 'pick-zone' withCommand test bench (dynamically loaded choice input) to
  the polyglot apphosts and regenerate the codegen snapshots for all languages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… surface

Expands the polyglot test bench so every newly added IInteractionService
member is exercised by the per-language typecheck: all prompt overloads
(confirmation/messageBox/notification/input/inputs), every input factory
(text/secret/boolean/number/choice), the builder methods (withValue/
withChoiceOptions/withDynamicLoading), the dynamic-loading context
accessors/setters (getInputName/getInputValue/setChoiceOptions/setValue),
and all option/result DTO fields plus the MessageIntent enum.

Added to the TypeScript, Python, Go and Java apphosts. Also removes the
redundant <remarks> on ExecuteCommandContext.ServiceProvider and
regenerates the TypeScript snapshot to drop the propagated doc comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tsc (strict) widened the const zones ternary into a union of object
literals with mutually-exclusive optional undefined properties, which is
not assignable to setChoiceOptions(Record<string, string>). Annotate the
variable as Record<string, string> so each branch is contextually typed.

Verified by generating the SDK modules with 'aspire restore' and running
'tsc --noEmit' on the TypeScript apphost (now passes clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PromptInput/PromptInputs echoed the engine's InteractionInput instances back
to the polyglot caller. When an input was created via withDynamicLoading, the
input still carried its LoadCallback Func on DynamicLoading, which System.Text.Json
cannot serialize across the ATS/JSON-RPC boundary, surfacing as a failed
'promptInputs' invocation. Project result inputs onto callback-free copies so only
data fields (name, value, options, etc.) cross the boundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native InputsDialogInteractionOptions.ValidationCallback was not reachable from
the polyglot surface: it is a Func and InteractionInputsDialogOptions is a serializable
DTO that cannot carry delegates. Add an optional validationCallback parameter to the
ATS PromptInput/PromptInputs exports (the callback travels as a method argument, not a
DTO field) and bridge it onto the native options via a fresh instance so the shared
Default singleton is never mutated. The callback receives the already-curated, exported
InputsDialogValidationContext handle (its IServiceProvider is [AspireExportIgnore]'d), so
polyglot callers can read submitted inputs and call addValidationError.

Regenerate all 5 codegen snapshots + ats.txt and exercise the new callback in the
interaction-showcase command of every polyglot test bench app (TS/Python/Go/Java).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…surface

InteractionInput.DynamicLoading holds an InputLoadOptions with a non-serializable
LoadCallback delegate, and the dynamic-loading payload is always stripped from
interaction results at runtime (InteractionExports.ToResultInput). Polyglot app
hosts configure dynamic loading exclusively through
InteractionInputBuilder.WithDynamicLoading and never read this property back, so
advertising it on the result DTO described a value that is always null on the wire
(and pulled the otherwise-unused InputLoadOptions type into the surface).

Annotate the property with [AspireExportIgnore] so it is excluded from the ATS
surface, regenerate ats.txt and the Go/Java/Python/Rust snapshots (which drop the
generated field), and remove the matching hand-authored line from the TypeScript
base.mts so all five languages stay consistent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 5, 2026 17:39
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17959

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17959"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR exposes Aspire’s IInteractionService to polyglot app hosts by extending the ATS/JSON-RPC surface with interaction-specific exports and handle-based input builders so delegate-bearing callbacks (dynamic loading, validation) don’t need to serialize across the wire.

Changes:

  • Exposes ExecuteCommandContext.ServiceProvider to polyglot command callbacks and adds an ATS export to resolve IInteractionService from that provider.
  • Introduces an ATS-first interaction surface (InteractionExports) including prompt APIs, input factories, dynamic-loading context, and validation callback plumbing.
  • Regenerates ATS shape + polyglot SDK snapshots and updates polyglot bench app hosts to exercise the new interaction APIs (including dynamic loading + validation).

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts Adds TypeScript bench commands to exercise the interaction service surface (dynamic loading + validation).
tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py Adds Python bench commands to exercise interaction prompts, inputs, dynamic loading, and validation.
tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java Adds Java bench commands to exercise interaction prompts, inputs, dynamic loading, and validation.
tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go Adds Go bench commands to exercise interaction prompts, inputs, dynamic loading, and validation.
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts Updates generated TypeScript SDK snapshot to include the interaction service + input builder handles and related DTOs.
tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs Updates generated Rust SDK snapshot for interaction service, DTOs, and handle wrappers.
tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py Updates generated Python SDK snapshot for interaction service, DTOs, and handle wrappers.
tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go Updates generated Go SDK snapshot for interaction service, DTOs, and handle wrappers.
src/Aspire.Hosting/IInteractionService.cs Excludes InteractionInput.DynamicLoading from the ATS surface to avoid delegate serialization.
src/Aspire.Hosting/Ats/InteractionExports.cs Adds ATS exports and supporting DTO/handle types for prompts, input factories/builders, dynamic loading, and validation callbacks.
src/Aspire.Hosting/Ats/AtsTypeMappings.cs Adds ATS export mapping for IInteractionService.
src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs Removes ATS ignore on ExecuteCommandContext.ServiceProvider to enable service resolution from polyglot command callbacks.
src/Aspire.Hosting/api/Aspire.Hosting.ats.txt Regenerates ATS shape to include the interaction-related handles, DTOs, and capabilities.
src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts Removes InteractionInput.dynamicLoading from the TypeScript base model to match ATS-ignore behavior.

Comment thread src/Aspire.Hosting/Ats/InteractionExports.cs
Comment thread src/Aspire.Hosting/api/Aspire.Hosting.ats.txt Outdated
Comment thread src/Aspire.Hosting/api/Aspire.Hosting.ats.txt Outdated
sebastienros and others added 4 commits June 5, 2026 11:27
The polyglot choice input factories (createChoiceInput, withChoiceOptions,
setChoiceOptions) accepted an IReadOnlyDictionary<string,string>, which does
not guarantee option order across the ATS boundary. Dropdown option order is
user-visible, so model choices as an ordered InteractionChoiceOption[] (value +
label) instead, matching the native IReadOnlyList ordering contract.

Regenerate ats.txt and all five language snapshots, and update the TypeScript,
Go, Java, and Python polyglot test bench apphosts to the new ordered array form.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Java generator named the options-bag parameter "options" and then
flattened its properties into locals of the same camelCase name. When a
capability has an optional parameter literally named "options" (the
interaction prompt methods, createChoiceInput, etc.), the generated
"var options = options.getOptions()" shadowed the parameter, producing
"variable options is already defined" plus cascading "cannot infer type"
javac errors. This broke the Java SDK Validation CI job for every apphost
because IInteractionService is part of core Aspire.Hosting.

Rename the bag parameter to "optionsBag" (matching the TypeScript
generator) so flattened locals never collide. Regenerated the Java
snapshots accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The interaction option DTOs declared their nullable string properties with
plain setters. The ATS DTO scanner can only detect optionality for nullable
value types (Nullable.GetUnderlyingType) or init-only properties; it cannot
see nullable reference-type annotations through reflection. As a result
PrimaryButtonText, SecondaryButtonText, LinkText, LinkUrl, Label,
Description, Placeholder, Value and DependsOnInputs were emitted as required.

Downstream generators then treated them as required: the Go and Rust SDKs
serialized empty strings unconditionally, blanking server-side defaults when
a caller only wanted to set a single option such as Intent. (TypeScript,
Python and Java already rendered them optional.)

Switch these option-bag properties to init-only, matching the existing
HttpsCertificateInfo.Thumbprint convention, so the scanner marks them
optional. Regenerated ats.txt and the Go/Rust snapshots (now Option/pointer
with conditional serialization) and updated the Go validation apphost to use
aspire.StringPtr for the now-optional fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…InputValue

Address PR review feedback on the interaction-service exports:

- PromptInputs now validates each array element and throws a clear
  ArgumentException naming the parameter and index instead of letting a
  null element (possible over JSON-RPC) surface as a NullReferenceException.
- GetInputValue returns an empty string instead of null when an input has
  no value or no input with that name exists. The ATS code-generation
  system has no support for nullable reference return types, so every
  generated SDK already types this as a non-nullable string; returning null
  caused Go/Rust clients to fail decoding and made the TS contract unsound.

Updated the TypeScript snapshot for the revised returns doc text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

…-interaction-service

# Conflicts:
#	src/Aspire.Hosting/api/Aspire.Hosting.ats.txt
#	tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go
#	tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs
#	tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java
#	tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 4 jobs were identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

Matched test failure patterns (1 test)
  • Aspire.Cli.EndToEnd.Tests.KubernetesDeployWithGarnetTests.DeployK8sWithGarnet — Unable to access container registry during publish

@sebastienros

Copy link
Copy Markdown
Contributor Author

Let me know if you think we should try the DTO again to be closer to the C# API since we support callbacks in them with #17950

@davidfowl

Copy link
Copy Markdown
Collaborator

@sebastienros where?

@davidfowl

Copy link
Copy Markdown
Collaborator

pr-testing report?

@JamesNK

JamesNK commented Jun 6, 2026

Copy link
Copy Markdown
Member

Let me know if you think we should try the DTO again to be closer to the C# API since we support callbacks in them with #17950

Can you give an example of what using interaction service as DTOs would look like? It seems like we should stick to the .NET API unless there is a good reason not to.

In the .NET API the inputs passed in are mutated with the set value (at least, I remember from memory). For example, region.Value and zone.Value have their values set in this example if the prompt was successful:

const result = await interactionService.promptInputs(
    "Pick a zone",
    "Choose a region, then pick a zone from the dynamically loaded options.",
    [region, zone]);

Does the TypeScript API work the same way? A test to check would be good.
The result.Data also returns the passed in list of inputs. Are they the same references?
Can inputs be accessed from the result collection by name? (there is an indexer for getting them by name on InputsCollection)

@JamesNK

JamesNK commented Jun 6, 2026

Copy link
Copy Markdown
Member

I see this kind of code in some of the samples:

const selectedColor = multi.inputs?.find(input => input.name === "color")?.value;
const name = inputs.find(input => input.name === "name");
if (name?.value === "bad") {
    await validationContext.addValidationError("name", "Name cannot be 'bad'.");
}

There should be an easy way to get an input by name. See examples of using .NET API:

var subscriptionId = context.AllInputs[SubscriptionIdName].Value ?? string.Empty;
var resourceGroupName = context.AllInputs[ResourceGroupName].Value ?? string.Empty;

sebastienros and others added 4 commits June 8, 2026 09:46
… strong DTO callbacks for Go/Java/Python

Relocate the inputs-dialog validation callback from a method parameter of
promptInput/promptInputs onto the InteractionInputsDialogOptions DTO property,
matching the C# IInteractionService usage and leveraging DTO-callback support.

DTO-property callbacks previously rendered weakly typed (Object / func(...any) any)
in Go/Java/Python while TypeScript already rendered them strongly typed. Teach the
Go, Java, and Python generators to render DTO callback properties with full strong
typing using the same metadata TS already consumes (no runtime-template changes:
each language's recursive transport marshaller already registers funcs/closures
embedded in serialized DTO maps).

- Go: DTO callback fields use RenderCallbackType; ToMap embeds a typed shim via a
  shared EmitGoCallbackShimBody factored out of EmitCallbackRegistration.
- Java: strong field/getter/setter; toMap wraps AspireActionN/AspireFuncN in a
  java.util.function.Function with typed arg conversion (guarded to <=1 param to
  match the single-arg runtime wrapper); fromMap skips client->host callbacks.
  Lambda parameter renamed to transportArg to avoid shadowing the converted local.
- Python: DTO callback properties use typing.Callable[[T], R].
- Rust: DTO callbacks remain Option<Value> (serde derive cannot serialize
  Box<dyn Fn>); expanded comment documents the known follow-up, consistent with
  existing PrepareRequest/CreateProcessSpec handling.

Regenerated ats.txt and the 5 codegen snapshots, and updated the Go/Java/Python/TS
polyglot apphosts to set validationCallback inside InteractionInputsDialogOptions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lback

Local `aspire restore` + compile (go build / javac) validation surfaced that the
withCommand UpdateState DTO callback now renders strongly typed in Go and Java
(a side effect of emitting strong DTO callbacks), so the benches' weak forms no
longer compiled.

- Go: updateCommandState is now func(ctx UpdateCommandStateContext) ResourceCommandState
  (drops the manual args[0] type assertion since ctx is strongly typed).
- Java: setUpdateState takes a bare lambda matching AspireFunc1 instead of a
  java.util.function.Function cast.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…p test

Address JamesNK's PR feedback on clunky by-name input lookups. The TypeScript
InteractionInputCollection already ships client-side by-name accessors
(get/value/required/requiredValue), so the showcase bench now uses value(name)
in validation callbacks and the echo command instead of toArray().find().

Add a round-trip test proving PromptInputs returns submitted values keyed by
input name, documenting that polyglot callers receive values via the result
rather than by mutating the server-side input handles they pass in.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bring the Go, Java, and Python polyglot code generators to parity with
TypeScript by injecting hand-authored by-name accessor methods
(get/required/value/requiredValue) onto the auto-generated
InteractionInputCollection wrapper. These delegate to the generated
toArray capability and match names case-insensitively, mirroring the
.NET InteractionInputCollection indexer and the TS get/value helpers.

Update the Go/Java/Python test-bench apphosts to use value("name")
instead of toArray() loops, and regenerate the affected snapshots.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sebastienros

Copy link
Copy Markdown
Contributor Author

PR Testing Report

PR Information

CLI Version Verification

  • Expected Commit (PR head): 0c5539d
  • Installed Version: 13.5.0-pr.17959.g0c5539d4
  • Status: PASS - version contains the PR head short SHA g0c5539d4
  • Hive: Aspire.Hosting.13.5.0-pr.17959.g0c5539d4.nupkg present in PR hive

Changes Analyzed (23 files)

  • Hosting (5): Ats/InteractionExports.cs (new polyglot surface), ResourceCommandAnnotation.cs (ServiceProvider exposure), AtsTypeMappings.cs, IInteractionService.cs, api/Aspire.Hosting.ats.txt
  • CodeGen (5): Go, Java, Python, Rust, TypeScript generators (incl. new InteractionInputCollection by-name accessors)
  • Polyglot benches (4): TypeScript/Python/Go/Java app hosts
  • Tests/snapshots (9): 7 verified snapshots + RemoteHost round-trip test
  • No CLI / Dashboard / Template / Component code changes.

Test Scenarios Executed

Scenario 1: CLI install + version gate

Objective: Install the PR CLI via the dogfood script and confirm it matches the PR head.
Coverage Type: Happy path
Status: PASS

  • Dogfood install succeeded; package version suffix pr.17959.g0c5539d4.
  • aspire --version -> 13.5.0-pr.17959.g0c5539d4 (matches head 0c5539d).
  • Note: the head-commit native CLI archive was still building at first attempt; retried after the macOS ARM64 archive job completed.

Scenario 2: SDK regeneration from the PR build (aspire restore)

Objective: Regenerate the TypeScript SDK from the PR's Aspire.Hosting and confirm the new polyglot interaction API surface is present.
Coverage Type: Happy path (core functional)
Status: PASS

aspire restore regenerated .aspire/modules/{aspire.mts, base.mts, transport.mts} from the PR hive. Verified the three requested layers plus design details:

  • ServiceProvider on ExecuteCommandContext: services(): ServiceProviderPromise + IServiceProvider handle generated on the command context.
  • InteractionService exports + impl: InteractionService interface, InteractionServiceImpl, and InteractionServicePromise generated; reachable as the new polyglot service.
  • IInteractionService implementation surface: prompt inputs/message box/notification factories present.
  • Handle-based factory for non-serializable callbacks: withDynamicLoading(callback: (arg: InteractionInputLoadContext) => Promise<void>, options?) uses an opaque InteractionInputLoadContext handle (the factory/opaque-handle design for callbacks that can't be DTO-serialized).
  • validationCallback on the options DTO: validationCallback?: (arg: InputsDialogValidationContext) => Promise<void> lives on the options object.
  • Cross-language parity accessors: InteractionInputCollection exposes get / required / value / requiredValue (base.mts), matching the Go/Java/Python accessors added in this branch.

Scenario 3: AppHost typecheck against regenerated SDK (tsc --noEmit)

Objective: Confirm the public API is consumable and sound in real TypeScript caller code.
Coverage Type: Happy path
Status: PASS

npm run build (tsc --noEmit) exited 0. The 44 KB bench app host exercises every new member - getInteractionService, prompt input/inputs, message box, notification, withDynamicLoading, dynamic pick-zone / interaction-showcase commands, and validation callbacks - and it all typechecks cleanly against the regenerated PR SDK.

Scenario 4 (soundness): GetInputValue nullability contract

Objective: Validate an earlier review concern that the generated getInputValue typed as non-nullable Promise<string> could be unsound.
Coverage Type: Boundary / contract
Status: PASS (no defect)

The generated surface is getInputValue(inputName: string): Promise<string>. The C# implementation returns input.Value ?? string.Empty (and empty string when the input is absent) and is documented to never return null. The non-nullable generated type is therefore correct across languages; the earlier review concern is resolved by the empty-string contract.

Unhappy-Path Coverage

  • Validation rejection (name == "bad" / empty "solo"): the strongly-typed validationCallback paths compile against the new DTO callback contract (covered by Scenario 3). Fully triggering the rejection at runtime requires interactive dashboard input plus runnable resources, which this fixture intentionally lacks - reported as a limitation, not a failure.

Notes / Limitations

  • No full orchestration run: the TypeScript bench is a code-generation / typecheck validation fixture, not a runnable app. Its resources reference non-existent ./app, ./src/MyProject, ./src/CSharpApp paths and Azure resources (vnet/aks), so aspire start is not applicable. Validation here is correctly centered on SDK regeneration + typecheck, which exercise the full generated API contract end-to-end.

Summary

Scenario Status Notes
1. CLI install + version gate PASS 13.5.0-pr.17959.g0c5539d4 == head 0c5539d
2. SDK regeneration (restore) PASS All 3 layers + dynamic-loading handle + validationCallback + parity accessors present
3. AppHost typecheck (tsc) PASS Full bench compiles against regenerated SDK (exit 0)
4. GetInputValue contract PASS Non-nullable type is sound (empty-string contract)

Overall Result

PR VERIFIED for the polyglot IInteractionService API surface.

Recommendations

  • Consider adding a minimal runnable polyglot interaction sample (real resources, no Azure) so the dynamic-loading and validation flows can be exercised at runtime, not only typechecked.

Change InputsInteractionResult from a by-value DTO to an
[AspireExport(ExposeProperties = true)] handle so its inputs are surfaced as
the InteractionInputCollection handle. Polyglot callers can now read submitted
values by name (for example result.inputs().value("color")) using the same
accessors the validation and command-argument collections expose, instead of
scanning a serialized array by hand. canceled and inputs become cheap
RPC-backed accessors.

From() wraps callback-free input copies (ToResultInput) in a fresh collection
so the handle can be enumerated/serialized safely after the prompt completes.

Regenerated all 5 language snapshots and the ats.txt baseline, updated the
Go/Java/Python/TypeScript polyglot benches to use the by-name accessor, and
extended the round-trip test to assert by-name access.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 3 jobs were identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

Matched test failure patterns (1 test)
  • Aspire.Cli.EndToEnd.Tests.KubernetesDeployWithRabbitMQTests.DeployK8sWithRabbitMQ — Unable to access container registry during publish

@sebastienros

Copy link
Copy Markdown
Contributor Author

@JamesNK added by-name accessors to all the input collections so you don't have to scan arrays. It works the same on validation inputs, command arguments, and the multi-input prompt result:

// validation
const color = await ctx.inputs().value("color");

// multi-input result
const result = await interactions.promptInputs(...);
if (!await result.canceled()) {
  const color = await result.inputs().value("color");
}

The result is now a handle (like the validation context), so inputs() returns the same InteractionInputCollection. Accessors available: get/value (optional) and required/requiredValue (throw if missing).

sebastienros and others added 3 commits June 8, 2026 16:23
Collection-returning getters (`result.inputs()`, `validationContext.inputs()`,
and a command's `arguments()`) returned `Promise<InteractionInputCollection>`,
which forced a double await to reach the by-name accessors:
`await (await result.inputs()).value("color")`. The C#, Go, Java, and Python
surfaces all chain in a single step, so this was a TypeScript-only wart.

Introduce a hand-written `InteractionInputCollectionPromise` thenable in
base.mts that is awaitable to the collection but also forwards the by-name
accessors (value/get/required/requiredValue/toArray), and register
`InteractionInputCollection` as a promise-wrapper type so the generator emits
the fluent wrapper for every collection-returning getter. Callers can now write
`await result.inputs().value("color")`; the previous double-await form still
compiles because the wrapper resolves to the plain collection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The prompt-result-as-handle change made `PromptInputs` return a single
`InputsInteractionResult` handle (captured-error pattern) instead of
`(result, error)`, but the Go bench still used two-variable assignment,
breaking the Go SDK Validation build (`assignment mismatch: 2 variables but
PromptInputs returns 1 value`). Update both call sites to single-value
assignment; the error now surfaces on the terminal `Canceled()`/`Inputs()`
calls, consistent with the other handle wrappers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… present

The TypeScript generator only collapsed a lone `options` DTO parameter
directly when it was the *single* optional parameter. Methods that also
accept a trailing `cancellationToken` (e.g. the IInteractionService prompt
methods) therefore bundled both optionals into a generated
`{ options?, cancellationToken? }` object, forcing callers to write the
awkward double-nested `promptNotification(t, m, { options: { ... } })`.

Thread a trailing cancellation token as its own parameter so the options
DTO can be used directly alongside it, collapsing the public API to
`promptNotification(t, m, { intent, ... }, cancellationToken?)` while
keeping the named C# options object. This mirrors the C# IInteractionService
shape and removes the generated PromptXxxOptions wrapper interfaces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 113 passed, 0 failed, 2 unknown (commit 6d0e481)

View all recordings
- Test Detail
AddPackageInteractiveWhileAppHostRunningDetached Recording · Job · CLI logs
AddPackageWhileAppHostRunningDetached Recording · Job · CLI logs
AgentCommands_AllHelpOutputs_AreCorrect Recording · Job · CLI logs
AgentInitCommand_DefaultSelection_InstallsDefaultSkills Recording · Job · CLI logs
AgentInitCommand_MigratesDeprecatedConfig Recording · Job · CLI logs
AgentInit_NonInteractive_BundleOnlySkillsNotInCatalog Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated Recording · Job · CLI logs
AllPublishMethodsBuildDockerImages Recording · Job · CLI logs
AspireAddAndStartWorkAgainstLegacyAppHostTs Recording · Job · CLI logs
AspireAddPackageVersionToDirectoryPackagesProps Recording · Job · CLI logs
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost Recording · Job · CLI logs
AspireInit_ExistingAppHostDir_RecreatesNuGetConfigKeepsFiles Recording · Job · CLI logs
AspireInit_SolutionFile_BuildsAgainstChannelHive Recording · Job · CLI logs
AspireStartUpdatesStaleTypeScriptAppHostPath Recording · Job · CLI logs
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps Recording · Job · CLI logs
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent Recording · Job · CLI logs
Banner_DisplayedOnFirstRun Recording · Job · CLI logs
Banner_DisplayedWithExplicitFlag Recording · Job · CLI logs
Banner_NotDisplayedWithNoLogoFlag Recording · Job · CLI logs
CertificatesClean_RemovesCertificates Recording · Job · CLI logs
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate Recording · Job · CLI logs
CertificatesTrust_WithUntrustedCert_TrustsCertificate Recording · Job · CLI logs
ConfigSetGet_CreatesNestedJsonFormat Recording · Job · CLI logs
CreateAndRunAspireStarterProject Recording · Job · CLI logs
CreateAndRunAspireStarterProjectWithBundle Recording · Job · CLI logs
CreateAndRunEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunJavaEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunJsReactProject Recording · Job · CLI logs
CreateAndRunPolyglotAppHostWithDevLocalhostUrls Recording · Job · CLI logs
CreateAndRunPythonReactProject Recording · Job · CLI logs
CreateAndRunTypeScriptEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunTypeScriptStarterProject Recording · Job · CLI logs
CreateJavaAppHostWithViteApp Recording · Job · CLI logs
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain Recording · Job · CLI logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces Recording · Job · CLI logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost Recording · Job · CLI logs
DashboardRunWithOtelTracesReturnsNoTraces Recording · Job · CLI logs
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost Recording · Job · CLI logs
DeployK8sBasicApiService Recording · Job · CLI logs
DeployK8sWithExternalHelmChart Recording · Job · CLI logs
DeployK8sWithGarnet Recording · Job · CLI logs
DeployK8sWithMongoDB Recording · Job · CLI logs
DeployK8sWithMySql Recording · Job · CLI logs
DeployK8sWithPostgres Recording · Job · CLI logs
DeployK8sWithRabbitMQ Recording · Job · CLI logs
DeployK8sWithRedis Recording · Job · CLI logs
DeployK8sWithSqlServer Recording · Job · CLI logs
DeployK8sWithValkey Recording · Job · CLI logs
DeployTypeScriptAppToKubernetes Recording · Job · CLI logs
DescribeCommandResolvesReplicaNames Recording · Job · CLI logs
DescribeCommandShowsRunningResources Recording · Job · CLI logs
DetachFormatJsonProducesValidJson Recording · Job · CLI logs
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance Recording · Job · CLI logs
DoPublishAndDeployListStepsWork Recording · Job · CLI logs
DocsCommand_RendersInteractiveMarkdownFromLocalSource Recording · Job · CLI logs
DoctorCommand_DetectsDeprecatedAgentConfig Recording · Job · CLI logs
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain Recording · Job · CLI logs
DoctorCommand_WithSslCertDir_ShowsTrusted Recording · Job · CLI logs
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted Recording · Job · CLI logs
DotNetRunFileBasedAppHostUsesAspireCliBundle Recording · Job · CLI logs
DotNetRunProjectAppHostUsesAspireCliBundle Recording · Job · CLI logs
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance Recording · Job · CLI logs
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain Recording · Job · CLI logs
GlobalMigration_HandlesCommentsAndTrailingCommas Recording · Job · CLI logs
GlobalMigration_HandlesMalformedLegacyJson Recording · Job · CLI logs
GlobalMigration_PreservesAllValueTypes Recording · Job · CLI logs
GlobalMigration_SkipsWhenNewConfigExists Recording · Job · CLI logs
GlobalSettings_MigratedFromLegacyFormat Recording · Job · CLI logs
IngressWithoutExternalEndpoint_FailsPublishWithGuidance Recording · Job · CLI logs
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory Recording · Job · CLI logs
InteractiveCSharpInitCreatesExpectedFiles Recording · Job · CLI logs
InvalidAppHostPathWithComments_IsHealedOnRun Recording · Job · CLI logs
JavaScriptHostingApisRunFromTypeScriptAppHost Recording · Job · CLI logs
LatestCliCanStartStableChannelAppHost Recording · Job · CLI logs
LatestCliCanStartStableChannelTypeScriptAppHost Recording · Job · CLI logs
LegacySettingsMigration_AdjustsRelativeAppHostPath Recording · Job · CLI logs
LogsCommandShowsResourceLogs Recording · Job · CLI logs
OtelLogsReturnsStructuredLogsFromStarterApp Recording · Job · CLI logs
OtelLogsReturnsStructuredLogsFromStarterAppIsolated Recording · Job · CLI logs
ProcessCommandCallbackReceivesCliArguments Recording · Job · CLI logs
PsCommandListsRunningAppHost Recording · Job · CLI logs
PsFormatJsonOutputsOnlyJsonToStdout Recording · Job · CLI logs
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts Recording · Job · CLI logs
PublishWithConfigureEnvFileUpdatesEnvOutput Recording · Job · CLI logs
PublishWithDockerComposeServiceCallbackSucceeds Recording · Job · CLI logs
PublishWithoutOutputPathUsesAppHostDirectoryDefault Recording · Job · CLI logs
ResourceCommand_FailedExec_ShowsLogPathAndLogHasEntries Recording · Job · CLI logs
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput Recording · Job · CLI logs
RestoreGeneratesSdkFiles Recording · Job · CLI logs
RestoreGeneratesSdkFiles_WithConfiguredToolchain Recording · Job · CLI logs
RestoreRefreshesGeneratedSdkAfterAddingIntegration Recording · Job · CLI logs
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes Recording · Job · CLI logs
RunFromParentDirectory_UsesExistingConfigNearAppHost Recording · Job · CLI logs
RunReportsSyntaxErrorsForDotNetAppHost Recording · Job · CLI logs
RunReportsSyntaxErrorsForTypeScriptAppHost Recording · Job · CLI logs
SecretCrudOnDotNetAppHost Recording · Job · CLI logs
SecretCrudOnTypeScriptAppHost Recording · Job · CLI logs
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels Recording · Job · CLI logs
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets Recording · Job · CLI logs
StartReportsSyntaxErrorsForDotNetAppHost Recording · Job · CLI logs
StartReportsSyntaxErrorsForTypeScriptAppHost Recording · Job · CLI logs
StopAllAppHostsFromAppHostDirectory Recording · Job · CLI logs
StopJavaPolyglotAppHostUsingApphostDirectory Recording · Job · CLI logs
StopNonInteractiveSingleAppHost Recording · Job · CLI logs
StopTypeScriptPolyglotAppHostUsingApphostDirectory Recording · Job · CLI logs
StopWithNoRunningAppHostExitsSuccessfully Recording · Job · CLI logs
TypeScriptAppHostRunDoesNotDeadlockWhenLazyOptionsInvokeAsyncCallback Recording · Job · CLI logs
TypeScriptAppHostWithVite_AllowsDifferentGuestPkgManager Recording · Job · CLI logs
UnAwaitedChainsCompileWithAutoResolvePromises Recording · Job · CLI logs
UpdateToStable_CSharpEmptyAppHost_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_CSharpSingleFileInit_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_TypeScriptSingleFileInit_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_TypeScript_PreviewsStablePkgsAndKeepsChannel Recording · Job · CLI logs

📹 Recordings uploaded automatically from CI run #27174145002

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants