test: add deterministic Stage 3 executor safe-output E2E suite#1353
Conversation
Adds a no-LLM end-to-end test of the `ado-aw execute` (Stage 3) executor: a TypeScript ado-script harness crafts the executor NDJSON directly, sets up ADO preconditions via REST, runs the real binary, asserts effects, cleans up, and files a deduped GitHub issue on failure. Covers all ADO-write safe outputs including the flagship create-pull-request. The harness is test-only and excluded from the shipped ado-script.zip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-structured addition with good test hygiene — a few real issues worth fixing before merging. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
- github-issue: URL-encode SYSTEM_TEAMPROJECT in the build URL - create-pull-request: record PR id before fallible asserts so cleanup always abandons it; pass git auth via GIT_CONFIG_* env instead of -c argv so the token is not exposed in /proc/<pid>/cmdline - execute-cli: add a bounded timeout to the ado-aw execute child so a hung run fails fast instead of blocking the suite - runner: catch execute errors as a clean scenario failure; type runAll as Scenario<unknown>[] - ado-rest: move the readonly token field up with the other declarations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-engineered test harness with a clean scenario contract, correct cleanup semantics, and solid security hygiene. A few minor issues worth noting. Findings
|
- ado-rest: add a per-request AbortSignal timeout and route getWikiPage through the centralized authedFetch so auth + timeout stay in one place (covers hung ADO endpoints during the assert phase) - create-pull-request: log git commands via ctx (token rides in env, not argv) so the previously-unused ctx param has a purpose - execute-cli: quote the safe-outputs tool key as a JSON string (valid YAML) so an unusual tool name cannot emit broken YAML Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Overall solid work — the harness design is clean and well-structured. A few issues worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
- runner: run cleanup whenever setup succeeded (guard on setupDone only), so a scenario with a void setup is not silently skipped - ado-rest: throw on an unexpected non-JSON response instead of casting the body to T - create-pull-request: add a timeout + GIT_TERMINAL_PROMPT=0 to runGit; remove the cloned checkout (sourcesDir) in cleanup - scenario: document that assert() may populate state fields for cleanup - pipeline: replace the `curl | sh` rustup install with the internal CargoAuthenticate@0 + RustInstaller@1 tasks (ms-stable, internal feeds) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-structured addition — clean design, good security hygiene, solid unit test coverage. One correctness gap in the runner's error-containment logic is worth fixing; a dead-code fallback in the git scenario setup is worth noting. Findings🐛 Bugs / Logic Issues
|
CargoAuthenticate@0 only attaches credentials to feeds declared in the config file it is given; the checked-in .cargo/config.toml carries only the Windows MSVC stack setting. Add a step that appends the internal sparse crates.io source replacement to .cargo/config.toml before CargoAuthenticate runs, and reuse the feed URL (CRATES_IO_FEED var) as RustInstaller's cratesIoFeedOverride so the two never drift. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid test infrastructure — good security hygiene, clean architecture. A few items worth fixing before the first live run. Findings🐛 Bugs / Logic Issues
|
- resolve-pr-thread: allowlist resolved ADO thread statuses (numeric or string) instead of denylisting only active, and widen getThread status type to string|number - extract duplicated defaultBranchShortName into scenarios/common.ts - render name/description YAML values via JSON.stringify to avoid broken front-matter for tool names with quotes/backslashes - wrap runner auxiliary methods (config/ndjson/files/env) in error handling so a harness bug records a failure instead of aborting runAll - replace Scenario<any>[] with Scenario<unknown>[] across scenario arrays - make create-pull-request origin/HEAD symref fallback to main reachable via try/catch (git() throws on non-zero exit) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-structured TypeScript E2E harness with solid security posture. One genuine bug and one minor consistency issue. Findings🐛 Bugs / Logic Issues
|
- main() now wraps the run in try/finally and removes the mkdtemp workDir so CI/local runs don't accumulate ado-aw-e2e-* directories - renderSourceMarkdown JSON-stringifies the repos alias=name entry for consistency and to guard against YAML-significant repo names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-structured test harness with one real bug in a utility function and a minor test gap worth addressing. Findings🐛 Bugs / Logic Issues
|
Use the msazuresphere/AgentPlayground/_packaging/cargo feed via the [registries] cargo + [source.crates-io] replace-with form, scoped to the e2e pipeline's generated .cargo/config.toml only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-structured test harness — the core architecture is sound and security-conscious. Three issues worth addressing, one of which is a real resource-leak bug. Findings🐛 Bugs / Logic Issues
|
Drop the CargoAuthenticate@0 + RustInstaller@1 + internal crates.io feed setup and go back to installing the toolchain directly with rustup (curl | sh, guarded by a cargo-present check). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- renderNdjsonLine: spread entry before name so the injected tool name is canonical and a scenario can't accidentally override it - create-pull-request: wire the resolved default branch into the executor target-branch config instead of leaving targetBranch as dead state / hardcoding main - pr.ts setupPr: abandon the PR + delete the source branch if createThread throws, so a partial setup failure doesn't leak dangling ADO objects - ado-rest: drop the unused public AdoRest.token field (callers use ctx.token) - runner.test: enforce the cleanup-skip invariant via explicit flags instead of a swallowed cleanup throw Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: All previously-flagged bugs have been addressed — the harness is now in good shape. One genuine deduplication gap remains, plus a minor defensive-coding suggestion. Findings🐛 Bugs / Logic Issues
|
- findOpenIssueByTitle: bump per_page 20 -> 100 so partial-phrase search matches can't push an existing issue off page 1 and cause a duplicate - pr.ts: replace state.threadId! non-null assertions with explicit invariant guards for clearer errors than an ADO 404 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-structured deterministic test harness with one concrete test-logic bug and a few minor concerns worth addressing. Findings🐛 Bugs / Logic Issues
|
- resolve-pr-thread: accept only 'fixed'/2 so an executor mapping regression to wontFix/closed/byDesign is caught, not passed - create-work-item: populate state.createdId before the fallible numeric id check so cleanup deletes the item on a non-numeric result (honours the Scenario.assert contract) - index.ts: log err.stack (fall back to message) in the crash handler - ado-rest putWikiPage: document the no-ETag limitation (create-only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-architected test harness with correct security practices; two small issues worth addressing. Findings🐛 Bugs / Logic Issues
|
Bring back the CargoAuthenticate@0 + RustInstaller@1 + internal crates.io feed setup, but omit RustInstaller's toolchainFeed input to test whether the task resolves the toolchain from its default source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-architected test harness with solid security practices and comprehensive scenario coverage. One unresolved bug from the previous review (the Findings🐛 Bugs / Logic Issues
✅ What Looks Good
|
…ntent - runner.ts: wrap the scenarioDir mkdir in its own try/catch that records a failed ScenarioResult, so a disk-full/permission error no longer propagates out of runScenario and aborts runAll's remaining scenarios - pr.ts replyToPrComment: guard c.content with ?? before .includes so a null system-comment content yields a clean 'reply not found' instead of a TypeError; widen getThread comment content type to string|null to match Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-structured deterministic test harness with sound error handling and correct security practices. Two minor issues worth flagging. Findings🐛 Bugs / Logic Issues
|
…d feed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-engineered test harness with one real bug in the wiki scenario setup and a couple of minor issues worth tightening. Findings🐛 Bugs / Logic Issues
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ado-rest putWikiPage: accept an optional If-Match ETag (and add generic RequestOptions.headers); update-wiki-page setup now reads the existing page's ETag and does a conditional PUT so a leftover page from a failed run is overwritten instead of throwing ADO 412 - pr.ts: guard state.threadId in reply/resolve ndjson so a broken setup fails loudly instead of silently dropping thread_id - execute-cli: reject scenario files() keys that escape the scratch dir Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-structured test harness with strong isolation, cleanup discipline, and good security hygiene. A few targeted findings below. Findings
|
- create-pull-request runGit: inject the auth header at the next free GIT_CONFIG index instead of clobbering GIT_CONFIG_COUNT to 1, preserving any agent/variable-group GIT_CONFIG_KEY/VALUE_N entries - scenario.ts: document that setup() must tear down partially created ADO objects itself before throwing (runner skips cleanup on setup failure) - work-item upload-workitem-attachment: assert the AttachedFile relation matches the uploaded 'attachment.txt' name, not just any attachment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-isolated test harness with solid security hygiene. Two actionable issues worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
…emantics - execute-cli/create-pull-request: include truncated stdout/stderr in the spawnCollect + runGit timeout errors (shared partialOutput helper) so a hung run is diagnosable from the error body, not just raw ADO logs - scenario.ts: clarify that cleanup() DOES run for post-setup failures in config/ndjson/files/env/assert (gated on setupDone), only setup() throws skip it - create-pull-request: document the git add -N untracked-path assumption Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-structured test harness with one resource-leak bug and a performance concern in the flagship scenario. Findings🐛 Bugs / Logic Issues
|
- ado-rest pushAddFileBranch: use zeros oldObjectId + commit parents:[base] for a NEW branch (matches the Rust executor); the old oldObjectId=base form would be rejected and fail all PR scenarios in a live run - pr.ts setupPr: delete the pushed source branch if createPullRequest throws so a partial setup failure doesn't leak the branch (contract compliance) - ado-rest getRefObjectId: match the exact ref name, not the first prefix match (heads/main also matches heads/main-foo) - common.ts numResult: reject non-number values (Number(null/''/false/[]) all coerce to a finite 0, silently passing an invalid id) - github-issue renderIssueBody: collapse newlines in table cells so a multi-line message can't terminate the row and corrupt the report Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-designed test harness — solid architecture, thoughtful safety rails, and good coverage. A couple of correctness issues worth addressing before the live run. Findings🐛 Bugs / Logic Issues
|
… docs - teardownPr: guard abandonPullRequest so deleteRef always runs (no orphaned branch) - submitPrReview.assert: require exact vote===10 to catch wrong-vote regressions - runner.ts: point-of-action comment that cleanup is skipped when setup throws - README: document EXECUTOR_E2E_*_TIMEOUT_MS tuning knobs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sequential-await cleanups (await a(); await b();) silently skip later steps when an earlier one throws, leaking resources. Introduce a Teardown class that attempts every registered step and aggregates failures into one error, so no step is skipped and genuine failures still surface as cleanup WARNINGs. Also fixes two more instances of the leak the reviewer flagged in teardownPr: - create-pull-request cleanup (abandon PR could skip branch + local dir delete) - link-work-items cleanup (source delete could skip target delete) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: All previously flagged issues are fixed. Two new issues worth addressing before merge — one correctness bug, one resilience gap. Findings🐛 Bugs / Logic Issues
|
🔍 Rust PR ReviewSummary: Solid, well-structured test harness — looks good to merge with one medium-priority fix and a couple of minor notes. Findings
|
…I timeouts - createWorkItem/createPullRequest assert: use numResult (checks Number.isFinite) instead of bare typeof===number, which accepts NaN and would leak as deleteWorkItem(NaN)/abandonPullRequest(NaN) in cleanup - github-issue.ts: add AbortSignal.timeout (30s default, matching AdoRest) to findOpenIssueByTitle and createGitHubIssue so a hung GitHub API response can't stall the pipeline step after all scenarios complete Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — high-quality test harness with thoughtful error handling and security hardening throughout. Findings
|
… path Both verified by a live run against msazuresphere/AgentPlayground: - execute-cli: default BUILD_SOURCESDIRECTORY to the safe-output dir so the executor resolves relative file_path payloads (upload-workitem-attachment) against where files() stages them; create-pull-request still overrides it. - wiki: use a single flat page path segment (ado-aw-det-<buildId>-<tool>); ADO wiki does not auto-create ancestor pages, so the nested path 404'd with WikiAncestorPageNotFoundException for both create/update. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elf-approval guard The executor blocks positive self-votes (approve/approve-with-suggestions) on a PR the authenticated identity created (submit_pr_review.rs self-approval guard). The harness — like the real single-identity pipeline — creates and reviews the PR with the same identity, so an 'approve' could never succeed. Switch to 'request-changes' (vote=-5), which exercises the same executor submit path without tripping the guard, and assert the exact vote. Verified by a live run against msazuresphere/AgentPlayground: 15 passed, 0 failed, 4 skipped (build-context scenarios). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-structured test harness with thoughtful defensive patterns. A few items worth addressing. Findings🐛 Bugs / Logic Issues
|
Summary
Adds a deterministic, no-LLM end-to-end test of the Stage 3 (
ado-aw execute) safe-output executor.The existing daily suite in
tests/safe-outputs/drives Stage 3 by having an LLM agent (Stage 1) emit each safe output, so it validates the full agentic flow but is inherently non-deterministic/flaky. This new suite removes the agent from the loop: for every ADO-write safe output itsafe_outputs.ndjsoninput directly (fixed literal values),ado-aw executebinary (built from the checkout),and, on any failure, files a deduped
[executor-e2e-failure]GitHub issue ongithubnext/ado-awand fails the build.What's included
scripts/ado-script/src/executor-e2e/(TypeScript): scenario contract, focused ADO REST client,ado-aw executewrapper, runner, entry point, and direct GitHub issue filer.agent-definitions, generates a realgit diffpatch +base_commit+patch_sha256, asserts the PR + pushed branch, then abandons + deletes).tests/executor-e2e/azure-pipelines.yml(daily + manual, builds HEAD, runs againstmsazuresphere/AgentPlayground) with a registration/handoffREADME.md.ado-script.zip:executor-e2eadded toNON_BUNDLE_DIRS, abuild:executor-e2escript emitting to gitignoredtest-bin/, kept out of the mainbuildchain.docs/ado-script.mdandtests/safe-outputs/README.md.Test plan
npm run typecheck— clean.npm test(vitest) — 477 pass, including 20 new offline unit tests (ndjson/source rendering, executed-record parsing, issue title/body/dedupe, runner skip/setup-failure handling, bundle-coverage carve-out).ado-aw execute --dry-runagainst a synthesized source + NDJSON — confirmed the source/config/NDJSON plumbing and executed-record output.npm run build:executor-e2e— bundle builds totest-bin/,git check-ignoreconfirms it's ignored, and no rootado-script/*.jsis produced (so it never enters the release glob).Remaining (one-time manual ADO handoff, documented in
tests/executor-e2e/README.md): register the pipeline in AgentPlayground, grant the build identity write access onagent-definitions, and set theEXECUTOR_E2E_GITHUB_TOKENsecret — after which the first live run exercises the scenarios end-to-end.