Skip to content

Commit ad65fcc

Browse files
refactor(tests): distill safe-output smoke suite from 27 to 5 pipelines (#1498)
* feat(executor-e2e): add signal scenarios for noop/missing-tool/missing-data/report-incomplete Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> * feat(safe-outputs): add canary.md omnibus smoke pipeline Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> * chore(safe-outputs): remove 23 per-tool smoke pairs; update docs and failure reporter Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> * fix: update test fixtures from deleted create-pull-request.md to canary.md/complete-agent.md Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
1 parent 928cd59 commit ad65fcc

58 files changed

Lines changed: 298 additions & 21812 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tests/fixtures/stage-agent.lock.yml linguist-generated=true merge=ours text eol=
1919
tests/safe-outputs/add-build-tag.lock.yml linguist-generated=true merge=ours text eol=lf
2020
tests/safe-outputs/add-pr-comment.lock.yml linguist-generated=true merge=ours text eol=lf
2121
tests/safe-outputs/azure-cli.lock.yml linguist-generated=true merge=ours text eol=lf
22+
tests/safe-outputs/canary.lock.yml linguist-generated=true merge=ours text eol=lf
2223
tests/safe-outputs/comment-on-work-item.lock.yml linguist-generated=true merge=ours text eol=lf
2324
tests/safe-outputs/create-branch.lock.yml linguist-generated=true merge=ours text eol=lf
2425
tests/safe-outputs/create-git-tag.lock.yml linguist-generated=true merge=ours text eol=lf

scripts/ado-script/src/executor-e2e/scenarios/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { buildScenarios } from "./build.js";
77
import { createPullRequestScenarios } from "./create-pull-request.js";
88
import { gitScenarios } from "./git.js";
99
import { prScenarios } from "./pr.js";
10+
import { signalScenarios } from "./signals.js";
1011
import { wikiScenarios } from "./wiki.js";
1112
import { workItemScenarios } from "./work-item.js";
1213

1314
/** Every scenario, in a deterministic run order. */
1415
export const allScenarios: Scenario<unknown>[] = [
16+
...signalScenarios,
1517
...workItemScenarios,
1618
...wikiScenarios,
1719
...prScenarios,
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Signal safe-output scenarios: noop, missing-tool, missing-data,
3+
* report-incomplete.
4+
*
5+
* These tools have no ADO write path — they emit a signal record to
6+
* `safe_outputs.ndjson` and the executor writes back the result without
7+
* touching any ADO API. Accordingly:
8+
* - `setup()` is trivial (no REST calls, returns an empty state).
9+
* - `assert()` is a no-op for the succeeded tools; `report-incomplete`
10+
* uses `expectedFailure` so `assert()` is never reached.
11+
* - `cleanup()` is a no-op.
12+
*
13+
* Adding these scenarios closes the executor-e2e coverage gap left by
14+
* removing the dedicated per-tool agentic smoke pipelines.
15+
*
16+
* Test-harness module; not shipped in `ado-script.zip`.
17+
*/
18+
import type { Scenario } from "../scenario.js";
19+
20+
export const noop: Scenario<unknown> = {
21+
tool: "noop",
22+
config: () => ({}),
23+
setup: async () => ({}),
24+
ndjson: async (ctx) => ({
25+
context: `deterministic executor e2e noop for build ${ctx.buildId}`,
26+
}),
27+
assert: async () => {
28+
/* no ADO side effect to verify */
29+
},
30+
cleanup: async () => {
31+
/* nothing to tear down */
32+
},
33+
};
34+
35+
export const missingTool: Scenario<unknown> = {
36+
id: "missing-tool",
37+
tool: "missing-tool",
38+
config: () => ({}),
39+
setup: async () => ({}),
40+
ndjson: async (ctx) => ({
41+
tool_name: `ado-aw-det-${ctx.buildId}-bash`,
42+
context: `deterministic executor e2e missing-tool for build ${ctx.buildId}`,
43+
}),
44+
assert: async () => {
45+
/* no ADO side effect to verify */
46+
},
47+
cleanup: async () => {
48+
/* nothing to tear down */
49+
},
50+
};
51+
52+
export const missingData: Scenario<unknown> = {
53+
id: "missing-data",
54+
tool: "missing-data",
55+
config: () => ({}),
56+
setup: async () => ({}),
57+
ndjson: async (ctx) => ({
58+
data_type: "deterministic-e2e-data-type",
59+
reason: `deterministic executor e2e missing-data for build ${ctx.buildId}`,
60+
}),
61+
assert: async () => {
62+
/* no ADO side effect to verify */
63+
},
64+
cleanup: async () => {
65+
/* nothing to tear down */
66+
},
67+
};
68+
69+
export const reportIncomplete: Scenario<unknown> = {
70+
id: "report-incomplete",
71+
tool: "report-incomplete",
72+
config: () => ({}),
73+
setup: async () => ({}),
74+
ndjson: async (ctx) => ({
75+
// reason must be >= 10 characters (validated by ReportIncompleteParams).
76+
reason: `deterministic executor e2e report-incomplete for build ${ctx.buildId}`,
77+
context: `build ${ctx.buildId}`,
78+
}),
79+
// report-incomplete always returns ExecutionResult::failure(), so the
80+
// executor writes status="failed". Declare that as the expected outcome so
81+
// the runner treats it as a pass rather than a suite failure.
82+
expectedFailure: {
83+
status: "failed",
84+
error: /Agent reported task incomplete/,
85+
},
86+
assert: async () => {
87+
throw new Error("report-incomplete expected failure should have been caught before assert");
88+
},
89+
cleanup: async () => {
90+
/* nothing to tear down */
91+
},
92+
};
93+
94+
export const signalScenarios: Scenario<unknown>[] = [
95+
noop,
96+
missingTool,
97+
missingData,
98+
reportIncomplete,
99+
];

src/audit/pipeline_graph.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ mod tests {
219219
let source_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
220220
.join("tests")
221221
.join("safe-outputs")
222-
.join("create-pull-request.md");
222+
.join("canary.md");
223223
let aw_info = serde_json::json!({
224224
"source": source_path.display().to_string(),
225225
"target": "standalone"

src/inspect/lint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,8 @@ mod tests {
449449
async fn create_pull_request_fixture_has_no_unused_output_inspect_lint() {
450450
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
451451
.join("tests")
452-
.join("safe-outputs")
453-
.join("create-pull-request.md");
452+
.join("fixtures")
453+
.join("complete-agent.md");
454454
let (_fm, pipeline) = crate::compile::build_pipeline_ir(&fixture).await.unwrap();
455455
let summary = PipelineSummary::from_pipeline(&pipeline).unwrap();
456456
let findings = lint(&summary);

src/mcp_author/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fn fixture_path() -> String {
1111
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1212
.join("tests")
1313
.join("safe-outputs")
14-
.join("create-pull-request.md")
14+
.join("canary.md")
1515
.display()
1616
.to_string()
1717
}

tests/executor-e2e/README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ This directory holds a **deterministic**, non-agentic end-to-end test of the
55

66
## Why this exists
77

8-
The daily smoke suite in [`tests/safe-outputs/`](../safe-outputs/) exercises
9-
each safe output end-to-end, but it drives Stage 3 by having an **LLM agent
10-
(Stage 1) emit the safe output** — so a failure can be the model's fault and the
11-
suite is inherently flaky.
8+
The agentic smoke suite in [`tests/safe-outputs/`](../safe-outputs/) exercises
9+
the full Stage 1 → 2 → 3 pipeline shape, but it drives Stage 3 by having an
10+
**LLM agent (Stage 1) emit the safe output** — so a failure can be the model's
11+
fault and the suite is inherently flaky. It also only runs a small number of
12+
omnibus pipelines rather than one per tool.
1213

1314
This suite removes the LLM from the loop. For every ADO-write safe output it:
1415

@@ -41,8 +42,10 @@ gitignored, non-root path) and is **deliberately excluded** from the released
4142
## Coverage
4243

4344
All deterministically-assertable ADO-write safe outputs plus the flagship
44-
`create-pull-request`:
45+
`create-pull-request`, and the four signal-only tools:
4546

47+
- **Signals:** `noop`, `missing-tool`, `missing-data`, `report-incomplete`
48+
(no ADO write path; assert that the executor emits the expected status)
4649
- **Work items:** `create-work-item`, `update-work-item`,
4750
`comment-on-work-item`, `link-work-items`, `upload-workitem-attachment`
4851
- **Wiki:** `create-wiki-page`, `update-wiki-page`
@@ -53,9 +56,12 @@ All deterministically-assertable ADO-write safe outputs plus the flagship
5356
`upload-pipeline-artifact`
5457
- **Flagship:** `create-pull-request`
5558

56-
Excluded (nothing to assert or out of scope): the NDJSON-only tools (`noop`,
57-
`missing-data`, `missing-tool`, `report-incomplete`) and the GitHub-only
58-
`create-issue`.
59+
Excluded (out of scope or GitHub-only): the GitHub-only `create-issue`.
60+
61+
> **Coverage note.** The signal scenarios (`noop`, `missing-tool`,
62+
> `missing-data`, `report-incomplete`) were previously exercised only by
63+
> now-deleted per-tool agentic smoke pipelines. Adding them here closes
64+
> the coverage gap while keeping the test deterministic.
5965
6066
### Scenarios that skip when a precondition is missing
6167

tests/inspect_integration.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fn fixture_copy(fixture_name: &str) -> (tempfile::TempDir, PathBuf) {
2727

2828
#[test]
2929
fn inspect_emits_pipeline_summary_text() {
30-
let (_workspace, src) = fixture_copy("create-pull-request.md");
30+
let (_workspace, src) = fixture_copy("canary.md");
3131
let out = Command::new(binary_path())
3232
.arg("inspect")
3333
.arg(&src)
@@ -55,7 +55,7 @@ fn inspect_emits_pipeline_summary_text() {
5555

5656
#[test]
5757
fn inspect_json_emits_schema_version_one() {
58-
let (_workspace, src) = fixture_copy("create-pull-request.md");
58+
let (_workspace, src) = fixture_copy("canary.md");
5959
let out = Command::new(binary_path())
6060
.arg("inspect")
6161
.arg(&src)
@@ -85,7 +85,7 @@ fn inspect_json_emits_schema_version_one() {
8585

8686
#[test]
8787
fn graph_dot_emits_digraph_with_known_edges() {
88-
let (_workspace, src) = fixture_copy("create-pull-request.md");
88+
let (_workspace, src) = fixture_copy("canary.md");
8989
let out = Command::new(binary_path())
9090
.arg("graph")
9191
.arg("dump")
@@ -119,7 +119,7 @@ fn graph_dot_emits_digraph_with_known_edges() {
119119

120120
#[test]
121121
fn graph_rejects_unknown_format() {
122-
let (_workspace, src) = fixture_copy("create-pull-request.md");
122+
let (_workspace, src) = fixture_copy("canary.md");
123123
let out = Command::new(binary_path())
124124
.arg("graph")
125125
.arg("dump")

0 commit comments

Comments
 (0)