@skyramp/mcp 0.3.3 → 0.3.5
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 +16 -14
- 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 +257 -30
- 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 +41 -35
- package/build/tools/test-management/analyzeTestHealthTool.js +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +191 -41
- 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 +24 -1
- 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 +48 -2
- package/build/utils/AnalysisStateManager.js +49 -13
- package/build/utils/reportLanguage.d.ts +43 -0
- package/build/utils/reportLanguage.js +125 -0
- package/build/utils/reportVerification.d.ts +10 -0
- package/build/utils/reportVerification.js +31 -0
- 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
|
@@ -7,13 +7,45 @@ 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;
|
|
13
|
+
/**
|
|
14
|
+
* Record the run's tests repo without ever clearing a known value.
|
|
15
|
+
*
|
|
16
|
+
* The tests repo is a RUN-scoped fact, but analyze_changes is called once per
|
|
17
|
+
* repository and only the PRIMARY call carries `testsRepoDir` — a related
|
|
18
|
+
* repo's call omits it. An unconditional set therefore wiped the primary's
|
|
19
|
+
* value on the second call of every multi-repo run, and by report time the
|
|
20
|
+
* tests repo was unknown: a UI test generated there looked unbacked by any
|
|
21
|
+
* working tree and the agent demoted it out of newTestsCreated (SKYR-4204,
|
|
22
|
+
* run 32536569467). Callers that mean to clear it use setTestsRepoDir.
|
|
23
|
+
*/
|
|
24
|
+
export declare function rememberTestsRepoDir(dir: string | undefined): void;
|
|
12
25
|
export declare function getTestsRepoDir(): string | undefined;
|
|
13
|
-
/** Filename of the run-scoped analysis state file under
|
|
26
|
+
/** Filename of the run-scoped analysis state file under `runArtifactDir()`.
|
|
14
27
|
* Single-sourced so the constructor that WRITES there and `resolveRunStatePath`
|
|
15
28
|
* which LOOKS there cannot drift apart. */
|
|
16
29
|
export declare const RUN_STATE_FILE_NAME = "analyze-changes-state.json";
|
|
30
|
+
/**
|
|
31
|
+
* Directory holding this run's Skyramp artifacts — the state file, and the report written
|
|
32
|
+
* beside it (SKYR-4147) — or undefined outside a run.
|
|
33
|
+
*
|
|
34
|
+
* `RUNNER_TEMP` is a fresh directory per CI job, and the caller and this server can each
|
|
35
|
+
* work it out from the environment on their own. So neither has to tell the other where
|
|
36
|
+
* the run's files live, and the path never passes through the model.
|
|
37
|
+
*
|
|
38
|
+
* The GitHub Action is the only caller customers run, and GitHub sets `RUNNER_TEMP` for
|
|
39
|
+
* it; the eval harness sets it itself. Nothing customers run is on another CI system, so
|
|
40
|
+
* the GitHub-specific name is only a naming problem — it does not break anything today.
|
|
41
|
+
* If a caller on another CI system is added, rename this then, or let it read that
|
|
42
|
+
* system's variable. Do not add a second variable for a caller that does not exist.
|
|
43
|
+
*
|
|
44
|
+
* Kept in one place for the same reason `RUN_STATE_FILE_NAME` is: the directory used to be
|
|
45
|
+
* written out at three call sites, so only the filename was safe from the three copies
|
|
46
|
+
* drifting apart. testbot keeps its matching copy in one place too, in `skyrampTempDir()`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runArtifactDir(): string | undefined;
|
|
17
49
|
export declare function setActiveRunStatePath(stateFilePath: string): void;
|
|
18
50
|
export declare function getActiveRunStatePath(): string | undefined;
|
|
19
51
|
/** Reset the run anchor — for tests, and as a safety hook between runs. */
|
|
@@ -25,7 +57,7 @@ export declare function clearActiveRunStatePath(): void;
|
|
|
25
57
|
* `setActiveRunStatePath` is only called by skyramp_analyze_changes on multi-repo
|
|
26
58
|
* runs (the ones carrying a `repository` param), but a single-repo Testbot run's
|
|
27
59
|
* path is still deterministic in CI: the StateManager constructor above roots
|
|
28
|
-
* "analysis" state under
|
|
60
|
+
* "analysis" state under `runArtifactDir()`. Falling back to that covers
|
|
29
61
|
* single-repo runs without a new anchor mechanism. Standalone/IDE usage has no
|
|
30
62
|
* RUNNER_TEMP and resolves to undefined — correct, since there is no run state.
|
|
31
63
|
*
|
|
@@ -166,6 +198,20 @@ export interface UnifiedAnalysisState {
|
|
|
166
198
|
* which computes every value in-process; skyramp_submit_report merges it into
|
|
167
199
|
* the report's `reuse` field. Never supplied by the LLM. */
|
|
168
200
|
reuseOutcomes?: Record<string, ReuseRecord>;
|
|
201
|
+
/**
|
|
202
|
+
* SKYR-4156. Recorded video per executed browser test, keyed by test-file
|
|
203
|
+
* BASENAME (the same key `reuseOutcomes` uses, so matching needs no path
|
|
204
|
+
* normalization). Written in-process by skyramp_execute_test — the only place
|
|
205
|
+
* the path is known — and merged into the report's `testResults[].videoPath` by
|
|
206
|
+
* skyramp_submit_report. Never supplied by the LLM.
|
|
207
|
+
*
|
|
208
|
+
* Deliberately separate from `existingTests[].executionBefore/After`: those only
|
|
209
|
+
* exist for tests skyramp_analyze_changes discovered, so a NEWLY generated spec
|
|
210
|
+
* has nowhere to record into — which is exactly how one run recorded three videos
|
|
211
|
+
* and referenced none of them. Last write wins, so a retried execution replaces
|
|
212
|
+
* the earlier attempt's now-stale directory.
|
|
213
|
+
*/
|
|
214
|
+
executionVideos?: Record<string, VideoRecord>;
|
|
169
215
|
}
|
|
170
216
|
/**
|
|
171
217
|
* State file metadata
|
|
@@ -19,6 +19,21 @@ let _testsRepoDir;
|
|
|
19
19
|
export function setTestsRepoDir(dir) {
|
|
20
20
|
_testsRepoDir = dir;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Record the run's tests repo without ever clearing a known value.
|
|
24
|
+
*
|
|
25
|
+
* The tests repo is a RUN-scoped fact, but analyze_changes is called once per
|
|
26
|
+
* repository and only the PRIMARY call carries `testsRepoDir` — a related
|
|
27
|
+
* repo's call omits it. An unconditional set therefore wiped the primary's
|
|
28
|
+
* value on the second call of every multi-repo run, and by report time the
|
|
29
|
+
* tests repo was unknown: a UI test generated there looked unbacked by any
|
|
30
|
+
* working tree and the agent demoted it out of newTestsCreated (SKYR-4204,
|
|
31
|
+
* run 32536569467). Callers that mean to clear it use setTestsRepoDir.
|
|
32
|
+
*/
|
|
33
|
+
export function rememberTestsRepoDir(dir) {
|
|
34
|
+
if (dir)
|
|
35
|
+
_testsRepoDir = dir;
|
|
36
|
+
}
|
|
22
37
|
export function getTestsRepoDir() {
|
|
23
38
|
return _testsRepoDir;
|
|
24
39
|
}
|
|
@@ -35,10 +50,32 @@ export function getTestsRepoDir() {
|
|
|
35
50
|
* Single-repo / standalone calls (no `repository`) never set or read this.
|
|
36
51
|
*/
|
|
37
52
|
let _activeRunStatePath;
|
|
38
|
-
/** Filename of the run-scoped analysis state file under
|
|
53
|
+
/** Filename of the run-scoped analysis state file under `runArtifactDir()`.
|
|
39
54
|
* Single-sourced so the constructor that WRITES there and `resolveRunStatePath`
|
|
40
55
|
* which LOOKS there cannot drift apart. */
|
|
41
56
|
export const RUN_STATE_FILE_NAME = "analyze-changes-state.json";
|
|
57
|
+
/**
|
|
58
|
+
* Directory holding this run's Skyramp artifacts — the state file, and the report written
|
|
59
|
+
* beside it (SKYR-4147) — or undefined outside a run.
|
|
60
|
+
*
|
|
61
|
+
* `RUNNER_TEMP` is a fresh directory per CI job, and the caller and this server can each
|
|
62
|
+
* work it out from the environment on their own. So neither has to tell the other where
|
|
63
|
+
* the run's files live, and the path never passes through the model.
|
|
64
|
+
*
|
|
65
|
+
* The GitHub Action is the only caller customers run, and GitHub sets `RUNNER_TEMP` for
|
|
66
|
+
* it; the eval harness sets it itself. Nothing customers run is on another CI system, so
|
|
67
|
+
* the GitHub-specific name is only a naming problem — it does not break anything today.
|
|
68
|
+
* If a caller on another CI system is added, rename this then, or let it read that
|
|
69
|
+
* system's variable. Do not add a second variable for a caller that does not exist.
|
|
70
|
+
*
|
|
71
|
+
* Kept in one place for the same reason `RUN_STATE_FILE_NAME` is: the directory used to be
|
|
72
|
+
* written out at three call sites, so only the filename was safe from the three copies
|
|
73
|
+
* drifting apart. testbot keeps its matching copy in one place too, in `skyrampTempDir()`.
|
|
74
|
+
*/
|
|
75
|
+
export function runArtifactDir() {
|
|
76
|
+
const runnerTemp = process.env.RUNNER_TEMP?.trim();
|
|
77
|
+
return runnerTemp ? path.join(runnerTemp, "skyramp") : undefined;
|
|
78
|
+
}
|
|
42
79
|
export function setActiveRunStatePath(stateFilePath) {
|
|
43
80
|
_activeRunStatePath = stateFilePath;
|
|
44
81
|
}
|
|
@@ -56,7 +93,7 @@ export function clearActiveRunStatePath() {
|
|
|
56
93
|
* `setActiveRunStatePath` is only called by skyramp_analyze_changes on multi-repo
|
|
57
94
|
* runs (the ones carrying a `repository` param), but a single-repo Testbot run's
|
|
58
95
|
* path is still deterministic in CI: the StateManager constructor above roots
|
|
59
|
-
* "analysis" state under
|
|
96
|
+
* "analysis" state under `runArtifactDir()`. Falling back to that covers
|
|
60
97
|
* single-repo runs without a new anchor mechanism. Standalone/IDE usage has no
|
|
61
98
|
* RUNNER_TEMP and resolves to undefined — correct, since there is no run state.
|
|
62
99
|
*
|
|
@@ -70,8 +107,9 @@ export function resolveRunStatePath(explicitPath) {
|
|
|
70
107
|
const anchored = getActiveRunStatePath();
|
|
71
108
|
if (anchored)
|
|
72
109
|
return anchored;
|
|
73
|
-
|
|
74
|
-
|
|
110
|
+
const runDir = runArtifactDir();
|
|
111
|
+
if (runDir) {
|
|
112
|
+
return path.join(runDir, RUN_STATE_FILE_NAME);
|
|
75
113
|
}
|
|
76
114
|
return undefined;
|
|
77
115
|
}
|
|
@@ -169,17 +207,15 @@ export class StateManager {
|
|
|
169
207
|
constructor(stateType = "analysis", sessionId, stateDir, stateFilePath) {
|
|
170
208
|
this.stateType = stateType;
|
|
171
209
|
this.sessionId = sessionId || crypto.randomUUID();
|
|
210
|
+
const runDir = stateType === "analysis" ? runArtifactDir() : undefined;
|
|
172
211
|
if (stateFilePath) {
|
|
173
212
|
this.stateFile = stateFilePath;
|
|
174
213
|
}
|
|
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);
|
|
214
|
+
else if (runDir) {
|
|
215
|
+
// In CI, the run directory is fresh per job and the caller and this tool each work
|
|
216
|
+
// it out from the environment — no LLM input needed. It is also where
|
|
217
|
+
// skyramp_submit_report writes the report (SKYR-4147).
|
|
218
|
+
this.stateFile = path.join(runDir, RUN_STATE_FILE_NAME);
|
|
183
219
|
}
|
|
184
220
|
else {
|
|
185
221
|
const baseDir = stateDir || os.tmpdir();
|
|
@@ -435,7 +471,7 @@ export class StateManager {
|
|
|
435
471
|
* @returns Number of files deleted
|
|
436
472
|
*/
|
|
437
473
|
static async cleanupOldFiles(maxAgeHours = 24, stateDir, stateTypes) {
|
|
438
|
-
const baseDir = stateDir || (
|
|
474
|
+
const baseDir = stateDir || runArtifactDir() || os.tmpdir();
|
|
439
475
|
const files = await fs.promises.readdir(baseDir).catch(() => []);
|
|
440
476
|
const statePrefixes = stateTypes
|
|
441
477
|
? stateTypes.map((t) => STATE_FILE_PREFIXES[t])
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report-language enforcement for `skyramp_submit_report` (SKYR-4185).
|
|
3
|
+
*
|
|
4
|
+
* The report language (SKYR-4023) reaches the MCP server only as a testbot
|
|
5
|
+
* prompt/resource arg, and the instruction to write free-text report fields in
|
|
6
|
+
* that language is prose the agent sometimes ignores — the customer-visible
|
|
7
|
+
* failure this module exists to close. The prompt render captures the language
|
|
8
|
+
* here (same server process serves the prompt and later handles the report
|
|
9
|
+
* tool), and the report tool rejects submissions whose free-text fields leaked
|
|
10
|
+
* back into English.
|
|
11
|
+
*
|
|
12
|
+
* Capture is fail-open by design: if the testbot prompt was never served
|
|
13
|
+
* (local/IDE flows, or a run broken enough that the agent has no instructions),
|
|
14
|
+
* the language stays unset and no report is ever falsely rejected.
|
|
15
|
+
*/
|
|
16
|
+
/** Capture the report language at prompt-serve time. Last render wins:
|
|
17
|
+
* `undefined` clears any earlier capture, so a long-lived server (IDE use)
|
|
18
|
+
* that renders a ja prompt and later an English/argless one disarms the
|
|
19
|
+
* guardrail rather than falsely rejecting a legitimately-English report. */
|
|
20
|
+
export declare function setReportLanguage(language: string | undefined): void;
|
|
21
|
+
export declare function getReportLanguage(): string | undefined;
|
|
22
|
+
/** Test isolation only — module state persists across tests in one process. */
|
|
23
|
+
export declare function resetReportLanguage(): void;
|
|
24
|
+
export declare function isEnforcedReportLanguage(language: string | undefined): language is string;
|
|
25
|
+
/** English display name for a language code, falling back to the raw code when
|
|
26
|
+
* Intl rejects it as a structurally invalid tag (e.g. "ja_JP") — the code is
|
|
27
|
+
* ultimately caller-supplied, and a name lookup must never throw. */
|
|
28
|
+
export declare function reportLanguageDisplayName(language: string): string;
|
|
29
|
+
export interface ReportTextField {
|
|
30
|
+
/** Where the text came from, in the tool's input terms (e.g. "testResults[2].details"). */
|
|
31
|
+
path: string;
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
34
|
+
/** Field paths whose text leaked English under an enforced report language.
|
|
35
|
+
* Empty for non-enforced languages — unknown languages are skipped, never guessed. */
|
|
36
|
+
export declare function findLanguageViolations(fields: ReportTextField[], language: string): string[];
|
|
37
|
+
/** Field paths in the deliberately-lenient band: some English prose, but under
|
|
38
|
+
* the rejection threshold (i.e. exactly one stray word at the current
|
|
39
|
+
* MIN_PROSE_WORDS of 2). These SHIP — the guardrail accepts them so a lone
|
|
40
|
+
* technical word ("flaky", "timeout") never blocks a report. Counted into the
|
|
41
|
+
* submit-report analytics event so the residual leak rate is measurable
|
|
42
|
+
* before anyone tightens the threshold. */
|
|
43
|
+
export declare function findLanguageNearMisses(fields: ReportTextField[], language: string): string[];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report-language enforcement for `skyramp_submit_report` (SKYR-4185).
|
|
3
|
+
*
|
|
4
|
+
* The report language (SKYR-4023) reaches the MCP server only as a testbot
|
|
5
|
+
* prompt/resource arg, and the instruction to write free-text report fields in
|
|
6
|
+
* that language is prose the agent sometimes ignores — the customer-visible
|
|
7
|
+
* failure this module exists to close. The prompt render captures the language
|
|
8
|
+
* here (same server process serves the prompt and later handles the report
|
|
9
|
+
* tool), and the report tool rejects submissions whose free-text fields leaked
|
|
10
|
+
* back into English.
|
|
11
|
+
*
|
|
12
|
+
* Capture is fail-open by design: if the testbot prompt was never served
|
|
13
|
+
* (local/IDE flows, or a run broken enough that the agent has no instructions),
|
|
14
|
+
* the language stays unset and no report is ever falsely rejected.
|
|
15
|
+
*/
|
|
16
|
+
let sessionReportLanguage;
|
|
17
|
+
/** Capture the report language at prompt-serve time. Last render wins:
|
|
18
|
+
* `undefined` clears any earlier capture, so a long-lived server (IDE use)
|
|
19
|
+
* that renders a ja prompt and later an English/argless one disarms the
|
|
20
|
+
* guardrail rather than falsely rejecting a legitimately-English report. */
|
|
21
|
+
export function setReportLanguage(language) {
|
|
22
|
+
sessionReportLanguage = language;
|
|
23
|
+
}
|
|
24
|
+
export function getReportLanguage() {
|
|
25
|
+
return sessionReportLanguage;
|
|
26
|
+
}
|
|
27
|
+
/** Test isolation only — module state persists across tests in one process. */
|
|
28
|
+
export function resetReportLanguage() {
|
|
29
|
+
sessionReportLanguage = undefined;
|
|
30
|
+
}
|
|
31
|
+
// Languages the guardrail can enforce: detection is script-presence-based, so a
|
|
32
|
+
// language is enforceable only when its expected script is disjoint from Latin.
|
|
33
|
+
// English ('en') is deliberately absent — it is the default, not enforced.
|
|
34
|
+
// A Map, not a plain object: the key is caller-supplied, and object indexing
|
|
35
|
+
// would resolve prototype keys ("toString") to inherited members whose later
|
|
36
|
+
// .test() call crashes the tool.
|
|
37
|
+
const TARGET_SCRIPTS = new Map([
|
|
38
|
+
// Hiragana, Katakana (incl. halfwidth), CJK ideographs (incl. ext A / compat).
|
|
39
|
+
["ja", /[-ヿ㐀-䶿一-鿿豈-ヲ-゚]/],
|
|
40
|
+
]);
|
|
41
|
+
export function isEnforcedReportLanguage(language) {
|
|
42
|
+
return language !== undefined && TARGET_SCRIPTS.has(language);
|
|
43
|
+
}
|
|
44
|
+
/** English display name for a language code, falling back to the raw code when
|
|
45
|
+
* Intl rejects it as a structurally invalid tag (e.g. "ja_JP") — the code is
|
|
46
|
+
* ultimately caller-supplied, and a name lookup must never throw. */
|
|
47
|
+
export function reportLanguageDisplayName(language) {
|
|
48
|
+
try {
|
|
49
|
+
return new Intl.DisplayNames(["en"], { type: "language" }).of(language) ?? language;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return language;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Tokens that are legitimate untranslated content in any report language — the
|
|
56
|
+
// prompt's own "do NOT translate" list: enum values, HTTP verbs, test types.
|
|
57
|
+
const UNTRANSLATED_WORDS = new Set([
|
|
58
|
+
// Exact enum values only. NOT "passed"/"failed" — those are ordinary English
|
|
59
|
+
// prose and are exactly what leaks in pytest summaries ("1 passed in 0.88s",
|
|
60
|
+
// "failed: count was 13"; see letsramp/api-insight#215).
|
|
61
|
+
"pass", "fail", "skipped", "error", "unknown",
|
|
62
|
+
"critical", "high", "medium", "low",
|
|
63
|
+
"get", "post", "put", "patch", "delete", "head", "options",
|
|
64
|
+
"ui", "e2e", "api", "contract", "integration", "smoke", "fuzz", "load", "mock",
|
|
65
|
+
"http", "https", "json", "yaml", "ok",
|
|
66
|
+
]);
|
|
67
|
+
/** A field must contain at least this many plain English prose words (after the
|
|
68
|
+
* stripping below) to be flagged — so identifier-and-timing-only details like
|
|
69
|
+
* "10.8s, products_contract_test.py" stay legal, and one stray word ("flaky")
|
|
70
|
+
* never rejects a report. Two is deliberate: the shortest real leak observed
|
|
71
|
+
* ("1 passed in 0.88s", api-insight#215) has exactly two prose words. */
|
|
72
|
+
const MIN_PROSE_WORDS = 2;
|
|
73
|
+
/** Count the plain English prose words in `text`, or return 0 when the target
|
|
74
|
+
* language's script is present (the field is compliant regardless of any
|
|
75
|
+
* English around it). Backticked spans, file names/paths, identifiers
|
|
76
|
+
* (snake_case/camelCase/ALL-CAPS), digit-bearing tokens, and the prompt's
|
|
77
|
+
* untranslated vocabulary are all excluded from the count. */
|
|
78
|
+
function countProseWords(text, script) {
|
|
79
|
+
if (script.test(text))
|
|
80
|
+
return 0;
|
|
81
|
+
const withoutCode = text.replace(/`[^`]*`/g, " ");
|
|
82
|
+
let proseWords = 0;
|
|
83
|
+
for (const raw of withoutCode.split(/\s+/)) {
|
|
84
|
+
const token = raw.replace(/^[^A-Za-z0-9]+/, "").replace(/[^A-Za-z0-9]+$/, "");
|
|
85
|
+
// Pure alphabetic words only: anything with digits or interior punctuation
|
|
86
|
+
// (paths, file names, snake_case, versions, "10.8s") is not prose.
|
|
87
|
+
if (token.length < 2 || !/^[A-Za-z]+$/.test(token))
|
|
88
|
+
continue;
|
|
89
|
+
if (/[a-z][A-Z]/.test(token))
|
|
90
|
+
continue; // camelCase identifier
|
|
91
|
+
if (token === token.toUpperCase())
|
|
92
|
+
continue; // acronym / enum shout (GET, FAILED)
|
|
93
|
+
if (UNTRANSLATED_WORDS.has(token.toLowerCase()))
|
|
94
|
+
continue;
|
|
95
|
+
proseWords++;
|
|
96
|
+
}
|
|
97
|
+
return proseWords;
|
|
98
|
+
}
|
|
99
|
+
/** Field paths whose text leaked English under an enforced report language.
|
|
100
|
+
* Empty for non-enforced languages — unknown languages are skipped, never guessed. */
|
|
101
|
+
export function findLanguageViolations(fields, language) {
|
|
102
|
+
const script = TARGET_SCRIPTS.get(language);
|
|
103
|
+
if (!script)
|
|
104
|
+
return [];
|
|
105
|
+
return fields
|
|
106
|
+
.filter((f) => countProseWords(f.text, script) >= MIN_PROSE_WORDS)
|
|
107
|
+
.map((f) => f.path);
|
|
108
|
+
}
|
|
109
|
+
/** Field paths in the deliberately-lenient band: some English prose, but under
|
|
110
|
+
* the rejection threshold (i.e. exactly one stray word at the current
|
|
111
|
+
* MIN_PROSE_WORDS of 2). These SHIP — the guardrail accepts them so a lone
|
|
112
|
+
* technical word ("flaky", "timeout") never blocks a report. Counted into the
|
|
113
|
+
* submit-report analytics event so the residual leak rate is measurable
|
|
114
|
+
* before anyone tightens the threshold. */
|
|
115
|
+
export function findLanguageNearMisses(fields, language) {
|
|
116
|
+
const script = TARGET_SCRIPTS.get(language);
|
|
117
|
+
if (!script)
|
|
118
|
+
return [];
|
|
119
|
+
return fields
|
|
120
|
+
.filter((f) => {
|
|
121
|
+
const words = countProseWords(f.text, script);
|
|
122
|
+
return words > 0 && words < MIN_PROSE_WORDS;
|
|
123
|
+
})
|
|
124
|
+
.map((f) => f.path);
|
|
125
|
+
}
|
|
@@ -14,6 +14,16 @@ import { DriftAction } from "../types/TestAnalysis.js";
|
|
|
14
14
|
* `--untracked-files=all` lists each untracked file individually (otherwise git
|
|
15
15
|
* collapses a brand-new directory to "dir/", which would defeat path matching).
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Union of listChangedFiles over every tree a run legitimately writes to —
|
|
19
|
+
* the primary checkout, the tests-repo checkout (testRepoPath), and related
|
|
20
|
+
* repos (SKYR-4204 reopen: the backing check scanned only the primary, so a
|
|
21
|
+
* UI test generated into the tests-repo checkout was rejected as unbacked and
|
|
22
|
+
* demoted out of the report — run 32510028428). Undefined and duplicate roots
|
|
23
|
+
* are skipped; a root where git fails (missing dir, not a repo) contributes
|
|
24
|
+
* nothing rather than failing the whole check.
|
|
25
|
+
*/
|
|
26
|
+
export declare function listChangedFilesAcross(roots: Array<string | undefined>): Promise<string[]>;
|
|
17
27
|
export declare function listChangedFiles(repoRoot: string): Promise<string[]>;
|
|
18
28
|
export interface UnbackedClaimsInput {
|
|
19
29
|
/** Absolute path of the primary repo checkout the report pertains to. */
|
|
@@ -19,6 +19,37 @@ const execFileAsync = promisify(execFile);
|
|
|
19
19
|
* `--untracked-files=all` lists each untracked file individually (otherwise git
|
|
20
20
|
* collapses a brand-new directory to "dir/", which would defeat path matching).
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Union of listChangedFiles over every tree a run legitimately writes to —
|
|
24
|
+
* the primary checkout, the tests-repo checkout (testRepoPath), and related
|
|
25
|
+
* repos (SKYR-4204 reopen: the backing check scanned only the primary, so a
|
|
26
|
+
* UI test generated into the tests-repo checkout was rejected as unbacked and
|
|
27
|
+
* demoted out of the report — run 32510028428). Undefined and duplicate roots
|
|
28
|
+
* are skipped; a root where git fails (missing dir, not a repo) contributes
|
|
29
|
+
* nothing rather than failing the whole check.
|
|
30
|
+
*/
|
|
31
|
+
export async function listChangedFilesAcross(roots) {
|
|
32
|
+
const seenRoots = new Set();
|
|
33
|
+
const seenFiles = new Set();
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const root of roots) {
|
|
36
|
+
if (!root || seenRoots.has(root))
|
|
37
|
+
continue;
|
|
38
|
+
seenRoots.add(root);
|
|
39
|
+
try {
|
|
40
|
+
for (const f of await listChangedFiles(root)) {
|
|
41
|
+
if (seenFiles.has(f))
|
|
42
|
+
continue;
|
|
43
|
+
seenFiles.add(f);
|
|
44
|
+
files.push(f);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Non-git or missing tree — nothing to contribute.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
22
53
|
export async function listChangedFiles(repoRoot) {
|
|
23
54
|
const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: repoRoot });
|
|
24
55
|
const files = [];
|
|
@@ -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
|
+
}
|
package/build/utils/utils.d.ts
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
import { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
export declare function readDiffFile(diffFilePath: string | undefined): string | undefined;
|
|
3
3
|
export declare function toolError(message: string): CallToolResult;
|
|
4
|
+
/**
|
|
5
|
+
* Build a successful tool result whose payload is delivered on BOTH MCP result
|
|
6
|
+
* channels — `structuredContent` and `content[]` — carrying the identical text.
|
|
7
|
+
* The counterpart to `toolError` for any tool that returns a large text payload.
|
|
8
|
+
*
|
|
9
|
+
* Use this for every non-error return of such a tool, and declare a matching
|
|
10
|
+
* `outputSchema`. Two separate failures make that necessary:
|
|
11
|
+
*
|
|
12
|
+
* 1. A result with no `structuredContent` is persisted by the agent harness as a
|
|
13
|
+
* pretty-printed `.json` spill, which buries the payload inside an escaped
|
|
14
|
+
* JSON string. Coming through `structuredContent` instead lands it as a
|
|
15
|
+
* `.txt`. Measured on eval runs 32227152533 and 32283973875: the extension
|
|
16
|
+
* follows the result shape, with no exceptions either way.
|
|
17
|
+
*
|
|
18
|
+
* The extension is not cosmetic: it changes how the payload TOKENIZES. The
|
|
19
|
+
* reader refuses any spill over 25,000 tokens, and pretty-printed JSON with
|
|
20
|
+
* escaped quotes and newlines costs 2-3x the tokens of the same bytes in raw
|
|
21
|
+
* form. Measured on one payload either way: 141.2KB of `.json` counted 72,318
|
|
22
|
+
* tokens and was refused, while the same result at 142.0KB of `.txt` was read
|
|
23
|
+
* back in full. A second: 54,085 tokens as `.json`, 26,950 as `.txt`.
|
|
24
|
+
*
|
|
25
|
+
* So the shape raises the deliverable size ceiling by roughly 3x, but does not
|
|
26
|
+
* remove it. That second payload still missed the cap by 1,950 tokens, and
|
|
27
|
+
* `skyramp_actions` results of 178-621KB are 45,598-158,997 tokens even as
|
|
28
|
+
* `.txt` — the agent then falls back to slicing the file with `head -c`. The
|
|
29
|
+
* check covers the whole file, so a `limit:` on the read cannot evade it.
|
|
30
|
+
* Payloads that big have to shrink (SKYR-4188 and its sibling); this shape is
|
|
31
|
+
* what lets a shrunk payload actually arrive.
|
|
32
|
+
*
|
|
33
|
+
* 2. Each agent CLI testbot supports reads a different channel and none falls
|
|
34
|
+
* back to the other: Claude Code takes `structuredContent` and drops
|
|
35
|
+
* `content[]`; Cursor takes `content[]` and ignores `structuredContent`;
|
|
36
|
+
* GitHub Copilot CLI surfaces both and de-duplicates them only when the text
|
|
37
|
+
* is the literal JSON serialization of `structuredContent` (MCP spec 5.2.6),
|
|
38
|
+
* otherwise it concatenates. Hence `JSON.stringify` rather than the raw text,
|
|
39
|
+
* which would deliver the payload twice there. Trimming either channel
|
|
40
|
+
* silently starves a client.
|
|
41
|
+
*
|
|
42
|
+
* Declaring the outputSchema also obliges every non-error return to come through
|
|
43
|
+
* here: the SDK rejects a successful result without `structuredContent`
|
|
44
|
+
* ("has an output schema but no structured content was provided"). Error results
|
|
45
|
+
* are exempt — that check returns early on `isError` — so they keep using
|
|
46
|
+
* `toolError`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function dualChannelResult(structuredContent: Record<string, string>): CallToolResult;
|
|
4
49
|
/**
|
|
5
50
|
* Does `candidate` (an LLM- or caller-supplied name/path) identify `fullPath` (a known,
|
|
6
51
|
* absolute test file path)? Exact match first, then a real path-segment boundary match
|