-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathscenario.ts
More file actions
148 lines (143 loc) · 5.97 KB
/
Copy pathscenario.ts
File metadata and controls
148 lines (143 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* Deterministic Stage 3 (executor) end-to-end scenario contract.
*
* Each scenario exercises exactly one safe-output tool without any LLM in the
* loop: it sets up preconditions via the ADO REST API, crafts the executor's
* NDJSON input directly, runs the real `ado-aw execute` binary, asserts the
* effect via the ADO REST API, and cleans up everything it created.
*
* This module is a **test harness** — it deliberately does NOT ship in the
* released `ado-script.zip` (see package.json `build:executor-e2e` and the
* `NON_BUNDLE_DIRS` carve-out in `src/__tests__/bundle-coverage.test.ts`).
*/
import type { AdoRest } from "./ado-rest.js";
/** One parsed line of `safe-outputs-executed.ndjson` written by Stage 3. */
export interface ExecutedRecord {
/** snake_case tool name (dashes replaced by the executor). */
name: string;
/** "succeeded" | "failed" | "warning" | "budget_exhausted". */
status: string;
context?: string | null;
/** Present only on success; carries the tool's result data. */
result?: Record<string, unknown> | null;
/** Present only on non-success; the failure message. */
error?: string | null;
timestamp?: string;
}
/** Shared, read-only context handed to every scenario phase. */
export interface ScenarioContext {
/** ADO collection URI, e.g. https://dev.azure.com/msazuresphere/ */
readonly orgUrl: string;
/** ADO project name, e.g. AgentPlayground. */
readonly project: string;
/** ADO Git repo used by PR/git scenarios (initialised `main`). */
readonly adoRepo: string;
/** Current build id (drives the per-run object prefix). */
readonly buildId: string;
/** Write-capable ADO token (System.AccessToken or a PAT). */
readonly token: string;
/** Absolute path to the `ado-aw` binary under test. */
readonly adoAwBin: string;
/** Per-scenario scratch root; each scenario gets a child dir. */
readonly workDir: string;
/** REST helper bound to {orgUrl, project, token}. */
readonly rest: AdoRest;
/** Structured logger (writes to the pipeline log). */
readonly log: (msg: string) => void;
/** Deterministic object-name prefix: `ado-aw-det-<buildId>-<tool>`. */
readonly prefix: (tool: string) => string;
}
/**
* A single deterministic executor scenario.
*
* `State` is threaded from `setup` through the later phases so a scenario can
* remember the ids it created (work-item id, PR id, thread id, …) for
* assertion and cleanup.
*/
export interface Scenario<State = unknown> {
/**
* Unique harness id for reporting and scratch directories. Defaults to
* `tool`; set this when multiple scenarios exercise the same safe-output.
*/
readonly id?: string;
/** kebab-case safe-output tool name (matches the `safe-outputs:` key). */
readonly tool: string;
/**
* When true, the scenario targets the ADO `agent-definitions` repo rather
* than the pipeline's own repo, so the runner emits a `repos:` block and the
* per-tool `allowed-repositories` config.
*/
readonly targetsAdoRepo?: boolean;
/** Per-tool `safe-outputs: <tool>:` front-matter config fragment. */
config(ctx: ScenarioContext, state: State): Record<string, unknown>;
/**
* Create ADO preconditions; return remembered state.
*
* When this throws (a non-`SkipError`), the runner will NOT call `cleanup()`
* (state is considered unreliable). Any ADO objects partially created before
* the throw must therefore be torn down explicitly inside this function
* before rethrowing — see `pr.ts` `setupPr` for the pattern.
*
* Note: this applies only to failures in `setup()` itself. Once `setup()`
* completes, a later throw in `config()`/`ndjson()`/`files()`/`env()` or
* `assert()` DOES run `cleanup()` (the runner gates cleanup on `setupDone`).
*/
setup(ctx: ScenarioContext): Promise<State>;
/**
* Build the executor NDJSON entry (WITHOUT the `name` field — the runner
* injects `name: <tool>`).
*/
ndjson(ctx: ScenarioContext, state: State): Promise<Record<string, unknown>>;
/**
* Optional extra files to stage into the safe-output dir before running the
* executor (relative path -> UTF-8 contents). Used by attachment and
* create-pull-request scenarios.
*/
files?(ctx: ScenarioContext, state: State): Promise<Record<string, string>>;
/**
* Optional per-scenario environment overrides for the `ado-aw execute`
* child process (e.g. BUILD_SOURCESDIRECTORY pointing at a git checkout).
*/
env?(ctx: ScenarioContext, state: State): Promise<Record<string, string>>;
/**
* Some scenarios intentionally submit invalid staged output and should pass
* only when the executor rejects it with the expected failure.
*/
readonly expectedFailure?: {
readonly status?: string;
readonly error: RegExp;
};
/**
* Assert the ADO side-effect actually happened. Throw on failure.
*
* May populate fields on `state` (e.g. an id read from the executor result)
* that `cleanup` needs — do this **before** any fallible check so cleanup can
* still tear the object down if a later assertion throws.
*/
assert(ctx: ScenarioContext, state: State, record: ExecutedRecord): Promise<void>;
/** Best-effort teardown of everything setup/execute created. */
cleanup(ctx: ScenarioContext, state: State): Promise<void>;
}
/** Outcome of running one scenario. */
export interface ScenarioResult {
tool: string;
ok: boolean;
/** "setup" | "execute" | "assert" | "cleanup" | "skipped". */
phase?: string;
message?: string;
durationMs: number;
/** True when the scenario was skipped for a missing precondition (not a failure). */
skipped?: boolean;
}
/**
* Thrown by a scenario's `setup` when a required precondition is unavailable
* in this environment (e.g. no wiki exists, or the target pipeline id was not
* supplied). The runner records the scenario as **skipped** rather than
* failed, so an incomplete manual handoff never turns the whole suite red.
*/
export class SkipError extends Error {
constructor(reason: string) {
super(reason);
this.name = "SkipError";
}
}