@capagents/forgeqe 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HOW_TO_USE.md +40 -3
- package/README.md +1 -1
- package/docs/COST_ARCHITECTURE.md +280 -0
- package/docs/feature-test-prompts.md +95 -0
- package/docs/user-guide.html +54 -7
- package/evals/prompts/brain/route-api-scenarios.yaml +9 -0
- package/evals/prompts/brain/route-e2e.yaml +10 -0
- package/evals/prompts/brain/route-flaky.yaml +9 -0
- package/evals/prompts/brain/route-heal.yaml +9 -0
- package/evals/prompts/brain/route-regression-pack.yaml +9 -0
- package/evals/prompts/brain/route-review-update.yaml +10 -0
- package/evals/prompts/brain/route-run-cases.yaml +9 -0
- package/evals/prompts/brain/route-story-cases.yaml +12 -0
- package/forgeqe.example.yaml +15 -0
- package/package.json +1 -1
- package/prompts/README.md +30 -0
- package/prompts/brain.md +34 -49
- package/prompts/browser-checks.md +2 -0
- package/py/forgeqe_engine/checks.py +8 -2
- package/py/forgeqe_engine/llm.py +33 -5
- package/py/forgeqe_engine/memory.py +59 -2
- package/py/forgeqe_engine/observe.py +123 -0
- package/py/forgeqe_engine/replay.py +55 -2
- package/py/forgeqe_engine/service.py +155 -3
- package/src/agents.ts +52 -0
- package/src/brain/index.ts +8 -2
- package/src/capabilities/registry.ts +48 -6
- package/src/cases/ado.ts +1 -1
- package/src/cases/journey.ts +61 -0
- package/src/cases/readiness.ts +35 -6
- package/src/cases/save.ts +241 -48
- package/src/cases/types.ts +11 -0
- package/src/cli/help.ts +9 -0
- package/src/cli/index.ts +71 -1
- package/src/code/coverage.ts +22 -3
- package/src/code/decisions.ts +73 -0
- package/src/code/find.ts +3 -1
- package/src/code/graph/graph.ts +33 -2
- package/src/code/loop.ts +17 -1
- package/src/code/notes.ts +48 -1
- package/src/code/prompt.ts +31 -9
- package/src/code/protect.ts +103 -0
- package/src/code/review.ts +2 -0
- package/src/code/runtime.ts +35 -3
- package/src/code/shortlist.ts +309 -0
- package/src/code/testrun.ts +3 -1
- package/src/code/traceability.ts +2 -0
- package/src/commands.ts +6 -1
- package/src/config.ts +46 -0
- package/src/context.ts +75 -0
- package/src/engine/bridge.ts +47 -5
- package/src/mcp/captools.ts +19 -0
- package/src/mcp/casetools.ts +107 -11
- package/src/mcp/commands.ts +74 -20
- package/src/mcp/packtools.ts +187 -0
- package/src/mcp/routing.ts +145 -0
- package/src/mcp/server.ts +7 -3
- package/src/mcp/tools.ts +241 -29
- package/src/pack/collect.ts +251 -0
- package/src/pack/index.ts +134 -0
- package/src/pack/init.ts +45 -0
- package/src/pack/pack.ts +327 -0
- package/src/pack/pipeline.ts +373 -0
- package/src/pack/run.ts +155 -0
- package/src/registry/db.ts +249 -0
- package/src/registry/index.ts +72 -0
- package/src/registry/resolve.ts +57 -0
- package/src/registry/tier0.ts +81 -0
- package/src/report/data.ts +7 -0
- package/src/report/html.ts +14 -0
- package/src/report/template.html +52 -48
- package/src/report/usage.ts +113 -0
- package/src/run.ts +22 -2
- package/src/suite/run.ts +12 -5
- package/src/summary.ts +1 -0
- package/src/tokenomics/budget.ts +172 -0
- package/src/tokenomics/ledger.ts +287 -0
- package/src/tui/App.tsx +89 -2
- package/src/tui/inspector.tsx +40 -35
- package/src/types.ts +9 -2
|
@@ -53,6 +53,13 @@ export type Capability = {
|
|
|
53
53
|
/** What it does not do, so the agent never promises it. */
|
|
54
54
|
limits?: string[]
|
|
55
55
|
docs: DocRef[]
|
|
56
|
+
/**
|
|
57
|
+
* The sections of brain.md (### headings) this capability owns: they leave the system prompt and come on demand, with
|
|
58
|
+
* `when` in the index of playbooks that stays in it. A section no capability owns stays in the system prompt.
|
|
59
|
+
*/
|
|
60
|
+
playbook?: { heading: string; when: string }[]
|
|
61
|
+
/** Tools of other capabilities whose first result also carries this capability's playbook. */
|
|
62
|
+
alsoAfter?: string[]
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
export const CAPABILITIES: Capability[] = [
|
|
@@ -111,21 +118,26 @@ export const CAPABILITIES: Capability[] = [
|
|
|
111
118
|
area: "Test cases",
|
|
112
119
|
title: "Check and improve test cases",
|
|
113
120
|
summary:
|
|
114
|
-
"Checks test cases before they run (vague or missing expected results, missing test data, placeholders, duplicates) against the story, feature, and epic they test, then rewrites Azure DevOps cases once the user agrees.",
|
|
121
|
+
"Checks test cases before they run (vague or missing expected results, missing test data, placeholders, duplicates) against the story, feature, and epic they test, then rewrites Azure DevOps or Jira cases in place once the user agrees.",
|
|
115
122
|
tools: ["check_test_cases", "update_test_case"],
|
|
116
123
|
examples: ["Are the cases in test plan 12 good enough to run?", "Fix the expected results of case 4512"],
|
|
117
|
-
requires: [{ kind: "system", anyOf: ["azure_devops"], optional: true, why: "to rewrite cases in place" }],
|
|
124
|
+
requires: [{ kind: "system", anyOf: ["azure_devops", "jira"], optional: true, why: "to rewrite cases in place" }],
|
|
118
125
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Checking test cases before they run" }],
|
|
119
126
|
},
|
|
120
127
|
{
|
|
121
128
|
id: "write-test-cases",
|
|
122
129
|
area: "Test cases",
|
|
123
130
|
title: "Write test cases from a story",
|
|
124
|
-
summary:
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
summary:
|
|
132
|
+
"Writes test cases from a story's acceptance criteria and saves them in Azure DevOps or Jira, linked to the story. Reads the cases already linked first, so asking again adds only what is missing and updates cases in place instead of duplicating them; plans end-to-end journeys from the existing cases.",
|
|
133
|
+
tools: ["story_test_cases", "save_test_cases", "e2e_journey"],
|
|
134
|
+
examples: ["Write test cases for story 1234 and save them", "Build an end-to-end journey from story 1234's cases"],
|
|
127
135
|
requires: [{ kind: "system", anyOf: ["azure_devops", "jira"] }],
|
|
128
136
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Where cases come from" }],
|
|
137
|
+
playbook: [
|
|
138
|
+
{ heading: "Testing from requirements", when: "write test cases from a story, a Confluence page, or a specification" },
|
|
139
|
+
{ heading: "End-to-end journeys from test cases", when: "an end-to-end case or flow from test cases that exist" },
|
|
140
|
+
],
|
|
129
141
|
},
|
|
130
142
|
{
|
|
131
143
|
id: "domain-compliance",
|
|
@@ -149,6 +161,7 @@ export const CAPABILITIES: Capability[] = [
|
|
|
149
161
|
examples: ["Explore story 1234 and find bugs", "Test this feature beyond the test cases"],
|
|
150
162
|
requires: [{ kind: "live_model" }, { kind: "system", anyOf: ["azure_devops", "jira"], optional: true, why: "to read the story and file bugs" }],
|
|
151
163
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Exploratory testing from a story" }],
|
|
164
|
+
playbook: [{ heading: "Exploratory testing", when: "explore a story or feature past its scripted cases, or find bugs in it" }],
|
|
152
165
|
},
|
|
153
166
|
{
|
|
154
167
|
id: "write-tests",
|
|
@@ -160,6 +173,8 @@ export const CAPABILITIES: Capability[] = [
|
|
|
160
173
|
examples: ["Write a Playwright test for that run in ../app"],
|
|
161
174
|
requires: [{ kind: "coding_agent" }, { kind: "input", what: "a repository to write the test in (default: the working directory)" }],
|
|
162
175
|
docs: [{ doc: "HOW_TO_USE.md", heading: "6. Tests" }],
|
|
176
|
+
playbook: [{ heading: "Writing tests", when: "write an automated test from a run, or a session where passed runs become tests" }],
|
|
177
|
+
alsoAfter: ["browser_run", "run_test_cases"],
|
|
163
178
|
},
|
|
164
179
|
{
|
|
165
180
|
id: "heal-tests",
|
|
@@ -170,6 +185,7 @@ export const CAPABILITIES: Capability[] = [
|
|
|
170
185
|
examples: ["Heal the failing tests in ../app"],
|
|
171
186
|
requires: [{ kind: "coding_agent" }, { kind: "input", what: "a repository with tests" }],
|
|
172
187
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Fixing broken tests" }],
|
|
188
|
+
playbook: [{ heading: "Repairing broken tests", when: "automated tests that fail, broke after a release, or need healing" }],
|
|
173
189
|
},
|
|
174
190
|
{
|
|
175
191
|
id: "find-run-tests",
|
|
@@ -184,6 +200,21 @@ export const CAPABILITIES: Capability[] = [
|
|
|
184
200
|
{ kind: "system", anyOf: ["azure_devops", "jira"], optional: true, why: "to pick tests by sprint" },
|
|
185
201
|
],
|
|
186
202
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Finding and running existing tests" }],
|
|
203
|
+
playbook: [{ heading: "Repository tests", when: "find, list, or run the automated tests of the repository" }],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "regression-packs",
|
|
207
|
+
area: "Automated tests",
|
|
208
|
+
title: "Regression packs for a release",
|
|
209
|
+
summary: "Builds a regression pack from what goes into a release (an Azure DevOps sprint or work items, a commit range, pull requests, or changes read from any connected tool): the tests those changes can break, each with why, plus risk-area and smoke tests, and the changes no test covers. Runs it locally, on TestMu AI, or in a pipeline, and compares each run with the last.",
|
|
210
|
+
tools: ["build_regression_pack", "run_regression_pack", "list_regression_packs"],
|
|
211
|
+
examples: ["Build a regression pack for sprint 12 and run it on TestMu", "Which tests should we run for the PRs merged this week?"],
|
|
212
|
+
requires: [
|
|
213
|
+
{ kind: "input", what: "a repository with tests" },
|
|
214
|
+
{ kind: "system", anyOf: ["azure_devops", "jira"], optional: true, why: "to read a sprint's or release's work items" },
|
|
215
|
+
],
|
|
216
|
+
docs: [{ doc: "HOW_TO_USE.md", heading: "Regression packs for a release" }],
|
|
217
|
+
playbook: [{ heading: "Regression packs", when: "a regression pack, or which tests to run for a release, sprint, or pull requests" }],
|
|
187
218
|
},
|
|
188
219
|
{
|
|
189
220
|
id: "flaky-tests",
|
|
@@ -208,6 +239,8 @@ export const CAPABILITIES: Capability[] = [
|
|
|
208
239
|
{ kind: "either", of: [{ kind: "system", anyOf: ["azure_devops"], why: "for Azure DevOps pull requests" }, { kind: "command", command: "gh", why: "for GitHub pull requests" }] },
|
|
209
240
|
],
|
|
210
241
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Reviewing pull requests" }],
|
|
242
|
+
playbook: [{ heading: "Pull requests", when: "review a pull request, its impact, or the tests it can break" }],
|
|
243
|
+
alsoAfter: ["analyze_pr_impact", "select_pr_tests", "post_pr_test_results", "comment_on_pr"],
|
|
211
244
|
},
|
|
212
245
|
{
|
|
213
246
|
id: "pr-impact",
|
|
@@ -240,6 +273,7 @@ export const CAPABILITIES: Capability[] = [
|
|
|
240
273
|
{ doc: "HOW_TO_USE.md", heading: "API and contract tests" },
|
|
241
274
|
{ doc: "HOW_TO_USE.md", heading: "End-to-end API scenarios" },
|
|
242
275
|
],
|
|
276
|
+
playbook: [{ heading: "API testing", when: "API contracts, API tests, or end-to-end API scenarios (REST, GraphQL, SOAP, gRPC)" }],
|
|
243
277
|
},
|
|
244
278
|
{
|
|
245
279
|
id: "accessibility-performance",
|
|
@@ -251,6 +285,8 @@ export const CAPABILITIES: Capability[] = [
|
|
|
251
285
|
requires: [{ kind: "input", what: "a browser run (from browser_run or run_test_cases); new runs need a live model" }],
|
|
252
286
|
limits: ["Per-page Core Web Vitals, not load testing under many users."],
|
|
253
287
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Accessibility and performance budgets" }],
|
|
288
|
+
playbook: [{ heading: "Accessibility, performance, and visual changes", when: "accessibility, Core Web Vitals, performance budgets, or visual changes of pages" }],
|
|
289
|
+
alsoAfter: ["visual_compare", "visual_approve"],
|
|
254
290
|
},
|
|
255
291
|
{
|
|
256
292
|
id: "visual-comparison",
|
|
@@ -299,6 +335,7 @@ export const CAPABILITIES: Capability[] = [
|
|
|
299
335
|
examples: ["Check the login flow every 15 minutes and alert Teams"],
|
|
300
336
|
requires: [{ kind: "live_model" }, { kind: "monitor_alerts", optional: true }],
|
|
301
337
|
docs: [{ doc: "HOW_TO_USE.md", heading: "9. Monitoring" }],
|
|
338
|
+
playbook: [{ heading: "Monitoring", when: "checks on a schedule, with alerts" }],
|
|
302
339
|
},
|
|
303
340
|
{
|
|
304
341
|
id: "notifications",
|
|
@@ -340,6 +377,7 @@ export const CAPABILITIES: Capability[] = [
|
|
|
340
377
|
{ kind: "either", of: [{ kind: "command", command: "gh", why: "for GitHub Actions" }, { kind: "command", command: "az", why: "for Azure Pipelines" }] },
|
|
341
378
|
],
|
|
342
379
|
docs: [{ doc: "HOW_TO_USE.md", heading: "8. Pipelines" }],
|
|
380
|
+
playbook: [{ heading: "CI pipelines", when: "a GitHub Actions or Azure DevOps pipeline that runs ForgeQE or the tests" }],
|
|
343
381
|
},
|
|
344
382
|
{
|
|
345
383
|
id: "systems",
|
|
@@ -356,13 +394,17 @@ export const CAPABILITIES: Capability[] = [
|
|
|
356
394
|
],
|
|
357
395
|
requires: [{ kind: "system", anyOf: ["azure_devops", "jira", "xray", "confluence"] }],
|
|
358
396
|
docs: [{ doc: "README.md", heading: "Azure DevOps, Jira, and other systems" }],
|
|
397
|
+
playbook: [
|
|
398
|
+
{ heading: "Releases and pipelines in Azure DevOps", when: "releases, deployments, environments, or pipeline runs in Azure DevOps" },
|
|
399
|
+
{ heading: "Dashboards and wiki pages in Azure DevOps", when: "an Azure DevOps dashboard or wiki page" },
|
|
400
|
+
],
|
|
359
401
|
},
|
|
360
402
|
{
|
|
361
403
|
id: "setup-help",
|
|
362
404
|
area: "Setup",
|
|
363
405
|
title: "Setup, skills, and documentation",
|
|
364
406
|
summary: "Shows the model profiles and MCP servers, installs or repairs the browser engine, creates a forgeqe.yaml, manages skills, and answers questions about ForgeQE from its documentation and this list.",
|
|
365
|
-
tools: ["forgeqe_setup", "capabilities"],
|
|
407
|
+
tools: ["forgeqe_setup", "capabilities", "playbook"],
|
|
366
408
|
examples: ["What can you do?", "Which model am I using?", "How do I set up email alerts?"],
|
|
367
409
|
requires: [],
|
|
368
410
|
docs: [
|
package/src/cases/ado.ts
CHANGED
|
@@ -192,7 +192,7 @@ const xml = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "", pr
|
|
|
192
192
|
type Node = Record<string, unknown> & { ":@"?: Record<string, string> }
|
|
193
193
|
|
|
194
194
|
/** Microsoft.VSTS.TCM.Steps: `<step>` action/expected pairs in order, with shared steps (`<compref ref>`) inlined. */
|
|
195
|
-
async function parseSteps(source: string, base: string, auth: string, depth = 0): Promise<CaseStep[]> {
|
|
195
|
+
export async function parseSteps(source: string, base: string, auth: string, depth = 0): Promise<CaseStep[]> {
|
|
196
196
|
if (!source.trim()) return []
|
|
197
197
|
const steps: CaseStep[] = []
|
|
198
198
|
const walk = async (nodes: Node[]) => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { stepText, type CaseStep } from "./types.ts"
|
|
2
|
+
|
|
3
|
+
/** A test case as a journey uses it. */
|
|
4
|
+
export type JourneyCase = { id: string; title: string; url?: string; startUrl?: string; steps: CaseStep[] }
|
|
5
|
+
|
|
6
|
+
/** One case inside a journey: its steps are `from`-`to` of the journey (1-based), after the shared steps. */
|
|
7
|
+
export type JourneySection = { caseId: string; title: string; from: number; to: number; negative: boolean }
|
|
8
|
+
|
|
9
|
+
export type Journey = { startUrl?: string; shared: CaseStep[]; steps: CaseStep[]; sections: JourneySection[]; saved: number }
|
|
10
|
+
|
|
11
|
+
/** Steps that look like a negative path: a refusal, an error, or one that ends the session or removes data. */
|
|
12
|
+
const NEGATIVE = /\b(invalid|incorrect|wrong|error|denied|refused|rejects?|rejected|fails?|failed|locked|not allowed|cannot|can't|unauthori[sz]ed|forbidden|sign(?:s|ed)? out|log(?:s|ged)? out|delete[sd]?|remove[sd]?)\b/i
|
|
13
|
+
|
|
14
|
+
const key = (s: CaseStep) => `${stepText(s.action)}|${stepText(s.expected)}`
|
|
15
|
+
|
|
16
|
+
function sharedPrefix(cases: JourneyCase[]): number {
|
|
17
|
+
let n = 0
|
|
18
|
+
for (;;) {
|
|
19
|
+
const first = cases[0].steps[n]
|
|
20
|
+
if (!first || cases.some((c) => !c.steps[n] || key(c.steps[n]) !== key(first))) return n
|
|
21
|
+
n++
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Journeys from cases that start the same way: same start page and first step. The steps they all share come once,
|
|
27
|
+
* then each case's own steps follow as a section; cases that look negative go last, since they may end the session.
|
|
28
|
+
* A case that starts like no other stays on its own. The draft keeps the cases' order otherwise: the agent reorders it.
|
|
29
|
+
*/
|
|
30
|
+
export function planJourneys(cases: JourneyCase[]): { journeys: Journey[]; alone: JourneyCase[]; stepsSaved: number } {
|
|
31
|
+
const groups = new Map<string, JourneyCase[]>()
|
|
32
|
+
for (const c of cases) {
|
|
33
|
+
if (!c.steps.length) continue
|
|
34
|
+
const g = `${c.startUrl || ""}\n${key(c.steps[0])}`
|
|
35
|
+
groups.set(g, [...(groups.get(g) || []), c])
|
|
36
|
+
}
|
|
37
|
+
const journeys: Journey[] = []
|
|
38
|
+
const alone: JourneyCase[] = cases.filter((c) => !c.steps.length)
|
|
39
|
+
for (const group of groups.values()) {
|
|
40
|
+
if (group.length < 2) {
|
|
41
|
+
alone.push(...group)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
const n = sharedPrefix(group)
|
|
45
|
+
const shared = group[0].steps.slice(0, n)
|
|
46
|
+
const ordered = group
|
|
47
|
+
.map((c) => ({ c, tail: c.steps.slice(n) }))
|
|
48
|
+
.map((x) => ({ ...x, negative: NEGATIVE.test(x.tail.map((s) => `${s.action} ${s.expected || ""}`).join(" ")) }))
|
|
49
|
+
.sort((a, b) => Number(a.negative) - Number(b.negative))
|
|
50
|
+
const steps = [...shared]
|
|
51
|
+
const sections: JourneySection[] = []
|
|
52
|
+
for (const { c, tail, negative } of ordered) {
|
|
53
|
+
// A case that is only the shared steps is covered by them.
|
|
54
|
+
const from = tail.length ? steps.length + 1 : 1
|
|
55
|
+
steps.push(...tail)
|
|
56
|
+
sections.push({ caseId: c.id, title: c.title, from, to: tail.length ? steps.length : n, negative })
|
|
57
|
+
}
|
|
58
|
+
journeys.push({ startUrl: group[0].startUrl, shared, steps, sections, saved: (group.length - 1) * n })
|
|
59
|
+
}
|
|
60
|
+
return { journeys, alone, stepsSaved: journeys.reduce((sum, j) => sum + j.saved, 0) }
|
|
61
|
+
}
|
package/src/cases/readiness.ts
CHANGED
|
@@ -20,7 +20,8 @@ import { adoSprints, findSprint } from "./sprints.ts"
|
|
|
20
20
|
* runs: the newest suite report for the case, then the automated test's history.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** iteration: an iteration path and everything under it, such as a release ("Release 2026.09", or Project\Release 2026.09). */
|
|
24
|
+
export type Scope = { sprint?: string; iteration?: string; area?: string; parent?: number; ids?: number[]; project?: string }
|
|
24
25
|
|
|
25
26
|
export type Outcome = "passed" | "failed" | "blocked" | "not run"
|
|
26
27
|
export type CaseRow = { id: number; title: string; url: string; automated: boolean; test?: string; outcome: Outcome; when?: string; from?: "Azure DevOps" | "ForgeQE"; flaky?: boolean; quarantined?: boolean }
|
|
@@ -118,11 +119,26 @@ function forgeqeResults(suitesDir: string): Map<number, { outcome: Outcome; when
|
|
|
118
119
|
return out
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
export
|
|
122
|
+
export type WorkItem = Item
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* An Azure DevOps scope as work items: a sprint, an area, a parent epic or feature and everything under it, or given
|
|
126
|
+
* ids. Gives the stories in it, the WIQL filter its other work items share (empty for a parent or ids), and helpers to
|
|
127
|
+
* query and read more. Release readiness and regression packs both start here.
|
|
128
|
+
*/
|
|
129
|
+
export async function resolveScope(cfg: AppConfig, scope: Scope): Promise<{
|
|
130
|
+
label: string
|
|
131
|
+
filter: string
|
|
132
|
+
project?: string
|
|
133
|
+
org: string
|
|
134
|
+
storyIds: number[]
|
|
135
|
+
wiql: (query: string) => Promise<number[]>
|
|
136
|
+
items: (ids: number[]) => Promise<Item[]>
|
|
137
|
+
urlOf: (it: Item) => string
|
|
138
|
+
}> {
|
|
122
139
|
const { org, auth, project: configured } = adoSettings(cfg.sources)
|
|
123
140
|
const project = scope.project || configured
|
|
124
141
|
if (!project && !scope.ids?.length) throw new Error("Name the Azure DevOps project (project, or sources.azure_devops.project) for a sprint, area, or epic.")
|
|
125
|
-
const notes: string[] = []
|
|
126
142
|
const wiql = async (query: string) => {
|
|
127
143
|
const { data } = await http<{ workItems?: { id: number }[]; workItemRelations?: { target?: { id: number } }[] }>(`${org}/${encodeURIComponent(project!)}/_apis/wit/wiql?api-version=7.1`, { auth, body: { query } })
|
|
128
144
|
return [...new Set([...(data?.workItems || []).map((w) => w.id), ...(data?.workItemRelations || []).map((r) => r.target?.id).filter((n): n is number => Boolean(n))])]
|
|
@@ -130,8 +146,9 @@ export async function releaseReadiness(cfg: AppConfig, repo: string, scope: Scop
|
|
|
130
146
|
const items = async (ids: number[]) => {
|
|
131
147
|
const out: Item[] = []
|
|
132
148
|
for (let i = 0; i < ids.length; i += 200) {
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
// errorPolicy=omit: one id that does not exist, or that the token cannot read, does not lose the others.
|
|
150
|
+
const { data } = await http<{ value?: (Item | null)[] }>(`${org}/_apis/wit/workitems?ids=${ids.slice(i, i + 200).join(",")}&$expand=relations&errorPolicy=omit&api-version=7.1`, { auth })
|
|
151
|
+
out.push(...(data?.value || []).filter((w): w is Item => Boolean(w)))
|
|
135
152
|
}
|
|
136
153
|
return out
|
|
137
154
|
}
|
|
@@ -156,14 +173,26 @@ export async function releaseReadiness(cfg: AppConfig, repo: string, scope: Scop
|
|
|
156
173
|
parts.push(`[System.IterationPath] UNDER ${q(s.path)}`)
|
|
157
174
|
label = `sprint ${s.name}${s.start ? ` (${s.start} to ${s.end})` : ""}`
|
|
158
175
|
} else label = ""
|
|
176
|
+
if (scope.iteration) {
|
|
177
|
+
const iterationPath = scope.iteration.includes("\\") || !project ? scope.iteration : `${project}\\${scope.iteration}`
|
|
178
|
+
parts.push(`[System.IterationPath] UNDER ${q(iterationPath)}`)
|
|
179
|
+
label = [label, `iteration ${iterationPath}`].filter(Boolean).join(", ")
|
|
180
|
+
}
|
|
159
181
|
if (scope.area) {
|
|
160
182
|
parts.push(`[System.AreaPath] UNDER ${q(scope.area)}`)
|
|
161
183
|
label = [label, `area ${scope.area}`].filter(Boolean).join(", ")
|
|
162
184
|
}
|
|
163
|
-
if (!parts.length) throw new Error("Give a sprint, an area, a parent epic or feature, or work item ids.")
|
|
185
|
+
if (!parts.length) throw new Error("Give a sprint, an iteration path, an area, a parent epic or feature, or work item ids.")
|
|
164
186
|
filter = parts.join(" AND ")
|
|
165
187
|
storyIds = await wiql(`SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project AND ${REQUIREMENT} AND [System.State] <> 'Removed' AND ${filter}`)
|
|
166
188
|
}
|
|
189
|
+
return { label, filter, ...(project ? { project } : {}), org, storyIds, wiql, items, urlOf }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function releaseReadiness(cfg: AppConfig, repo: string, scope: Scope, thresholds: ReleaseThresholds = DEFAULT_THRESHOLDS): Promise<Readiness> {
|
|
193
|
+
const { org, auth } = adoSettings(cfg.sources)
|
|
194
|
+
const notes: string[] = []
|
|
195
|
+
const { label, filter, project, storyIds, wiql, items, urlOf } = await resolveScope(cfg, scope)
|
|
167
196
|
const storyItems = await items(storyIds)
|
|
168
197
|
|
|
169
198
|
// Test cases each story is tested by, and their newest outcome.
|