@capagents/forgeqe 0.0.0-stage → 1.0.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 +1639 -0
- package/README.md +338 -2
- package/bin/forgeqe.d.ts +3 -0
- package/bin/forgeqe.js +188 -0
- package/docs/ARCHITECTURE.md +408 -0
- package/docs/security-data-flow.html +1870 -0
- package/docs/user-guide.html +2094 -0
- package/evals/domain/access-role-change.yaml +37 -0
- package/evals/domain/banking-payment-transfer.yaml +41 -0
- package/evals/domain/gdpr-account-erasure.yaml +30 -0
- package/evals/domain/insurance-claim-fnol.yaml +30 -0
- package/evals/domain/insurance-policy-renewal.yaml +37 -0
- package/evals/domain/lending-loan-application.yaml +36 -0
- package/forgeqe-architecture.html +718 -0
- package/forgeqe.example.yaml +203 -0
- package/knowledge/README.md +18 -0
- package/knowledge/business-rules.md +10 -0
- package/knowledge/compliance.md +16 -0
- package/knowledge/environments.md +5 -0
- package/knowledge/glossary.md +5 -0
- package/knowledge/product.md +16 -0
- package/knowledge/roles.md +9 -0
- package/knowledge/test-data.md +7 -0
- package/llms.example.json +51 -0
- package/package.json +63 -4
- package/prompts/README.md +34 -0
- package/prompts/api-scenarios.md +21 -0
- package/prompts/api-tests.md +39 -0
- package/prompts/brain.md +166 -0
- package/prompts/browser-capture.md +7 -0
- package/prompts/browser-case-report.md +7 -0
- package/prompts/browser-checks.md +14 -0
- package/prompts/browser-efficiency.md +7 -0
- package/prompts/browser-error-recovery.md +15 -0
- package/prompts/browser-extra.md +5 -0
- package/prompts/browser-goal-verification.md +13 -0
- package/prompts/browser-skill-select.md +6 -0
- package/prompts/domain-rule.md +13 -0
- package/prompts/eval-judge.md +19 -0
- package/prompts/explore-charters.md +27 -0
- package/prompts/find-tests.md +28 -0
- package/prompts/heal.md +40 -0
- package/prompts/pr-review.md +29 -0
- package/prompts/skill-rule.md +6 -0
- package/prompts/test-agent.md +34 -0
- package/prompts/test-reviewer.md +30 -0
- package/prompts/test-rules.md +28 -0
- package/py/forgeqe_engine/__init__.py +3 -0
- package/py/forgeqe_engine/__main__.py +3 -0
- package/py/forgeqe_engine/brand.py +453 -0
- package/py/forgeqe_engine/brand_assets.py +6 -0
- package/py/forgeqe_engine/browser.py +196 -0
- package/py/forgeqe_engine/capture.py +538 -0
- package/py/forgeqe_engine/checks.py +98 -0
- package/py/forgeqe_engine/cli.py +188 -0
- package/py/forgeqe_engine/diagnostics.py +609 -0
- package/py/forgeqe_engine/llm.py +195 -0
- package/py/forgeqe_engine/logins.py +251 -0
- package/py/forgeqe_engine/loops.py +224 -0
- package/py/forgeqe_engine/memory.py +1004 -0
- package/py/forgeqe_engine/network.py +244 -0
- package/py/forgeqe_engine/prompts.py +20 -0
- package/py/forgeqe_engine/replay.py +240 -0
- package/py/forgeqe_engine/report.py +173 -0
- package/py/forgeqe_engine/service.py +1335 -0
- package/py/forgeqe_engine/settings.py +93 -0
- package/py/forgeqe_engine/skills.py +88 -0
- package/py/forgeqe_engine/summary.py +239 -0
- package/py/forgeqe_engine/urls.py +81 -0
- package/py/forgeqe_engine/workflow_yaml.py +689 -0
- package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
- package/skills/dynamics-365/SKILL.md +21 -0
- package/skills/playwright-typescript/SKILL.md +35 -0
- package/skills/writing-skills/SKILL.md +30 -0
- package/src/api/plan.ts +246 -0
- package/src/api/run.ts +204 -0
- package/src/api/scenario.ts +502 -0
- package/src/api/schema.ts +170 -0
- package/src/api/spec.ts +624 -0
- package/src/api/store.ts +232 -0
- package/src/api/traffic.ts +281 -0
- package/src/api/types.ts +163 -0
- package/src/api/write.ts +272 -0
- package/src/auth/signin.ts +494 -0
- package/src/auth/store.ts +170 -0
- package/src/brain/index.ts +189 -0
- package/src/brain/prompt.ts +19 -0
- package/src/capabilities/docs.ts +101 -0
- package/src/capabilities/guide.ts +455 -0
- package/src/capabilities/registry.ts +343 -0
- package/src/capabilities/render.ts +106 -0
- package/src/capabilities/status.ts +137 -0
- package/src/card.ts +11 -0
- package/src/cases/ado.ts +325 -0
- package/src/cases/api.ts +117 -0
- package/src/cases/bugs.ts +223 -0
- package/src/cases/confluence.ts +111 -0
- package/src/cases/context.ts +223 -0
- package/src/cases/dashboards.ts +137 -0
- package/src/cases/failure.ts +97 -0
- package/src/cases/files.ts +295 -0
- package/src/cases/goal.ts +53 -0
- package/src/cases/http.ts +111 -0
- package/src/cases/index.ts +131 -0
- package/src/cases/jira.ts +122 -0
- package/src/cases/qtest.ts +108 -0
- package/src/cases/quality.ts +190 -0
- package/src/cases/rag.ts +100 -0
- package/src/cases/readiness.ts +284 -0
- package/src/cases/save.ts +149 -0
- package/src/cases/sprints.ts +148 -0
- package/src/cases/testrail.ts +176 -0
- package/src/cases/types.ts +107 -0
- package/src/cases/wiki.ts +105 -0
- package/src/cases/xray.ts +206 -0
- package/src/cases/zephyr.ts +153 -0
- package/src/childEnv.ts +86 -0
- package/src/cli/args.ts +114 -0
- package/src/cli/help.ts +167 -0
- package/src/cli/index.ts +1142 -0
- package/src/cli/terminal.ts +187 -0
- package/src/code/areas.ts +337 -0
- package/src/code/attention.ts +214 -0
- package/src/code/find.ts +540 -0
- package/src/code/handoff.ts +541 -0
- package/src/code/history.ts +170 -0
- package/src/code/impact.ts +231 -0
- package/src/code/index.ts +34 -0
- package/src/code/livebrowser.ts +67 -0
- package/src/code/loop.ts +603 -0
- package/src/code/notes.ts +75 -0
- package/src/code/profile.ts +226 -0
- package/src/code/prompt.ts +303 -0
- package/src/code/prreview.ts +649 -0
- package/src/code/prtests.ts +103 -0
- package/src/code/replay.ts +772 -0
- package/src/code/review.ts +202 -0
- package/src/code/runtime.ts +781 -0
- package/src/code/slices.ts +159 -0
- package/src/code/testrun.ts +187 -0
- package/src/code/traceability.ts +100 -0
- package/src/code/worktree.ts +153 -0
- package/src/commands.ts +101 -0
- package/src/config.ts +970 -0
- package/src/detail.ts +65 -0
- package/src/domain.ts +19 -0
- package/src/engine/bridge.ts +359 -0
- package/src/engine/mock.ts +102 -0
- package/src/engine/setup.ts +298 -0
- package/src/entra.ts +154 -0
- package/src/env.ts +24 -0
- package/src/evals/domain.ts +338 -0
- package/src/explore/charters.ts +151 -0
- package/src/explore/run.ts +280 -0
- package/src/explore/story.ts +106 -0
- package/src/guard.ts +51 -0
- package/src/heal/loop.ts +357 -0
- package/src/heal/prompt.ts +143 -0
- package/src/heal/report.ts +95 -0
- package/src/index.ts +12 -0
- package/src/init.ts +102 -0
- package/src/knowledge.ts +114 -0
- package/src/legacyEnv.ts +9 -0
- package/src/llm.ts +61 -0
- package/src/locale.ts +25 -0
- package/src/logins.ts +148 -0
- package/src/managed.ts +101 -0
- package/src/mcp/apitools.ts +335 -0
- package/src/mcp/captools.ts +25 -0
- package/src/mcp/casetools.ts +223 -0
- package/src/mcp/commands.ts +421 -0
- package/src/mcp/exploretools.ts +129 -0
- package/src/mcp/external.ts +172 -0
- package/src/mcp/git.ts +78 -0
- package/src/mcp/notifytools.ts +49 -0
- package/src/mcp/prtools.ts +261 -0
- package/src/mcp/qualitytools.ts +154 -0
- package/src/mcp/repotests.ts +286 -0
- package/src/mcp/server.ts +234 -0
- package/src/mcp/tools.ts +810 -0
- package/src/memory.ts +250 -0
- package/src/money.ts +39 -0
- package/src/monitor/alerts.ts +94 -0
- package/src/monitor/background.ts +68 -0
- package/src/monitor/run.ts +263 -0
- package/src/monitor/state.ts +105 -0
- package/src/notify/events.ts +144 -0
- package/src/notify/notice.ts +177 -0
- package/src/prices.ts +175 -0
- package/src/print.ts +54 -0
- package/src/privacy.ts +35 -0
- package/src/prompts.ts +165 -0
- package/src/quality/budgets.ts +249 -0
- package/src/quality/history.ts +149 -0
- package/src/quality/visual.ts +201 -0
- package/src/report/data.ts +475 -0
- package/src/report/html.ts +106 -0
- package/src/report/template.html +84 -0
- package/src/run.ts +243 -0
- package/src/sessionLog.ts +216 -0
- package/src/sessionLogins.ts +94 -0
- package/src/skills/registry.ts +217 -0
- package/src/skills/scaffold.ts +65 -0
- package/src/skills/select.ts +37 -0
- package/src/skills/view.ts +44 -0
- package/src/suite/report.ts +135 -0
- package/src/suite/run.ts +303 -0
- package/src/summary.ts +188 -0
- package/src/tools.ts +130 -0
- package/src/tui/App.tsx +983 -0
- package/src/tui/clipboard.ts +36 -0
- package/src/tui/components.tsx +263 -0
- package/src/tui/dialogs.ts +65 -0
- package/src/tui/main.tsx +20 -0
- package/src/tui/text.ts +28 -0
- package/src/tui/theme.ts +104 -0
- package/src/tui/transcript.tsx +344 -0
- package/src/types.ts +146 -0
- package/src/update.ts +311 -0
- package/src/usage.ts +158 -0
package/src/api/write.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { existsSync, realpathSync, statSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import type { AppConfig } from "../config.ts"
|
|
5
|
+
import { privateEnvNames } from "../childEnv.ts"
|
|
6
|
+
import { addUsage } from "../usage.ts"
|
|
7
|
+
import type { CodeEvent, Usage } from "../types.ts"
|
|
8
|
+
import { loadSkills } from "../skills/registry.ts"
|
|
9
|
+
import { buildProfile, profileSection } from "../code/profile.ts"
|
|
10
|
+
import { RESULT_SCHEMA, type AgentReport } from "../code/prompt.ts"
|
|
11
|
+
import { openSession, type AgentSpec, type OpencodeSession } from "../code/runtime.ts"
|
|
12
|
+
import { headAndTail, runTestCommand } from "../code/testrun.ts"
|
|
13
|
+
import { ownPaths, protectPatterns } from "../code/worktree.ts"
|
|
14
|
+
import { associateAdoAutomation, recordMapping } from "../code/traceability.ts"
|
|
15
|
+
import type { WriteBack } from "../cases/types.ts"
|
|
16
|
+
import { briefMarkdown, scenarioMarkdown } from "./store.ts"
|
|
17
|
+
import type { ContractSet, Scenario } from "./types.ts"
|
|
18
|
+
import { prompt } from "../prompts.ts"
|
|
19
|
+
import { domainNotes } from "../domain.ts"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The coding agent writes API and contract tests for a contract set in the repository's own framework, and ForgeQE
|
|
23
|
+
* runs them until they pass: Playwright's request fixture in a Playwright repository, pytest with requests or httpx,
|
|
24
|
+
* REST Assured, RestSharp or HttpClient, supertest... whatever the repository already uses for HTTP, else what its
|
|
25
|
+
* language uses most. No browser: these tests call the API directly, so they are fast and do not break when the page
|
|
26
|
+
* changes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const apiAgentPrompt = () => prompt("api-tests")
|
|
30
|
+
|
|
31
|
+
export const API_AGENT: AgentSpec = {
|
|
32
|
+
name: "forgeqe-api",
|
|
33
|
+
description: "Writes API and contract tests in this repository from ForgeQE's contracts",
|
|
34
|
+
get prompt() {
|
|
35
|
+
return apiAgentPrompt()
|
|
36
|
+
},
|
|
37
|
+
permission: { "live_*": "deny" },
|
|
38
|
+
skills: "scribe",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The test result Scribe reports, plus the scenario tests' names and the regression command for changed helpers. */
|
|
42
|
+
export const API_RESULT_SCHEMA = {
|
|
43
|
+
...RESULT_SCHEMA,
|
|
44
|
+
properties: {
|
|
45
|
+
...RESULT_SCHEMA.properties,
|
|
46
|
+
scenario_tests: {
|
|
47
|
+
type: "array",
|
|
48
|
+
items: { type: "object", properties: { scenario_id: { type: "string" }, test_name: { type: "string" } }, required: ["scenario_id", "test_name"] },
|
|
49
|
+
description: "Each scenario's test, by the scenario id ForgeQE gave and the test's full name as the framework reports it",
|
|
50
|
+
},
|
|
51
|
+
helpers_changed: { type: "array", items: { type: "string" }, description: "Existing helpers you changed that other tests use" },
|
|
52
|
+
regression_command: { type: "string", description: "When you changed helpers other tests use: the command that runs those tests" },
|
|
53
|
+
},
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
type ApiReport = AgentReport & { scenario_tests?: { scenario_id: string; test_name: string }[]; helpers_changed?: string[]; regression_command?: string }
|
|
57
|
+
|
|
58
|
+
export type ApiTestsResult = {
|
|
59
|
+
status: "passed" | "failed" | "blocked" | "stopped"
|
|
60
|
+
repo: string
|
|
61
|
+
attempts: number
|
|
62
|
+
testFile?: string
|
|
63
|
+
command?: string
|
|
64
|
+
changed: string[]
|
|
65
|
+
reused: string[]
|
|
66
|
+
notes: string
|
|
67
|
+
output: string
|
|
68
|
+
usage: Usage
|
|
69
|
+
model: string
|
|
70
|
+
/** Scenarios with a test case: the test each got, and the write-back to the test case's system. */
|
|
71
|
+
mapped: { caseId: string; testName: string }[]
|
|
72
|
+
writeBack: WriteBack[]
|
|
73
|
+
helpersChanged: string[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The names of environment variables api.headers reads (never their values), for the agent to use the same ones. */
|
|
77
|
+
export function credentialNames(config: AppConfig): string[] {
|
|
78
|
+
const names = new Set<string>()
|
|
79
|
+
for (const v of Object.values(config.api?.headers || {})) for (const m of v.matchAll(/\$\{(\w+)\}|\$(\w+)/g)) names.add(m[1] || m[2])
|
|
80
|
+
return [...names]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function apiTestsPrompt(set: ContractSet, opts: { baseUrl: string; profile?: string; credentials: string[]; headers: string[]; writes: boolean; scenarios?: Scenario[] }): string {
|
|
84
|
+
if (opts.scenarios?.length) return scenarioTestsPrompt(set, { ...opts, scenarios: opts.scenarios })
|
|
85
|
+
const runnable = set.contracts.filter((c) => opts.writes || !c.writes)
|
|
86
|
+
const leftOut = set.contracts.length - runnable.length
|
|
87
|
+
return [
|
|
88
|
+
`Write API and contract tests for these ${runnable.length} operation${runnable.length === 1 ? "" : "s"} of ${set.name}.`,
|
|
89
|
+
`Base URL: ${opts.baseUrl || "(not known: read it from API_BASE_URL)"}`,
|
|
90
|
+
opts.headers.length
|
|
91
|
+
? `Every call sends these headers: ${opts.headers.join(", ")}. Read their values from the environment variables ${opts.credentials.join(", ") || "(none named)"}, as forgeqe.yaml's api.headers does.`
|
|
92
|
+
: set.contracts.some((c) => c.auth)
|
|
93
|
+
? "Some operations need auth (see each one). No credential variables are configured: read a bearer token from API_TOKEN, and skip the test with a clear message when it is not set."
|
|
94
|
+
: "",
|
|
95
|
+
leftOut ? `${leftOut} operation${leftOut === 1 ? "" : "s"} that change data are left out: do not write tests for them.` : "",
|
|
96
|
+
opts.profile || "",
|
|
97
|
+
"",
|
|
98
|
+
"The contracts (contracts.json has the full schemas):",
|
|
99
|
+
"",
|
|
100
|
+
briefMarkdown({ ...set, contracts: runnable }),
|
|
101
|
+
]
|
|
102
|
+
.filter((l, i, all) => l !== "" || all[i - 1] !== "")
|
|
103
|
+
.join("\n")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function credentialLine(set: ContractSet, opts: { credentials: string[]; headers: string[] }): string {
|
|
107
|
+
return opts.headers.length
|
|
108
|
+
? `Every call sends these headers: ${opts.headers.join(", ")}. Read their values from the environment variables ${opts.credentials.join(", ") || "(none named)"}, as forgeqe.yaml's api.headers does.`
|
|
109
|
+
: set.contracts.some((c) => c.auth)
|
|
110
|
+
? "Some operations need auth. No credential variables are configured: read a bearer token from API_TOKEN, and skip the test with a clear message when it is not set."
|
|
111
|
+
: ""
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** End-to-end scenarios to write as tests, on the repository's existing API helpers. */
|
|
115
|
+
export function scenarioTestsPrompt(set: ContractSet, opts: { baseUrl: string; profile?: string; credentials: string[]; headers: string[]; scenarios: Scenario[] }): string {
|
|
116
|
+
const used = new Set(opts.scenarios.flatMap((s) => [...s.steps, ...(s.cleanup || [])].map((st) => st.operation).filter(Boolean)))
|
|
117
|
+
const ops = set.contracts.filter((c) => used.has(c.id))
|
|
118
|
+
return [
|
|
119
|
+
`Write ${opts.scenarios.length === 1 ? "an end-to-end API scenario test" : `${opts.scenarios.length} end-to-end API scenario tests`} for ${set.name}: one test per scenario, built on the repository's existing API helpers (map them first, as your instructions say).`,
|
|
120
|
+
`Base URL: ${opts.baseUrl || "(not known: read it from API_BASE_URL)"}`,
|
|
121
|
+
credentialLine(set, opts),
|
|
122
|
+
opts.profile || "",
|
|
123
|
+
"",
|
|
124
|
+
briefMarkdown({ ...set, contracts: ops, scenarios: opts.scenarios, skipped: undefined }),
|
|
125
|
+
]
|
|
126
|
+
.filter((l, i, all) => l !== "" || all[i - 1] !== "")
|
|
127
|
+
.join("\n")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function writeApiTests(opts: {
|
|
131
|
+
config: AppConfig
|
|
132
|
+
repo: string
|
|
133
|
+
set: ContractSet
|
|
134
|
+
setDir: string
|
|
135
|
+
baseUrl?: string
|
|
136
|
+
writes?: boolean
|
|
137
|
+
/** Write these scenarios as end-to-end tests instead of one test per contract. */
|
|
138
|
+
scenarios?: Scenario[]
|
|
139
|
+
/** Link scenario tests to their Azure DevOps test cases (Associated Automation). Default: sources.write_back. */
|
|
140
|
+
writeBack?: boolean
|
|
141
|
+
signal?: AbortSignal
|
|
142
|
+
onEvent?: (e: CodeEvent) => void
|
|
143
|
+
open?: (o: Parameters<typeof openSession>[0]) => Promise<OpencodeSession>
|
|
144
|
+
runTest?: typeof runTestCommand
|
|
145
|
+
}): Promise<ApiTestsResult> {
|
|
146
|
+
const given = path.resolve(opts.repo)
|
|
147
|
+
if (!existsSync(given) || !statSync(given).isDirectory()) throw new Error(`Repo not found: ${given}`)
|
|
148
|
+
const repo = realpathSync(given)
|
|
149
|
+
const { config } = opts
|
|
150
|
+
const emit = opts.onEvent || (() => {})
|
|
151
|
+
const runTest = opts.runTest || runTestCommand
|
|
152
|
+
const maxAttempts = Math.max(1, config.code.maxAttempts)
|
|
153
|
+
const baseUrl = opts.baseUrl || config.api?.baseUrl || opts.set.baseUrl
|
|
154
|
+
const result: ApiTestsResult = { status: "failed", repo, attempts: 0, changed: [], reused: [], notes: "", output: "", usage: { input: 0, output: 0 }, model: "", mapped: [], writeBack: [], helpersChanged: [] }
|
|
155
|
+
const own = ownPaths(repo, [config.export.outDir, config.memory.dir])
|
|
156
|
+
const session = await (opts.open || openSession)({
|
|
157
|
+
repo,
|
|
158
|
+
profile: config.llm,
|
|
159
|
+
domain: domainNotes(config),
|
|
160
|
+
model: config.code.model,
|
|
161
|
+
bin: config.code.opencode,
|
|
162
|
+
readDirs: [repo, opts.setDir],
|
|
163
|
+
hide: privateEnvNames(config),
|
|
164
|
+
signal: opts.signal,
|
|
165
|
+
onEvent: emit,
|
|
166
|
+
protect: protectPatterns(repo, own, []),
|
|
167
|
+
stallAfter: config.code.stallAfter ?? 5,
|
|
168
|
+
agent: { ...API_AGENT, steps: config.code.maxSteps ?? 50 },
|
|
169
|
+
skills: loadSkills(config.skills, repo).skills,
|
|
170
|
+
schema: API_RESULT_SCHEMA as unknown as Record<string, unknown>,
|
|
171
|
+
title: opts.scenarios?.length ? "ForgeQE API scenarios" : "ForgeQE API tests",
|
|
172
|
+
})
|
|
173
|
+
result.model = session.model
|
|
174
|
+
const budget = config.code.maxTokens
|
|
175
|
+
const credentials = credentialNames(config)
|
|
176
|
+
const env = { ...(baseUrl ? { API_BASE_URL: baseUrl } : {}) }
|
|
177
|
+
try {
|
|
178
|
+
const profile = buildProfile(repo)
|
|
179
|
+
let message = apiTestsPrompt(opts.set, { baseUrl, profile: profile ? profileSection(profile) : undefined, credentials, headers: Object.keys(config.api?.headers || {}), writes: Boolean(opts.writes), scenarios: opts.scenarios })
|
|
180
|
+
emit({ type: "code", kind: "info", text: opts.scenarios?.length ? `Scribe (${session.model}) is writing ${opts.scenarios.length} API scenario test${opts.scenarios.length === 1 ? "" : "s"} in ${repo}, on its existing API helpers` : `Scribe (${session.model}) is writing API tests for ${opts.set.contracts.length} operations in ${repo}` })
|
|
181
|
+
let report: ApiReport | null = null
|
|
182
|
+
for (let attempt = 1; attempt <= maxAttempts && !opts.signal?.aborted; attempt++) {
|
|
183
|
+
result.attempts = attempt
|
|
184
|
+
const reply = await session.prompt(message)
|
|
185
|
+
report = reply.report as ApiReport | null
|
|
186
|
+
if (!report) {
|
|
187
|
+
result.notes = reply.error ? `Scribe stopped: ${reply.error}` : "Scribe gave no result."
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
result.testFile = report.test_file
|
|
191
|
+
result.command = report.command
|
|
192
|
+
result.changed = report.changed || []
|
|
193
|
+
result.reused = report.reused || []
|
|
194
|
+
result.notes = report.notes || ""
|
|
195
|
+
if (report.status === "blocked") {
|
|
196
|
+
result.status = "blocked"
|
|
197
|
+
result.notes = report.blocked_reason || result.notes
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
if (!report.command) {
|
|
201
|
+
message = "Give the command that runs only these API tests from the repository root, in the structured result."
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
emit({ type: "code", kind: "info", text: `ForgeQE runs the API tests (attempt ${attempt} of ${maxAttempts}): ${report.command}` })
|
|
205
|
+
const prefix = Object.entries(env).map(([k, v]) => `${k}=${JSON.stringify(v)} `).join("")
|
|
206
|
+
const run = await runTest({ command: process.platform === "win32" ? report.command : `${prefix}${report.command}`, cwd: repo, timeoutSeconds: config.code.testTimeoutSeconds, signal: opts.signal, save: path.join(opts.setDir, `tests-attempt-${attempt}`), hide: privateEnvNames(config) })
|
|
207
|
+
result.output = run.output
|
|
208
|
+
result.helpersChanged = report.helpers_changed || []
|
|
209
|
+
// Changed shared helpers: the tests that already use them must still pass.
|
|
210
|
+
const regression = run.ok && report.regression_command?.trim() && report.regression_command.trim() !== report.command.trim() ? report.regression_command.trim() : ""
|
|
211
|
+
if (regression) {
|
|
212
|
+
emit({ type: "code", kind: "info", text: `ForgeQE runs the tests that use the changed helpers: ${regression}` })
|
|
213
|
+
const again = await runTest({ command: process.platform === "win32" ? regression : `${prefix}${regression}`, cwd: repo, timeoutSeconds: config.code.testTimeoutSeconds, signal: opts.signal, save: path.join(opts.setDir, `regression-attempt-${attempt}`), hide: privateEnvNames(config) })
|
|
214
|
+
if (!again.ok) {
|
|
215
|
+
emit({ type: "code", kind: "info", text: `Tests that use the changed helpers fail now (exit ${again.exitCode ?? "timeout"})` })
|
|
216
|
+
result.output = again.output
|
|
217
|
+
message = [
|
|
218
|
+
`The new tests pass, but tests that already used the helpers you changed now fail. ForgeQE ran: ${regression}`,
|
|
219
|
+
"",
|
|
220
|
+
"```",
|
|
221
|
+
headAndTail(again.output),
|
|
222
|
+
"```",
|
|
223
|
+
"",
|
|
224
|
+
"Change the helpers so the existing tests pass as before (add, do not change what they rely on), then give the structured result again.",
|
|
225
|
+
].join("\n")
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (run.ok) {
|
|
230
|
+
result.status = "passed"
|
|
231
|
+
emit({ type: "code", kind: "info", text: `API tests passed in ${(run.ms / 1000).toFixed(1)}s` })
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
emit({ type: "code", kind: "info", text: `API tests failed (exit ${run.exitCode ?? "timeout"})` })
|
|
235
|
+
if (budget && session.usage.input + session.usage.output > budget) {
|
|
236
|
+
result.notes = `Stopped: the job reached its token budget (code.max_tokens ${budget}).`
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
message = [
|
|
240
|
+
`ForgeQE ran: ${report.command}`,
|
|
241
|
+
`It failed (exit ${run.exitCode ?? "timeout: the command did not finish"}). The output${run.log ? ` (full output in ${run.log})` : ""}:`,
|
|
242
|
+
"",
|
|
243
|
+
"```",
|
|
244
|
+
headAndTail(run.output),
|
|
245
|
+
"```",
|
|
246
|
+
"",
|
|
247
|
+
"Fix the tests (or say in notes where the API itself is wrong), then give the structured result again.",
|
|
248
|
+
].join("\n")
|
|
249
|
+
}
|
|
250
|
+
if (opts.signal?.aborted) result.status = "stopped"
|
|
251
|
+
if (result.status === "passed" && report?.test_file) await mapScenarios(opts, repo, report, result)
|
|
252
|
+
} finally {
|
|
253
|
+
result.usage = addUsage(result.usage, session.usage)
|
|
254
|
+
await session.close()
|
|
255
|
+
}
|
|
256
|
+
return result
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Scenario tests that automate a test case: recorded in the repository's mapping file, and in Azure DevOps when write-back is on. */
|
|
260
|
+
async function mapScenarios(opts: { config: AppConfig; scenarios?: Scenario[]; writeBack?: boolean }, repo: string, report: ApiReport, result: ApiTestsResult): Promise<void> {
|
|
261
|
+
for (const sc of opts.scenarios || []) {
|
|
262
|
+
if (!sc.caseId) continue
|
|
263
|
+
const testName = report.scenario_tests?.find((t) => t.scenario_id === sc.id)?.test_name || `${sc.caseId} ${sc.name}`
|
|
264
|
+
recordMapping(repo, { caseId: sc.caseId, title: sc.caseTitle || sc.name, source: sc.caseSource || "text", ...(sc.caseUrl ? { url: sc.caseUrl } : {}), status: "automated", testFile: report.test_file, testName, command: report.command })
|
|
265
|
+
result.mapped.push({ caseId: sc.caseId, testName })
|
|
266
|
+
if (sc.caseSource === "ado" && (opts.writeBack ?? opts.config.sources.writeBack)) {
|
|
267
|
+
result.writeBack.push(
|
|
268
|
+
await associateAdoAutomation(opts.config.sources, { id: sc.caseId, title: sc.caseTitle || sc.name, source: "ado", steps: [], ...(sc.caseUrl ? { url: sc.caseUrl } : {}), ...(sc.caseRef ? { ref: sc.caseRef } : {}) }, { name: testName, file: report.test_file!, command: report.command }),
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|