@skyramp/mcp 0.3.3 → 0.3.4
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/build/playwright/registerPlaywrightTools.js +42 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +19 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +2 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -4
- package/build/prompts/test-recommendation/recommendationSections.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationSections.js +5 -5
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +13 -7
- package/build/prompts/testbot/testbot-prompts.js +6 -13
- package/build/recommendation/discriminators.d.ts +7 -1
- package/build/recommendation/discriminators.js +16 -3
- package/build/resources/testbotResource.js +0 -1
- package/build/services/ScenarioGenerationService.js +5 -2
- package/build/services/TestExecutionService.js +25 -1
- package/build/services/TestGenerationService.js +24 -9
- package/build/services/containerEnv.d.ts +12 -1
- package/build/services/containerEnv.js +94 -1
- package/build/tools/executeSkyrampTestTool.d.ts +9 -0
- package/build/tools/executeSkyrampTestTool.js +20 -6
- package/build/tools/execution-video-state.d.ts +21 -0
- package/build/tools/execution-video-state.js +51 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +31 -11
- package/build/tools/generate-tests/planGuard.d.ts +5 -5
- package/build/tools/generate-tests/planGuard.js +5 -17
- package/build/tools/submitReportTool.d.ts +83 -10
- package/build/tools/submitReportTool.js +169 -28
- package/build/tools/test-management/actionsTool.js +40 -39
- package/build/tools/test-management/analyzeChangesTool.d.ts +11 -0
- package/build/tools/test-management/analyzeChangesTool.js +37 -33
- package/build/tools/test-management/analyzeTestHealthTool.js +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +113 -31
- package/build/types/TestExecution.d.ts +14 -0
- package/build/types/TestTypes.js +3 -2
- package/build/types/TestbotPromptOptions.d.ts +0 -1
- package/build/types/TestbotReport.d.ts +10 -0
- package/build/types/TestbotReport.js +10 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/index.js +1 -0
- package/build/utils/AnalysisStateManager.d.ts +36 -2
- package/build/utils/AnalysisStateManager.js +34 -13
- package/build/utils/scenarioDrafting.js +7 -1
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/urlPath.d.ts +37 -0
- package/build/utils/urlPath.js +55 -0
- package/build/utils/utils.d.ts +45 -0
- package/build/utils/utils.js +50 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +15 -15
- package/build/utils/workspaceAuth.js +32 -17
- package/build/workspace/queryParamResolution.d.ts +93 -0
- package/build/workspace/queryParamResolution.js +201 -0
- package/build/workspace/workspace.d.ts +104 -0
- package/build/workspace/workspace.js +24 -0
- package/package.json +3 -2
|
@@ -62,7 +62,7 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
62
62
|
inputSchema: {
|
|
63
63
|
stateFile: z
|
|
64
64
|
.string()
|
|
65
|
-
.describe("Path to the analyze-changes-state.json file returned by skyramp_analyze_changes (the stateFile field in its output). Do NOT pass
|
|
65
|
+
.describe("Path to the analyze-changes-state.json file returned by skyramp_analyze_changes (the stateFile field in its output). Do NOT pass testbot-result.txt here."),
|
|
66
66
|
blueprintCaptured: z
|
|
67
67
|
.boolean()
|
|
68
68
|
.optional()
|
|
@@ -88,10 +88,10 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
88
88
|
return toolError(`State file is empty or invalid: ${args.stateFile}${args.repository ? ` (no section for repository ${args.repository})` : ""}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
89
89
|
}
|
|
90
90
|
if (!repositoryPath || typeof repositoryPath !== "string") {
|
|
91
|
-
return toolError(`repositoryPath not found in state file metadata. The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not
|
|
91
|
+
return toolError(`repositoryPath not found in state file metadata. The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not testbot-result.txt. Re-run skyramp_analyze_changes to regenerate it.`);
|
|
92
92
|
}
|
|
93
93
|
if (!Array.isArray(stateData.existingTests)) {
|
|
94
|
-
return toolError(`stateFile does not contain test discovery data (existingTests missing). The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not
|
|
94
|
+
return toolError(`stateFile does not contain test discovery data (existingTests missing). The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not testbot-result.txt.`);
|
|
95
95
|
}
|
|
96
96
|
// External tests: UPDATE only — REGENERATE/DELETE are report-only (enforced in
|
|
97
97
|
// skyramp_actions). Already scoped to relevant ones by discovery.
|
|
@@ -128,38 +128,57 @@ function toDraftedScenario(input) {
|
|
|
128
128
|
testType: input.testType,
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
-
/** Build the agent-submitted candidates
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
/** Build the agent-submitted candidates. Discriminator claims are verified
|
|
132
|
+
* later, by {@link applyDiscriminatorClaims}, once the merge has settled which
|
|
133
|
+
* scenario each name resolves to. */
|
|
134
|
+
function buildAgentCandidates(candidates) {
|
|
135
|
+
return candidates.map((input) => {
|
|
136
136
|
const scenario = toDraftedScenario(input);
|
|
137
|
-
|
|
138
|
-
let verifiedDiscriminator;
|
|
139
|
-
if (input.discriminator) {
|
|
140
|
-
const claim = input.discriminator;
|
|
141
|
-
const result = validateDiscriminator(scenario, claim, diffText);
|
|
142
|
-
if (result.verified) {
|
|
143
|
-
verifiedDiscriminator = claim.kind;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
demotions.push({
|
|
147
|
-
candidateId,
|
|
148
|
-
reason: result.reason ?? `${claim.kind} discriminator claim could not be verified.`,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const candidate = {
|
|
137
|
+
return {
|
|
153
138
|
scenario,
|
|
154
139
|
priority: derivePriorityTier(scenario),
|
|
155
140
|
novelty: Novelty.NEW,
|
|
156
141
|
source: CandidateSource.AGENT,
|
|
157
|
-
candidateId,
|
|
158
|
-
...(verifiedDiscriminator ? { verifiedDiscriminator } : {}),
|
|
142
|
+
candidateId: computeCandidateId(scenario),
|
|
159
143
|
};
|
|
160
|
-
return candidate;
|
|
161
144
|
});
|
|
162
|
-
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Verify each declared discriminator against the candidate that actually won
|
|
148
|
+
* the merge, and mark it. Failed claims demote (never reject).
|
|
149
|
+
*
|
|
150
|
+
* Runs AFTER the merge, not while building the agent's candidates: a server
|
|
151
|
+
* pre-seeded candidate wins the name, so verifying the agent's copy first
|
|
152
|
+
* attaches the result to a candidate that is about to be discarded — silently
|
|
153
|
+
* stripping the verification. Verification is against the WINNER's
|
|
154
|
+
* steps, so a claim that only held for the agent's rewrite is correctly refused
|
|
155
|
+
* rather than carried across.
|
|
156
|
+
*
|
|
157
|
+
* The marker is cleared before the current claim is applied. Each call replaces
|
|
158
|
+
* the agent's previous submission (see the `candidates` schema), and
|
|
159
|
+
* recoverServerCandidates restores whatever the LAST plan recorded — so keeping
|
|
160
|
+
* it would let a boost earned by an earlier registration outlive the claim that
|
|
161
|
+
* earned it, on a submission that no longer makes that claim or whose new claim
|
|
162
|
+
* fails.
|
|
163
|
+
*/
|
|
164
|
+
function applyDiscriminatorClaims(candidates, claimsByKey, diffText, reposWithoutDiff = []) {
|
|
165
|
+
const demotions = [];
|
|
166
|
+
const marked = candidates.map((candidate) => {
|
|
167
|
+
const { verifiedDiscriminator: _stale, ...unmarked } = candidate;
|
|
168
|
+
const claim = claimsByKey.get(scenarioMergeKey(candidate.scenario.scenarioName) || candidate.candidateId);
|
|
169
|
+
if (!claim)
|
|
170
|
+
return unmarked;
|
|
171
|
+
const result = validateDiscriminator(candidate.scenario, claim, diffText, reposWithoutDiff);
|
|
172
|
+
if (result.verified) {
|
|
173
|
+
return { ...unmarked, verifiedDiscriminator: claim.kind };
|
|
174
|
+
}
|
|
175
|
+
demotions.push({
|
|
176
|
+
candidateId: candidate.candidateId,
|
|
177
|
+
reason: result.reason ?? `${claim.kind} discriminator claim could not be verified.`,
|
|
178
|
+
});
|
|
179
|
+
return unmarked;
|
|
180
|
+
});
|
|
181
|
+
return { candidates: marked, demotions };
|
|
163
182
|
}
|
|
164
183
|
/** Recover full Candidates for pre-seeded ("server") plan items by matching
|
|
165
184
|
* candidateId back to the analyzed scenarios (repositoryAnalysis.scenarios) —
|
|
@@ -190,15 +209,49 @@ function recoverServerCandidates(priorPlan, allScenarios) {
|
|
|
190
209
|
}
|
|
191
210
|
return recovered;
|
|
192
211
|
}
|
|
193
|
-
|
|
212
|
+
/** Whether a repo section's persisted diff classification contains any new,
|
|
213
|
+
* modified, or removed endpoint — the same signal `computeScoredCandidates`
|
|
214
|
+
* derives `hasApiChanges` from at analyze time. */
|
|
215
|
+
function sectionHasApiChanges(data) {
|
|
216
|
+
const diff = data?.repositoryAnalysis?.diff;
|
|
217
|
+
return ((diff?.newEndpoints?.length ?? 0) > 0 ||
|
|
218
|
+
(diff?.modifiedEndpoints?.length ?? 0) > 0 ||
|
|
219
|
+
(diff?.removedEndpoints?.length ?? 0) > 0);
|
|
220
|
+
}
|
|
221
|
+
function resolveBudgetContext(stateData, fullState) {
|
|
194
222
|
const pbc = stateData.planBudgetContext;
|
|
195
|
-
|
|
223
|
+
const base = {
|
|
196
224
|
maxGenerate: pbc?.maxGenerate ?? MAX_TESTS_TO_GENERATE,
|
|
197
225
|
maxTotal: pbc?.maxTotal ?? MAX_RECOMMENDATIONS,
|
|
198
226
|
isUIOnlyPR: pbc?.isUIOnlyPR ?? false,
|
|
199
227
|
hasFrontendChanges: pbc?.hasFrontendChanges ?? false,
|
|
200
228
|
externalCoverage: new Set(pbc?.externalCoverageKeys ?? []),
|
|
201
229
|
};
|
|
230
|
+
const relatedSections = Object.values(fullState?.relatedRepos ?? {}).map((section) => section.data);
|
|
231
|
+
if (relatedSections.length === 0)
|
|
232
|
+
return base;
|
|
233
|
+
// SKYR-4204: isUIOnlyPR/hasFrontendChanges are per-SECTION facts — each
|
|
234
|
+
// analyze call computes them from its own repo's diff. The approved plan is
|
|
235
|
+
// one run-wide decision, so a frontend-only PRIMARY diff must not put the
|
|
236
|
+
// budgeter on its all-UI branch (backendGenerateCount() = 0) while a related
|
|
237
|
+
// repo carries endpoint changes; that pinned GENERATE to UI placeholders and
|
|
238
|
+
// left every API candidate in ADDITIONAL regardless of maxGenerate. Derive
|
|
239
|
+
// the flags across ALL sections. A related repo checked out on its default
|
|
240
|
+
// branch classifies no endpoint changes, so it leaves a UI-only run UI-only.
|
|
241
|
+
// Budget numbers come from the root (primary) section: the testbot prompt
|
|
242
|
+
// passes topN/maxGenerate only to the primary analyze call, so related
|
|
243
|
+
// sections carry defaults.
|
|
244
|
+
const sections = [fullState ?? undefined, ...relatedSections];
|
|
245
|
+
const hasFrontendChanges = sections.some((s) => s?.planBudgetContext?.hasFrontendChanges ?? false);
|
|
246
|
+
const hasApiChanges = sections.some(sectionHasApiChanges);
|
|
247
|
+
const rootPbc = fullState?.planBudgetContext;
|
|
248
|
+
return {
|
|
249
|
+
maxGenerate: rootPbc?.maxGenerate ?? base.maxGenerate,
|
|
250
|
+
maxTotal: rootPbc?.maxTotal ?? base.maxTotal,
|
|
251
|
+
isUIOnlyPR: hasFrontendChanges && !hasApiChanges,
|
|
252
|
+
hasFrontendChanges,
|
|
253
|
+
externalCoverage: base.externalCoverage,
|
|
254
|
+
};
|
|
202
255
|
}
|
|
203
256
|
function describeGenerationCall(item) {
|
|
204
257
|
switch (item.testType) {
|
|
@@ -316,9 +369,27 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
316
369
|
errorResult = toolError(`State file is empty or invalid: ${params.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
317
370
|
return errorResult;
|
|
318
371
|
}
|
|
319
|
-
|
|
372
|
+
// SKYR-4203: one register call carries candidates for EVERY repo of the
|
|
373
|
+
// run, but `stateData` is a single repo's section — and the candidate
|
|
374
|
+
// schema has no per-candidate repository, so a related repo's anchors
|
|
375
|
+
// were unverifiable by construction. Verify anchors (and rank diff-hunk
|
|
376
|
+
// proximity) against the union of ALL sections' diffs: an anchor
|
|
377
|
+
// grounded in a related repo's change is as real as one in the
|
|
378
|
+
// primary's. Sections with no diff (a related repo checked out on its
|
|
379
|
+
// default branch) are collected so an unmatched anchor reads as
|
|
380
|
+
// uncheckable rather than ungrounded.
|
|
381
|
+
const fullState = await stateManager.readFullState();
|
|
382
|
+
const diffSections = [
|
|
383
|
+
{ repo: fullState?.metadata?.repository ?? "the primary repository", diff: fullState?.diffText },
|
|
384
|
+
...Object.entries(fullState?.relatedRepos ?? {}).map(([repo, section]) => ({
|
|
385
|
+
repo,
|
|
386
|
+
diff: section.data?.diffText,
|
|
387
|
+
})),
|
|
388
|
+
];
|
|
389
|
+
const diffText = diffSections.map((s) => s.diff).filter(Boolean).join("\n");
|
|
390
|
+
const reposWithoutDiff = diffSections.filter((s) => !s.diff).map((s) => s.repo);
|
|
320
391
|
const allScenarios = stateData.repositoryAnalysis?.scenarios ?? [];
|
|
321
|
-
const
|
|
392
|
+
const agentCandidates = buildAgentCandidates(params.candidates ?? []);
|
|
322
393
|
const serverCandidates = recoverServerCandidates(stateData.approvedPlan, allScenarios);
|
|
323
394
|
// Merge by scenario-name identity, NOT the full content-hashed candidateId:
|
|
324
395
|
// the server (analyze_changes) and the agent frequently draft their own
|
|
@@ -341,7 +412,18 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
341
412
|
merged.set(mergeKey(candidate), candidate);
|
|
342
413
|
for (const candidate of serverCandidates)
|
|
343
414
|
merged.set(mergeKey(candidate), candidate);
|
|
344
|
-
|
|
415
|
+
// The agent declares a claim per scenario NAME, so key the claims the
|
|
416
|
+
// same way the merge does and verify against whichever copy survived.
|
|
417
|
+
const claimsByKey = new Map();
|
|
418
|
+
// Keyed through mergeKey, not scenarioMergeKey alone: a name that
|
|
419
|
+
// slugifies to nothing (e.g. an all-non-ASCII name) would be stored
|
|
420
|
+
// under "" while the lookup falls back to the candidateId, so the claim
|
|
421
|
+
// would be neither verified nor demoted.
|
|
422
|
+
(params.candidates ?? []).forEach((input, i) => {
|
|
423
|
+
if (input.discriminator)
|
|
424
|
+
claimsByKey.set(mergeKey(agentCandidates[i]), input.discriminator);
|
|
425
|
+
});
|
|
426
|
+
const { candidates: allCandidates, demotions } = applyDiscriminatorClaims([...merged.values()], claimsByKey, diffText, reposWithoutDiff);
|
|
345
427
|
// An empty union would persist an authoritative plan with an empty
|
|
346
428
|
// GENERATE list, which the generation gate then enforces — bricking
|
|
347
429
|
// the run until re-registration. Reject instead so the agent can
|
|
@@ -351,7 +433,7 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
351
433
|
"Submit your complete candidate list (every test you would generate or recommend) in `candidates` — an empty plan would block all generation.");
|
|
352
434
|
return errorResult;
|
|
353
435
|
}
|
|
354
|
-
const budgetContext = resolveBudgetContext(stateData);
|
|
436
|
+
const budgetContext = resolveBudgetContext(stateData, fullState);
|
|
355
437
|
const result = selectPlan(allCandidates, { ...budgetContext, demotions, diffText });
|
|
356
438
|
const approvedPlan = {
|
|
357
439
|
planId: crypto.randomUUID(),
|
|
@@ -26,6 +26,20 @@ export interface TestExecutionResult extends TestExecutionData {
|
|
|
26
26
|
testFile: string;
|
|
27
27
|
videoPath?: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* One browser execution's recorded video, as persisted in run state by
|
|
31
|
+
* skyramp_execute_test and read back by skyramp_submit_report (SKYR-4156).
|
|
32
|
+
*
|
|
33
|
+
* `status`/`executedAt` are not rendered in the report — they identify WHICH
|
|
34
|
+
* execution the referenced recording belongs to, which matters because a retried
|
|
35
|
+
* test leaves a video directory behind for every attempt.
|
|
36
|
+
*/
|
|
37
|
+
export interface VideoRecord {
|
|
38
|
+
/** Host path to the recorded video.webm. */
|
|
39
|
+
videoPath: string;
|
|
40
|
+
status: TestExecutionStatus;
|
|
41
|
+
executedAt: string;
|
|
42
|
+
}
|
|
29
43
|
export interface BatchExecutionResult {
|
|
30
44
|
totalTests: number;
|
|
31
45
|
passed: number;
|
package/build/types/TestTypes.js
CHANGED
|
@@ -216,8 +216,9 @@ export const baseTestSchema = {
|
|
|
216
216
|
.string()
|
|
217
217
|
.default("")
|
|
218
218
|
.describe("MUST be string of comma separated values like 'id=1,name=John' for URL query parameters. "
|
|
219
|
-
+ "Workspace-configured api.defaultQueryParams (if set) are merged in automatically
|
|
220
|
-
+ "
|
|
219
|
+
+ "Workspace-configured api.defaultQueryParams (if set) are merged in automatically, with any "
|
|
220
|
+
+ "api.queryParamOverrides entry whose pathPattern matches this endpoint layered on top — "
|
|
221
|
+
+ "no need to repeat them here. An explicit value for the same key here overrides both."),
|
|
221
222
|
formParams: z
|
|
222
223
|
.string()
|
|
223
224
|
.default("")
|
|
@@ -2,6 +2,15 @@ import type { DriftAction } from "./TestAnalysis.js";
|
|
|
2
2
|
import type { TestExecutionStatus } from "./TestExecution.js";
|
|
3
3
|
import type { HttpMethod, TestType } from "./TestTypes.js";
|
|
4
4
|
import type { ReuseOutcome } from "./ReuseOutcome.js";
|
|
5
|
+
/** Classification of an issuesFound entry. Bug is a product/code defect; the
|
|
6
|
+
* other values are tooling noise that the Testbot report renders in its
|
|
7
|
+
* Configuration Errors section instead of Issues Found. */
|
|
8
|
+
export declare enum IssueFoundCategory {
|
|
9
|
+
Bug = "bug",
|
|
10
|
+
Lint = "lint",
|
|
11
|
+
Type = "type",
|
|
12
|
+
Config = "config"
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* Shape of the JSON report written by skyramp_submit_report and read by testbot
|
|
7
16
|
* for rendering as Markdown. All fields mirror the corresponding Zod schemas in
|
|
@@ -69,6 +78,7 @@ export interface TestbotReport {
|
|
|
69
78
|
issuesFound: {
|
|
70
79
|
description: string;
|
|
71
80
|
severity?: "critical" | "high" | "medium" | "low";
|
|
81
|
+
category: IssueFoundCategory;
|
|
72
82
|
}[];
|
|
73
83
|
nextSteps: string[];
|
|
74
84
|
commitMessage: string;
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/** Classification of an issuesFound entry. Bug is a product/code defect; the
|
|
2
|
+
* other values are tooling noise that the Testbot report renders in its
|
|
3
|
+
* Configuration Errors section instead of Issues Found. */
|
|
4
|
+
export var IssueFoundCategory;
|
|
5
|
+
(function (IssueFoundCategory) {
|
|
6
|
+
IssueFoundCategory["Bug"] = "bug";
|
|
7
|
+
IssueFoundCategory["Lint"] = "lint";
|
|
8
|
+
IssueFoundCategory["Type"] = "type";
|
|
9
|
+
IssueFoundCategory["Config"] = "config";
|
|
10
|
+
})(IssueFoundCategory || (IssueFoundCategory = {}));
|
package/build/types/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { TestExecutionStatus } from "./TestExecution.js";
|
|
|
2
2
|
export { DriftAction } from "./TestAnalysis.js";
|
|
3
3
|
export { TestType, HttpMethod } from "./TestTypes.js";
|
|
4
4
|
export type { TestbotReport } from "./TestbotReport.js";
|
|
5
|
+
export { IssueFoundCategory } from "./TestbotReport.js";
|
|
5
6
|
export { ReuseDeclinedBy, ReuseVerificationOutcome } from "./ReuseOutcome.js";
|
|
6
7
|
export type { ReuseOutcome, ReuseSkippedEntry } from "./ReuseOutcome.js";
|
|
7
8
|
export type { RelatedRepository, TestbotPromptOptions, } from "./TestbotPromptOptions.js";
|
package/build/types/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { TestExecutionStatus } from "./TestExecution.js";
|
|
2
2
|
export { DriftAction } from "./TestAnalysis.js";
|
|
3
3
|
export { TestType, HttpMethod } from "./TestTypes.js";
|
|
4
|
+
export { IssueFoundCategory } from "./TestbotReport.js";
|
|
4
5
|
export { ReuseDeclinedBy, ReuseVerificationOutcome } from "./ReuseOutcome.js";
|
|
@@ -7,13 +7,33 @@ import type { CandidateUiPage } from "./uiPageEnumerator.js";
|
|
|
7
7
|
import type { FrontendFileIntegration } from "../types/FrontendIntegration.js";
|
|
8
8
|
import type { ApprovedPlanItem } from "../types/Recommendation.js";
|
|
9
9
|
import type { ExternalTestRunRecord } from "../types/ExternalTestExecution.js";
|
|
10
|
+
import type { VideoRecord } from "../types/TestExecution.js";
|
|
10
11
|
export type { CandidateUiPage } from "./uiPageEnumerator.js";
|
|
11
12
|
export declare function setTestsRepoDir(dir: string | undefined): void;
|
|
12
13
|
export declare function getTestsRepoDir(): string | undefined;
|
|
13
|
-
/** Filename of the run-scoped analysis state file under
|
|
14
|
+
/** Filename of the run-scoped analysis state file under `runArtifactDir()`.
|
|
14
15
|
* Single-sourced so the constructor that WRITES there and `resolveRunStatePath`
|
|
15
16
|
* which LOOKS there cannot drift apart. */
|
|
16
17
|
export declare const RUN_STATE_FILE_NAME = "analyze-changes-state.json";
|
|
18
|
+
/**
|
|
19
|
+
* Directory holding this run's Skyramp artifacts — the state file, and the report written
|
|
20
|
+
* beside it (SKYR-4147) — or undefined outside a run.
|
|
21
|
+
*
|
|
22
|
+
* `RUNNER_TEMP` is a fresh directory per CI job, and the caller and this server can each
|
|
23
|
+
* work it out from the environment on their own. So neither has to tell the other where
|
|
24
|
+
* the run's files live, and the path never passes through the model.
|
|
25
|
+
*
|
|
26
|
+
* The GitHub Action is the only caller customers run, and GitHub sets `RUNNER_TEMP` for
|
|
27
|
+
* it; the eval harness sets it itself. Nothing customers run is on another CI system, so
|
|
28
|
+
* the GitHub-specific name is only a naming problem — it does not break anything today.
|
|
29
|
+
* If a caller on another CI system is added, rename this then, or let it read that
|
|
30
|
+
* system's variable. Do not add a second variable for a caller that does not exist.
|
|
31
|
+
*
|
|
32
|
+
* Kept in one place for the same reason `RUN_STATE_FILE_NAME` is: the directory used to be
|
|
33
|
+
* written out at three call sites, so only the filename was safe from the three copies
|
|
34
|
+
* drifting apart. testbot keeps its matching copy in one place too, in `skyrampTempDir()`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runArtifactDir(): string | undefined;
|
|
17
37
|
export declare function setActiveRunStatePath(stateFilePath: string): void;
|
|
18
38
|
export declare function getActiveRunStatePath(): string | undefined;
|
|
19
39
|
/** Reset the run anchor — for tests, and as a safety hook between runs. */
|
|
@@ -25,7 +45,7 @@ export declare function clearActiveRunStatePath(): void;
|
|
|
25
45
|
* `setActiveRunStatePath` is only called by skyramp_analyze_changes on multi-repo
|
|
26
46
|
* runs (the ones carrying a `repository` param), but a single-repo Testbot run's
|
|
27
47
|
* path is still deterministic in CI: the StateManager constructor above roots
|
|
28
|
-
* "analysis" state under
|
|
48
|
+
* "analysis" state under `runArtifactDir()`. Falling back to that covers
|
|
29
49
|
* single-repo runs without a new anchor mechanism. Standalone/IDE usage has no
|
|
30
50
|
* RUNNER_TEMP and resolves to undefined — correct, since there is no run state.
|
|
31
51
|
*
|
|
@@ -166,6 +186,20 @@ export interface UnifiedAnalysisState {
|
|
|
166
186
|
* which computes every value in-process; skyramp_submit_report merges it into
|
|
167
187
|
* the report's `reuse` field. Never supplied by the LLM. */
|
|
168
188
|
reuseOutcomes?: Record<string, ReuseRecord>;
|
|
189
|
+
/**
|
|
190
|
+
* SKYR-4156. Recorded video per executed browser test, keyed by test-file
|
|
191
|
+
* BASENAME (the same key `reuseOutcomes` uses, so matching needs no path
|
|
192
|
+
* normalization). Written in-process by skyramp_execute_test — the only place
|
|
193
|
+
* the path is known — and merged into the report's `testResults[].videoPath` by
|
|
194
|
+
* skyramp_submit_report. Never supplied by the LLM.
|
|
195
|
+
*
|
|
196
|
+
* Deliberately separate from `existingTests[].executionBefore/After`: those only
|
|
197
|
+
* exist for tests skyramp_analyze_changes discovered, so a NEWLY generated spec
|
|
198
|
+
* has nowhere to record into — which is exactly how one run recorded three videos
|
|
199
|
+
* and referenced none of them. Last write wins, so a retried execution replaces
|
|
200
|
+
* the earlier attempt's now-stale directory.
|
|
201
|
+
*/
|
|
202
|
+
executionVideos?: Record<string, VideoRecord>;
|
|
169
203
|
}
|
|
170
204
|
/**
|
|
171
205
|
* State file metadata
|
|
@@ -35,10 +35,32 @@ export function getTestsRepoDir() {
|
|
|
35
35
|
* Single-repo / standalone calls (no `repository`) never set or read this.
|
|
36
36
|
*/
|
|
37
37
|
let _activeRunStatePath;
|
|
38
|
-
/** Filename of the run-scoped analysis state file under
|
|
38
|
+
/** Filename of the run-scoped analysis state file under `runArtifactDir()`.
|
|
39
39
|
* Single-sourced so the constructor that WRITES there and `resolveRunStatePath`
|
|
40
40
|
* which LOOKS there cannot drift apart. */
|
|
41
41
|
export const RUN_STATE_FILE_NAME = "analyze-changes-state.json";
|
|
42
|
+
/**
|
|
43
|
+
* Directory holding this run's Skyramp artifacts — the state file, and the report written
|
|
44
|
+
* beside it (SKYR-4147) — or undefined outside a run.
|
|
45
|
+
*
|
|
46
|
+
* `RUNNER_TEMP` is a fresh directory per CI job, and the caller and this server can each
|
|
47
|
+
* work it out from the environment on their own. So neither has to tell the other where
|
|
48
|
+
* the run's files live, and the path never passes through the model.
|
|
49
|
+
*
|
|
50
|
+
* The GitHub Action is the only caller customers run, and GitHub sets `RUNNER_TEMP` for
|
|
51
|
+
* it; the eval harness sets it itself. Nothing customers run is on another CI system, so
|
|
52
|
+
* the GitHub-specific name is only a naming problem — it does not break anything today.
|
|
53
|
+
* If a caller on another CI system is added, rename this then, or let it read that
|
|
54
|
+
* system's variable. Do not add a second variable for a caller that does not exist.
|
|
55
|
+
*
|
|
56
|
+
* Kept in one place for the same reason `RUN_STATE_FILE_NAME` is: the directory used to be
|
|
57
|
+
* written out at three call sites, so only the filename was safe from the three copies
|
|
58
|
+
* drifting apart. testbot keeps its matching copy in one place too, in `skyrampTempDir()`.
|
|
59
|
+
*/
|
|
60
|
+
export function runArtifactDir() {
|
|
61
|
+
const runnerTemp = process.env.RUNNER_TEMP?.trim();
|
|
62
|
+
return runnerTemp ? path.join(runnerTemp, "skyramp") : undefined;
|
|
63
|
+
}
|
|
42
64
|
export function setActiveRunStatePath(stateFilePath) {
|
|
43
65
|
_activeRunStatePath = stateFilePath;
|
|
44
66
|
}
|
|
@@ -56,7 +78,7 @@ export function clearActiveRunStatePath() {
|
|
|
56
78
|
* `setActiveRunStatePath` is only called by skyramp_analyze_changes on multi-repo
|
|
57
79
|
* runs (the ones carrying a `repository` param), but a single-repo Testbot run's
|
|
58
80
|
* path is still deterministic in CI: the StateManager constructor above roots
|
|
59
|
-
* "analysis" state under
|
|
81
|
+
* "analysis" state under `runArtifactDir()`. Falling back to that covers
|
|
60
82
|
* single-repo runs without a new anchor mechanism. Standalone/IDE usage has no
|
|
61
83
|
* RUNNER_TEMP and resolves to undefined — correct, since there is no run state.
|
|
62
84
|
*
|
|
@@ -70,8 +92,9 @@ export function resolveRunStatePath(explicitPath) {
|
|
|
70
92
|
const anchored = getActiveRunStatePath();
|
|
71
93
|
if (anchored)
|
|
72
94
|
return anchored;
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
const runDir = runArtifactDir();
|
|
96
|
+
if (runDir) {
|
|
97
|
+
return path.join(runDir, RUN_STATE_FILE_NAME);
|
|
75
98
|
}
|
|
76
99
|
return undefined;
|
|
77
100
|
}
|
|
@@ -169,17 +192,15 @@ export class StateManager {
|
|
|
169
192
|
constructor(stateType = "analysis", sessionId, stateDir, stateFilePath) {
|
|
170
193
|
this.stateType = stateType;
|
|
171
194
|
this.sessionId = sessionId || crypto.randomUUID();
|
|
195
|
+
const runDir = stateType === "analysis" ? runArtifactDir() : undefined;
|
|
172
196
|
if (stateFilePath) {
|
|
173
197
|
this.stateFile = stateFilePath;
|
|
174
198
|
}
|
|
175
|
-
else if (
|
|
176
|
-
// In CI
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
// (Jenkins, GitLab, Buildkite) must set it explicitly. Once merged, path flows through
|
|
181
|
-
// summaryOutputFile which is already testbot-controlled and CI-agnostic.
|
|
182
|
-
this.stateFile = path.join(process.env.RUNNER_TEMP, "skyramp", RUN_STATE_FILE_NAME);
|
|
199
|
+
else if (runDir) {
|
|
200
|
+
// In CI, the run directory is fresh per job and the caller and this tool each work
|
|
201
|
+
// it out from the environment — no LLM input needed. It is also where
|
|
202
|
+
// skyramp_submit_report writes the report (SKYR-4147).
|
|
203
|
+
this.stateFile = path.join(runDir, RUN_STATE_FILE_NAME);
|
|
183
204
|
}
|
|
184
205
|
else {
|
|
185
206
|
const baseDir = stateDir || os.tmpdir();
|
|
@@ -435,7 +456,7 @@ export class StateManager {
|
|
|
435
456
|
* @returns Number of files deleted
|
|
436
457
|
*/
|
|
437
458
|
static async cleanupOldFiles(maxAgeHours = 24, stateDir, stateTypes) {
|
|
438
|
-
const baseDir = stateDir || (
|
|
459
|
+
const baseDir = stateDir || runArtifactDir() || os.tmpdir();
|
|
439
460
|
const files = await fs.promises.readdir(baseDir).catch(() => []);
|
|
440
461
|
const statePrefixes = stateTypes
|
|
441
462
|
? stateTypes.map((t) => STATE_FILE_PREFIXES[t])
|
|
@@ -905,7 +905,13 @@ export function draftResponseShapeScenarios(hopEndpoints, byFile, addedFieldsByF
|
|
|
905
905
|
: `reflects the change made in ${fileName}`;
|
|
906
906
|
const nameSuffix = addedFields.length > 0 ? addedFields[0] : "response-change";
|
|
907
907
|
scenarios.push({
|
|
908
|
-
|
|
908
|
+
// The endpoint path is part of the name because one file can reach
|
|
909
|
+
// several GETs and neither the resource token (from the file) nor the
|
|
910
|
+
// suffix (a field name, or the constant "response-change" when the hunk
|
|
911
|
+
// yields none) varies between them. Two same-named candidates are
|
|
912
|
+
// merged by name in registerTestPlanTool, so a collision here loses a
|
|
913
|
+
// scenario outright rather than showing up as a duplicate.
|
|
914
|
+
scenarioName: `${slugify(resource)}-response-shape-${slugify(nameSuffix)}-${slugify(ep.path)}`,
|
|
909
915
|
description: `Verify GET ${ep.path} response ${fieldsPhrase}.`,
|
|
910
916
|
category: "breaking_change",
|
|
911
917
|
priority: "high",
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Skill content for skyramp.md — installed at auto-discovery paths: ~/.claude/skills/skyramp/SKILLS.md, ~/.cursor/skills/skyramp/SKILLS.md, ~/.github/skills/skyramp.md
|
|
3
3
|
* Follows the SKILL.md specification: https://agentskills.io/what-are-skills#the-skill-md-file
|
|
4
4
|
*/
|
|
5
|
-
export declare const SKYRAMP_MD_CONTENT = "---\nname: skyramp\ndescription: Generate, execute, and maintain API tests (smoke, contract, fuzz, load, integration, E2E, UI) using Skyramp MCP tools.\n---\n\n# Skyramp\n\n## When to use this skill\nUse this skill whenever the user asks to generate, run, or maintain API tests. Always read `<workspace-root>/.skyramp/workspace.yml` first to get `language`, `framework`, `outputDir`, and `api.baseUrl` \u2014 do not ask the user for values already present.\n\n---\n\n## Tools\n\n### Workspace Setup\n1. **`skyramp_initialize_workspace`** \u2014 Create or update `.skyramp/workspace.yml` in a git repository workspace. Scan the repo for all services before calling. Required before any other Skyramp tool.\n\n### Test Generation\n2. **`skyramp_smoke_test_generation`** \u2014 Verify an endpoint is reachable and returns a valid response.\n3. **`skyramp_contract_test_generation`** \u2014 Validate implementation matches OpenAPI/Swagger schema.\n4. **`skyramp_fuzz_test_generation`** \u2014 Send malformed or boundary inputs to find edge cases and security issues.\n5. **`skyramp_load_test_generation`** \u2014 Test performance under concurrent load. Optional: `loadDuration`, `loadNumThreads`. Accepts `trace` instead of `apiSchema`/`endpointURL`.\n6. **`skyramp_integration_test_generation`** \u2014 Multi-step workflows across one or more services. Supply one of: `apiSchema`+`endpointURL`, `trace`, or `scenarioFile`. Do not combine them.\n7. **`skyramp_e2e_test_generation`** \u2014 Full user journey covering UI and backend. Requires `trace` and `playwrightInput` zip. Do not pass `apiSchema` or `endpointURL`.\n8. **`skyramp_ui_test_generation`** \u2014 UI-only tests from Playwright recordings. Requires `playwrightInput` zip.\n\n### Trace Generation\n9. **`skyramp_start_trace_collection`** \u2014 Start capturing backend traffic. Set `playwright: true` for UI or E2E tests. Use an absolute path for `outputDir`.\n10. **`skyramp_stop_trace_collection`** \u2014 Stop capture and save the trace. Use the same `outputDir` and `playwrightEnabled` values as start.\n11. **`skyramp_scenario_test_generation`** \u2014 Scenario trace generation. Describe a multi-step flow in natural language to produce a scenario file. Pass output to `skyramp_integration_test_generation` via `scenarioFile`.\n\n### Test Execution\n12. **`skyramp_execute_test`** \u2014 Run a single Skyramp-generated test file. Required: `workspacePath`, `language`, `testType`, `testFile`. Optional: `stateFile` (writes execution results back for health analysis). For multiple tests, call sequentially to avoid env var conflicts.\n\n### Test Analysis & Maintenance\n13. **`skyramp_analyze_changes`** \u2014 Unified entry point: scans endpoints, discovers tests, computes diff. Takes `repositoryPath` and `scope`. Returns `stateFile` + recommendations.\n14. **`skyramp_analyze_test_health`** \u2014 Drift and health assessment for existing tests. Takes `stateFile`. Returns LLM prompt for scoring.\n15. **`skyramp_actions`** \u2014 Execute UPDATE / REGENERATE / VERIFY / DELETE actions. Takes `stateFile`. Call after analyze_test_health.\n\n### Code Quality\n16. **`skyramp_fix_errors`** \u2014 Fix compilation or runtime errors in a generated test file.\n17. **`skyramp_modularization`** \u2014 Refactor a test file into reusable modules. Set `isTraceBased: true` for trace-based tests.\n18. **`skyramp_reuse_code`** \u2014 Pull shared helpers from other Skyramp tests. Only when `code_reuse` was `true` at generation time.\n\n### Authentication\n19. **`skyramp_login`** \u2014 Log in to the Skyramp platform.\n20. **`skyramp_logout`** \u2014 Log out from the Skyramp platform.\n\n---\n\n## Prompts\n\nPrefer invoking a prompt over manually chaining tools \u2014 prompts run the full workflow automatically.\n\n- **`skyramp_trace_prompt`** \u2014 Trace collection setup and execution.\n- **`skyramp_test_health_analysis`** \u2014 Full maintenance flow (discover \u2192 drift \u2192 health \u2192 actions).\n- **`skyramp_testbot`** \u2014 PR-scoped recommendations + maintenance + report. Required: `prTitle`, `prDescription`, `
|
|
5
|
+
export declare const SKYRAMP_MD_CONTENT = "---\nname: skyramp\ndescription: Generate, execute, and maintain API tests (smoke, contract, fuzz, load, integration, E2E, UI) using Skyramp MCP tools.\n---\n\n# Skyramp\n\n## When to use this skill\nUse this skill whenever the user asks to generate, run, or maintain API tests. Always read `<workspace-root>/.skyramp/workspace.yml` first to get `language`, `framework`, `outputDir`, and `api.baseUrl` \u2014 do not ask the user for values already present.\n\n---\n\n## Tools\n\n### Workspace Setup\n1. **`skyramp_initialize_workspace`** \u2014 Create or update `.skyramp/workspace.yml` in a git repository workspace. Scan the repo for all services before calling. Required before any other Skyramp tool.\n\n### Test Generation\n2. **`skyramp_smoke_test_generation`** \u2014 Verify an endpoint is reachable and returns a valid response.\n3. **`skyramp_contract_test_generation`** \u2014 Validate implementation matches OpenAPI/Swagger schema.\n4. **`skyramp_fuzz_test_generation`** \u2014 Send malformed or boundary inputs to find edge cases and security issues.\n5. **`skyramp_load_test_generation`** \u2014 Test performance under concurrent load. Optional: `loadDuration`, `loadNumThreads`. Accepts `trace` instead of `apiSchema`/`endpointURL`.\n6. **`skyramp_integration_test_generation`** \u2014 Multi-step workflows across one or more services. Supply one of: `apiSchema`+`endpointURL`, `trace`, or `scenarioFile`. Do not combine them.\n7. **`skyramp_e2e_test_generation`** \u2014 Full user journey covering UI and backend. Requires `trace` and `playwrightInput` zip. Do not pass `apiSchema` or `endpointURL`.\n8. **`skyramp_ui_test_generation`** \u2014 UI-only tests from Playwright recordings. Requires `playwrightInput` zip.\n\n### Trace Generation\n9. **`skyramp_start_trace_collection`** \u2014 Start capturing backend traffic. Set `playwright: true` for UI or E2E tests. Use an absolute path for `outputDir`.\n10. **`skyramp_stop_trace_collection`** \u2014 Stop capture and save the trace. Use the same `outputDir` and `playwrightEnabled` values as start.\n11. **`skyramp_scenario_test_generation`** \u2014 Scenario trace generation. Describe a multi-step flow in natural language to produce a scenario file. Pass output to `skyramp_integration_test_generation` via `scenarioFile`.\n\n### Test Execution\n12. **`skyramp_execute_test`** \u2014 Run a single Skyramp-generated test file. Required: `workspacePath`, `language`, `testType`, `testFile`. Optional: `stateFile` (writes execution results back for health analysis). For multiple tests, call sequentially to avoid env var conflicts.\n\n### Test Analysis & Maintenance\n13. **`skyramp_analyze_changes`** \u2014 Unified entry point: scans endpoints, discovers tests, computes diff. Takes `repositoryPath` and `scope`. Returns `stateFile` + recommendations.\n14. **`skyramp_analyze_test_health`** \u2014 Drift and health assessment for existing tests. Takes `stateFile`. Returns LLM prompt for scoring.\n15. **`skyramp_actions`** \u2014 Execute UPDATE / REGENERATE / VERIFY / DELETE actions. Takes `stateFile`. Call after analyze_test_health.\n\n### Code Quality\n16. **`skyramp_fix_errors`** \u2014 Fix compilation or runtime errors in a generated test file.\n17. **`skyramp_modularization`** \u2014 Refactor a test file into reusable modules. Set `isTraceBased: true` for trace-based tests.\n18. **`skyramp_reuse_code`** \u2014 Pull shared helpers from other Skyramp tests. Only when `code_reuse` was `true` at generation time.\n\n### Authentication\n19. **`skyramp_login`** \u2014 Log in to the Skyramp platform.\n20. **`skyramp_logout`** \u2014 Log out from the Skyramp platform.\n\n---\n\n## Prompts\n\nPrefer invoking a prompt over manually chaining tools \u2014 prompts run the full workflow automatically.\n\n- **`skyramp_trace_prompt`** \u2014 Trace collection setup and execution.\n- **`skyramp_test_health_analysis`** \u2014 Full maintenance flow (discover \u2192 drift \u2192 health \u2192 actions).\n- **`skyramp_testbot`** \u2014 PR-scoped recommendations + maintenance + report. Required: `prTitle`, `prDescription`, `repositoryPath`.\n\n---\n\n## Workflows\n\nUse these when a prompt is not available.\n\n**Generate and run a test**\nRead `.skyramp/workspace.yml` \u2192 Call appropriate generation tool \u2192 `skyramp_execute_test`\n\n**Trace-based test (integration / load / E2E / UI)**\n`skyramp_start_trace_collection` (set `playwright: true` for UI/E2E) \u2192 User exercises the app \u2192 `skyramp_stop_trace_collection` \u2192 Call target generation tool with `trace` (and `playwrightInput` for E2E/UI)\n\n**Scenario \u2192 integration test**\n`skyramp_scenario_test_generation` \u2192 `skyramp_integration_test_generation` with `scenarioFile`\n\n**Recommend tests for a PR**\n`skyramp_analyze_changes` (with `scope: \"branch_diff\"`) \u2192 follow enrichment steps \u2192 `skyramp_recommend_tests` \u2192 Generate recommended tests\n\n**Maintain existing tests**\n`skyramp_analyze_changes` \u2192 `skyramp_analyze_test_health` \u2192 *(optional)* execute tests via `skyramp_execute_test` with `stateFile` param (writes results back) \u2192 `skyramp_actions` (do not skip)\n\n---\n\n## Conventions\n\n- **`endpointURL`** \u2014 Full URL to a specific endpoint, not just the base URL. Build from `api.baseUrl` + path (e.g. `http://localhost:8000/api/v1/users`).\n- **`outputDir`** \u2014 Use absolute paths for both test output and trace collection.\n- **Mutually exclusive inputs** \u2014 `apiSchema`/`endpointURL`, `trace`, and `scenarioFile` are mutually exclusive for integration and load tests. Use exactly one.\n- **Sequential execution** \u2014 Execute tests sequentially (not in parallel) to avoid environment variable conflicts with `SKYRAMP_TEST_BASE_URL`.\n";
|
|
@@ -58,7 +58,7 @@ Prefer invoking a prompt over manually chaining tools — prompts run the full w
|
|
|
58
58
|
|
|
59
59
|
- **\`skyramp_trace_prompt\`** — Trace collection setup and execution.
|
|
60
60
|
- **\`skyramp_test_health_analysis\`** — Full maintenance flow (discover → drift → health → actions).
|
|
61
|
-
- **\`skyramp_testbot\`** — PR-scoped recommendations + maintenance + report. Required: \`prTitle\`, \`prDescription\`, \`
|
|
61
|
+
- **\`skyramp_testbot\`** — PR-scoped recommendations + maintenance + report. Required: \`prTitle\`, \`prDescription\`, \`repositoryPath\`.
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL/path derivations shared by the generation tools.
|
|
3
|
+
*
|
|
4
|
+
* Both helpers here existed as private copies before: `pathFromEndpointURL`
|
|
5
|
+
* lived in tools/generate-tests/planGuard.ts, and the base-path derivation was
|
|
6
|
+
* inlined in ScenarioGenerationService and then mirrored a second time by
|
|
7
|
+
* generateBatchScenarioRestTool so it could predict the path the service would
|
|
8
|
+
* emit (SKYR-4127). Two copies of "which path does this request actually have"
|
|
9
|
+
* is exactly the kind of thing that drifts silently — a pattern that stops
|
|
10
|
+
* matching produces a 403, not a test failure — so they live in one place now.
|
|
11
|
+
*
|
|
12
|
+
* Dependency-free on purpose: this sits below both services/ and tools/ so
|
|
13
|
+
* either layer can import it without an inversion.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Extract the URL path from a generation tool's `endpointURL` param.
|
|
17
|
+
*
|
|
18
|
+
* Falls back to treating the value as a bare path when it isn't a parseable
|
|
19
|
+
* absolute URL, and to undefined when it's neither.
|
|
20
|
+
*
|
|
21
|
+
* `new URL()` percent-encodes `{param}` placeholders (`{id}` → `%7Bid%7D`),
|
|
22
|
+
* which breaks two consumers: plan matchKeys are computed from raw scenario
|
|
23
|
+
* paths with braces intact, and workspace queryParamOverrides patterns are
|
|
24
|
+
* authored with braces. Restore them.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pathFromEndpointURL(endpointURL: string | undefined): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The path prefix a service's `baseURL` contributes to every request under it:
|
|
29
|
+
* the URL's pathname with any trailing slash removed. Empty string when
|
|
30
|
+
* `baseURL` is absent or unparseable, and for a host-only URL (`http://host`
|
|
31
|
+
* → pathname `/` → `""`).
|
|
32
|
+
*
|
|
33
|
+
* This is the value ScenarioGenerationService prepends to each step path, so
|
|
34
|
+
* anything that needs to reason about the emitted path — such as matching
|
|
35
|
+
* workspace queryParamOverrides — must derive it the same way.
|
|
36
|
+
*/
|
|
37
|
+
export declare function deriveBasePath(baseURL: string | undefined): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL/path derivations shared by the generation tools.
|
|
3
|
+
*
|
|
4
|
+
* Both helpers here existed as private copies before: `pathFromEndpointURL`
|
|
5
|
+
* lived in tools/generate-tests/planGuard.ts, and the base-path derivation was
|
|
6
|
+
* inlined in ScenarioGenerationService and then mirrored a second time by
|
|
7
|
+
* generateBatchScenarioRestTool so it could predict the path the service would
|
|
8
|
+
* emit (SKYR-4127). Two copies of "which path does this request actually have"
|
|
9
|
+
* is exactly the kind of thing that drifts silently — a pattern that stops
|
|
10
|
+
* matching produces a 403, not a test failure — so they live in one place now.
|
|
11
|
+
*
|
|
12
|
+
* Dependency-free on purpose: this sits below both services/ and tools/ so
|
|
13
|
+
* either layer can import it without an inversion.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Extract the URL path from a generation tool's `endpointURL` param.
|
|
17
|
+
*
|
|
18
|
+
* Falls back to treating the value as a bare path when it isn't a parseable
|
|
19
|
+
* absolute URL, and to undefined when it's neither.
|
|
20
|
+
*
|
|
21
|
+
* `new URL()` percent-encodes `{param}` placeholders (`{id}` → `%7Bid%7D`),
|
|
22
|
+
* which breaks two consumers: plan matchKeys are computed from raw scenario
|
|
23
|
+
* paths with braces intact, and workspace queryParamOverrides patterns are
|
|
24
|
+
* authored with braces. Restore them.
|
|
25
|
+
*/
|
|
26
|
+
export function pathFromEndpointURL(endpointURL) {
|
|
27
|
+
if (!endpointURL)
|
|
28
|
+
return undefined;
|
|
29
|
+
try {
|
|
30
|
+
return new URL(endpointURL).pathname.replace(/%7B/gi, "{").replace(/%7D/gi, "}");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return endpointURL.startsWith("/") ? endpointURL : undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The path prefix a service's `baseURL` contributes to every request under it:
|
|
38
|
+
* the URL's pathname with any trailing slash removed. Empty string when
|
|
39
|
+
* `baseURL` is absent or unparseable, and for a host-only URL (`http://host`
|
|
40
|
+
* → pathname `/` → `""`).
|
|
41
|
+
*
|
|
42
|
+
* This is the value ScenarioGenerationService prepends to each step path, so
|
|
43
|
+
* anything that needs to reason about the emitted path — such as matching
|
|
44
|
+
* workspace queryParamOverrides — must derive it the same way.
|
|
45
|
+
*/
|
|
46
|
+
export function deriveBasePath(baseURL) {
|
|
47
|
+
if (!baseURL)
|
|
48
|
+
return "";
|
|
49
|
+
try {
|
|
50
|
+
return new URL(baseURL).pathname.replace(/\/$/, "");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|