@skyramp/mcp 0.4.2-rc.1 → 0.4.2-rc.3
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/commands/localDevTestChangesCommand.js +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +10 -1
- package/build/commands/testThisEndpointCommand.js +19 -2
- package/build/execution/wrapperConfig.d.ts +56 -0
- package/build/execution/wrapperConfig.js +155 -0
- package/build/index.js +6 -6
- package/build/playwright/registerPlaywrightTools.js +14 -0
- package/build/playwright/traceExportStore.d.ts +22 -0
- package/build/playwright/traceExportStore.js +81 -0
- package/build/playwright/traceRecordingPrompt.js +2 -1
- package/build/prompts/code-reuse.js +24 -21
- package/build/prompts/local-dev/local-dev-plan.js +6 -23
- package/build/prompts/local-dev/local-dev-prompts.js +1 -1
- package/build/prompts/shared-helper-policy.d.ts +36 -0
- package/build/prompts/shared-helper-policy.js +33 -1
- package/build/prompts/startTraceCollectionPrompts.js +1 -1
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +6 -7
- package/build/prompts/sut-setup/shared.d.ts +1 -1
- package/build/prompts/sut-setup/shared.js +5 -3
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +16 -8
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +90 -36
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +1 -1
- package/build/prompts/test-recommendation/recommendationShared.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationShared.js +0 -1
- package/build/prompts/testbot/testbot-prompts.js +11 -9
- package/build/services/TestDiscoveryService.js +32 -4
- package/build/skills/runTestSkill.d.ts +6 -0
- package/build/skills/runTestSkill.js +17 -0
- package/build/tool-phases.js +0 -1
- package/build/tools/budgetExcuse.d.ts +15 -0
- package/build/tools/budgetExcuse.js +113 -0
- package/build/tools/code-refactor/utils-verify-gates.js +17 -3
- package/build/tools/executeSkyrampTestTool.d.ts +97 -48
- package/build/tools/executeSkyrampTestTool.js +775 -449
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +0 -1
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -9
- package/build/tools/generate-tests/generateUIRestTool.d.ts +0 -2
- package/build/tools/generate-tests/generateUIRestTool.js +1 -9
- package/build/tools/submitReportTool.js +128 -0
- package/build/tools/test-management/actionsTool.js +31 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +4 -4
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +0 -11
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -63
- package/build/tools/test-management/testsOwedBeforeRun.d.ts +28 -0
- package/build/tools/test-management/testsOwedBeforeRun.js +53 -0
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +32 -32
- package/build/types/ReuseOutcome.d.ts +4 -3
- package/build/types/TestExecution.d.ts +2 -2
- package/build/types/TestTypes.d.ts +3 -7
- package/build/types/TestTypes.js +6 -20
- package/build/utils/AnalysisStateManager.d.ts +0 -7
- package/build/utils/AnalysisStateManager.js +1 -1
- package/build/utils/connectionErrors.d.ts +10 -0
- package/build/utils/connectionErrors.js +10 -0
- package/build/utils/language-helper.js +24 -3
- package/build/utils/progress.d.ts +1 -1
- package/build/utils/progress.js +1 -1
- package/build/utils/rebaselineSnapshots.d.ts +1 -1
- package/build/utils/rebaselineSnapshots.js +6 -16
- package/build/utils/reuseRouting.d.ts +10 -0
- package/build/utils/reuseRouting.js +15 -0
- package/build/utils/runContextGauge.d.ts +27 -0
- package/build/utils/runContextGauge.js +181 -0
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/skyrampSdkVersion.d.ts +9 -0
- package/build/utils/skyrampSdkVersion.js +16 -0
- package/build/utils/testDependencyPolicy.js +21 -0
- package/build/utils/testExecutionRecord.d.ts +5 -1
- package/build/utils/testExecutionRecord.js +3 -1
- package/build/utils/testFileClassification.d.ts +8 -0
- package/build/utils/testFileClassification.js +36 -3
- package/build/utils/utils-verify/action-key.d.ts +42 -0
- package/build/utils/utils-verify/action-key.js +118 -36
- package/build/utils/utils-verify/action-sites.d.ts +32 -0
- package/build/utils/utils-verify/action-sites.js +202 -0
- package/build/utils/utils-verify/body-reach.js +2 -4
- package/build/utils/utils-verify/call-sites.d.ts +25 -6
- package/build/utils/utils-verify/call-sites.js +8 -5
- package/build/utils/utils-verify/index.d.ts +1 -0
- package/build/utils/utils-verify/index.js +1 -0
- package/build/utils/utils-verify/language-spec.d.ts +25 -0
- package/build/utils/utils-verify/language-spec.js +16 -2
- package/build/utils/utils-verify/parse.d.ts +10 -1
- package/build/utils/utils-verify/parse.js +19 -2
- package/build/utils/utils-verify/verify.d.ts +3 -2
- package/build/utils/utils-verify/verify.js +16 -18
- package/build/workspace/workspace.d.ts +72 -52
- package/build/workspace/workspace.js +12 -8
- package/package.json +1 -1
- package/plugin/prompts/testbot-task1.md +0 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -1
- package/plugin/skills/enhance-assertions/reference/ui.md +1 -1
- package/plugin/skills/fix-test-import-errors/SKILL.md +2 -1
- package/plugin/skills/run-test/SKILL.md +16 -0
- package/build/adapters/jestAdapter.d.ts +0 -14
- package/build/adapters/jestAdapter.js +0 -131
- package/build/adapters/mochaAdapter.d.ts +0 -13
- package/build/adapters/mochaAdapter.js +0 -93
- package/build/adapters/playwrightAdapter.d.ts +0 -17
- package/build/adapters/playwrightAdapter.js +0 -184
- package/build/adapters/pytestAdapter.d.ts +0 -15
- package/build/adapters/pytestAdapter.js +0 -119
- package/build/tools/runExistingTestsTool.d.ts +0 -138
- package/build/tools/runExistingTestsTool.js +0 -666
- package/build/types/ExternalTestExecution.d.ts +0 -67
- package/build/types/ExternalTestExecution.js +0 -8
- package/build/workspace/testSuites.d.ts +0 -20
- package/build/workspace/testSuites.js +0 -17
|
@@ -7,7 +7,6 @@ export declare class E2ETestService extends TestGenerationService {
|
|
|
7
7
|
protected buildGenerationOptions(params: BaseTestParams & {
|
|
8
8
|
responseData?: string;
|
|
9
9
|
playwrightInput?: string;
|
|
10
|
-
testName?: string;
|
|
11
10
|
}): any;
|
|
12
11
|
protected handleApiAnalysis(_params: BaseTestParams): Promise<CallToolResult | null>;
|
|
13
12
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { baseSchema, baseTraceSchema, TestType, codeRefactoringSchema,
|
|
2
|
+
import { baseSchema, baseTraceSchema, TestType, codeRefactoringSchema, } from "../../types/TestTypes.js";
|
|
3
3
|
import { TestGenerationService, } from "../../services/TestGenerationService.js";
|
|
4
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
5
|
import { toolError } from "../../utils/utils.js";
|
|
@@ -16,7 +16,6 @@ const e2eTestSchema = {
|
|
|
16
16
|
"from a Playwright zip that already produced one is rejected — one recording covers one scenario, and reusing " +
|
|
17
17
|
"it manufactures identical tests under names the recording never covered. Set true only when the same " +
|
|
18
18
|
"recording genuinely covers the new scenario end-to-end."),
|
|
19
|
-
testName: testNameField("testE2e"),
|
|
20
19
|
...codeRefactoringSchema.shape,
|
|
21
20
|
...baseSchema.shape,
|
|
22
21
|
};
|
|
@@ -29,11 +28,6 @@ export class E2ETestService extends TestGenerationService {
|
|
|
29
28
|
...super.buildBaseGenerationOptions(params),
|
|
30
29
|
responseData: params.responseData,
|
|
31
30
|
playwrightInput: params.playwrightInput,
|
|
32
|
-
// SKYR-4468: free-form title for the generated test, so the spec names the
|
|
33
|
-
// behaviour it verifies instead of the generic `testE2e`. The option is
|
|
34
|
-
// inert until the pinned @skyramp/skyramp release carries it — the client
|
|
35
|
-
// ignores options it does not know, so passing it early is safe.
|
|
36
|
-
testName: params.testName,
|
|
37
31
|
};
|
|
38
32
|
}
|
|
39
33
|
async handleApiAnalysis(_params) {
|
|
@@ -50,8 +44,6 @@ End-to-End tests validate complete user journeys by testing the entire applicati
|
|
|
50
44
|
TRACE & UI INTEGRATION:
|
|
51
45
|
E2E tests require both trace files (capturing backend API interactions) and Playwright recordings (capturing UI interactions captured using start_trace_collection tool) to generate comprehensive tests that validate the complete user experience.
|
|
52
46
|
|
|
53
|
-
**Test title:** Pass \`testName\` with the behaviour this recording verifies (e.g. "Add to cart from search results"), taken from the recommendation the recording came from. Omit it and the spec is titled \`testE2e\`, which claims nothing.
|
|
54
|
-
|
|
55
47
|
**CRITICAL - When using trace parameter:**
|
|
56
48
|
If \`trace\` parameter is provided (path to a trace file), DO NOT pass \`apiSchema\` or \`endpointURL\` parameters. The trace file already contains all necessary endpoint and schema information. Passing both will cause test generation to fail.`,
|
|
57
49
|
inputSchema: e2eTestSchema,
|
|
@@ -8,7 +8,6 @@ export declare class UITestService extends TestGenerationService {
|
|
|
8
8
|
protected buildGenerationOptions(params: BaseTestParams & {
|
|
9
9
|
playwrightInput?: string;
|
|
10
10
|
uniqueUIFormInputs?: string[];
|
|
11
|
-
testName?: string;
|
|
12
11
|
}): any;
|
|
13
12
|
protected handleApiAnalysis(_params: BaseTestParams): Promise<CallToolResult | null>;
|
|
14
13
|
generateTest(params: BaseTestParams & Record<string, any>): Promise<CallToolResult>;
|
|
@@ -20,7 +19,6 @@ export declare const uiTestSchema: {
|
|
|
20
19
|
include: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
|
|
21
20
|
exclude: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
|
|
22
21
|
uniqueUIFormInputs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
23
|
-
testName: z.ZodOptional<z.ZodString>;
|
|
24
22
|
output: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
|
25
23
|
outputDir: z.ZodString;
|
|
26
24
|
force: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { baseSchema, languageSchema, TestType, codeRefactoringSchema, baseTraceSchema,
|
|
1
|
+
import { baseSchema, languageSchema, TestType, codeRefactoringSchema, baseTraceSchema, } from "../../types/TestTypes.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
4
4
|
import { TestGenerationService, } from "../../services/TestGenerationService.js";
|
|
@@ -21,11 +21,6 @@ export class UITestService extends TestGenerationService {
|
|
|
21
21
|
// SKYR-4012: create-form inputs the agent flagged as unique-constrained,
|
|
22
22
|
// so Skyramp codegen types a per-run-unique value into them.
|
|
23
23
|
uniqueUIFormInputs: params.uniqueUIFormInputs,
|
|
24
|
-
// SKYR-4468: free-form title for the generated test, so the spec names the
|
|
25
|
-
// behaviour it verifies instead of the generic `testUi`. The option is
|
|
26
|
-
// inert until the pinned @skyramp/skyramp release carries it — the client
|
|
27
|
-
// ignores options it does not know, so passing it early is safe.
|
|
28
|
-
testName: params.testName,
|
|
29
24
|
};
|
|
30
25
|
}
|
|
31
26
|
async handleApiAnalysis(_params) {
|
|
@@ -91,7 +86,6 @@ export const uiTestSchema = {
|
|
|
91
86
|
.array(z.string().min(1))
|
|
92
87
|
.optional()
|
|
93
88
|
.describe("Create-form inputs whose value is UNIQUE-constrained (e.g. a name/slug that the backend rejects as a duplicate). Identify each by the accessible label or selector you typed into during recording (the same value passed to getByLabel/getByPlaceholder/getByRole, or a raw selector). Skyramp types a per-run-unique value into these inputs — and reuses it in later assertions — so the generated UI test can be re-run without hitting a duplicate/conflict error. Inspect the app's data model for unique constraints; omit when no create-form field is unique-constrained."),
|
|
94
|
-
testName: testNameField("testUi"),
|
|
95
89
|
output: baseSchema.shape.output,
|
|
96
90
|
outputDir: baseSchema.shape.outputDir,
|
|
97
91
|
force: baseSchema.shape.force,
|
|
@@ -129,8 +123,6 @@ This tells you exactly which frontend files changed so you record traces for the
|
|
|
129
123
|
|
|
130
124
|
**Idempotency (re-runnable create flows):** If the recorded flow creates a resource by typing into a UNIQUE-constrained field (a name/slug the backend rejects as a duplicate on a second run), pass those inputs in \`uniqueUIFormInputs\`. Skyramp then types a per-run-unique value into them (and reuses it in later assertions) so the generated test can be re-run without a duplicate/conflict failure.
|
|
131
125
|
|
|
132
|
-
**Test title:** Pass \`testName\` with the behaviour this recording verifies (e.g. "Add to cart from search results"), taken from the recommendation the recording came from. Omit it and the spec is titled \`testUi\`, which claims nothing.
|
|
133
|
-
|
|
134
126
|
**DOM Analyzer tools for blueprint-aware recording:** alongside the basic \`browser_*\` interaction tools, the Skyramp MCP exposes \`browser_blueprint\` (canonical PageBlueprint capture). This enables semantic target selection and delta-derived assertions: capture a blueprint before each meaningful action, perform the action, then capture again — the delta between the two grounds your assertions in observable state changes rather than author guesses about what "success" looks like.
|
|
135
127
|
|
|
136
128
|
**CRITICAL: Do NOT use skyramp_start_trace_collection/skyramp_stop_trace_collection for UI test recording — use browser_* tools + skyramp_export_zip instead.**`,
|
|
@@ -20,6 +20,9 @@ import { checkDefectsReported, checkIssueTraceability, } from "../recommendation
|
|
|
20
20
|
import { checkExpectedOutcomeAfterExecution, } from "../recommendation/verifiers/expectedOutcome.js";
|
|
21
21
|
import { findInvalidSourceCitations, findUnchangedFileClaims, listChangedFiles, listChangedFilesAcross, listChangedFilesAbs, } from "../utils/reportVerification.js";
|
|
22
22
|
import { isPlanOnlyMode } from "../utils/planOnlyMode.js";
|
|
23
|
+
import { contextGaugeSentence } from "../utils/runContextGauge.js";
|
|
24
|
+
import { budgetExcuseRefusals, citesBudget, countBudgetExcuseRefusal, MAX_BUDGET_EXCUSE_REFUSALS, } from "./budgetExcuse.js";
|
|
25
|
+
import { countUiDeliveryRefusal, MAX_UI_DELIVERY_REFUSALS, traceExportAttempts, uiDeliveryRefusals, } from "../playwright/traceExportStore.js";
|
|
23
26
|
import { getReportLanguage, isEnforcedReportLanguage, findLanguageViolations, findLanguageNearMisses, reportLanguageDisplayName, } from "../utils/reportLanguage.js";
|
|
24
27
|
import { canonicalTestPath, findAssertionRecordByFileName, rederiveAssertionOutcome, } from "./code-refactor/assertion-state.js";
|
|
25
28
|
import { rederiveReuse, deriveReuseFromDelivered, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
|
|
@@ -1124,6 +1127,29 @@ function skippedDeliveredTests(delivered, results) {
|
|
|
1124
1127
|
!matching.some((row) => row.status !== "Skipped"));
|
|
1125
1128
|
});
|
|
1126
1129
|
}
|
|
1130
|
+
/** A planned test the plan typed `ui`. The plan's `testType` is free-form text —
|
|
1131
|
+
* the unapproved-entry check above says so — so it is trimmed and lower-cased
|
|
1132
|
+
* rather than compared against the enum. */
|
|
1133
|
+
function plannedUiTests(plannedTests) {
|
|
1134
|
+
return plannedTests.filter((test) => !!test?.plannedTestId?.trim() &&
|
|
1135
|
+
String(test?.scenario?.testType ?? "")
|
|
1136
|
+
.trim()
|
|
1137
|
+
.toLowerCase() === TestType.UI);
|
|
1138
|
+
}
|
|
1139
|
+
/** Planned UI tests with no entry in the report: the recordings that never
|
|
1140
|
+
* happened, named by the ids the agent would otherwise only have to answer for. */
|
|
1141
|
+
/** Every planned test with no entry in the report, whatever its type. The UI
|
|
1142
|
+
* filter below narrows this; the budget-excuse gate does not, because a dropped
|
|
1143
|
+
* contract test is dropped the same way a dropped recording is. */
|
|
1144
|
+
function undeliveredPlannedTests(plannedTests, delivered) {
|
|
1145
|
+
const shipped = new Set(delivered
|
|
1146
|
+
.map((test) => test.plannedTestId?.trim())
|
|
1147
|
+
.filter((id) => !!id));
|
|
1148
|
+
return plannedTests.filter((test) => !!test?.plannedTestId?.trim() && !shipped.has(test.plannedTestId.trim()));
|
|
1149
|
+
}
|
|
1150
|
+
function undeliveredPlannedUiTests(plannedTests, delivered) {
|
|
1151
|
+
return undeliveredPlannedTests(plannedUiTests(plannedTests), delivered);
|
|
1152
|
+
}
|
|
1127
1153
|
/** Whether the file can be read. The written record outlives the file, and a
|
|
1128
1154
|
* path that exists but cannot be opened owes no run either — the objection
|
|
1129
1155
|
* would name a file nobody can look at. */
|
|
@@ -2166,9 +2192,111 @@ export function registerSubmitReportTool(server) {
|
|
|
2166
2192
|
.map((test) => `${test.plannedTestId?.trim() || test.testId}`)
|
|
2167
2193
|
.join(", ")} ${skippedTests.length === 1 ? "has" : "have"} no non-skipped execution result. ` +
|
|
2168
2194
|
"Do not skip because you believe you are low on context or time, or because you speculate that another attempt will not help. " +
|
|
2195
|
+
contextGaugeSentence() +
|
|
2169
2196
|
"Run each remaining test with skyramp_execute_test, include its Pass or Fail result in testResults, then call skyramp_submit_report again.");
|
|
2170
2197
|
return errorResult;
|
|
2171
2198
|
}
|
|
2199
|
+
// A planned UI test with no report entry is a recording that never happened.
|
|
2200
|
+
// `deliveredMatchesPlan` already raises it and already accepts an answer, and
|
|
2201
|
+
// that is how one run shipped a 14-test plan with ZERO UI tests: a single
|
|
2202
|
+
// skyramp_export_zip, a trace that came out polluted, and an answer blaming
|
|
2203
|
+
// the recorder. Refuse while the run has recorded fewer times than it planned
|
|
2204
|
+
// UI tests; past that it has genuinely tried each one and the answer stands.
|
|
2205
|
+
//
|
|
2206
|
+
// WHY THE RETRY IS NOT FUTILE, and why the message says so: a successful
|
|
2207
|
+
// skyramp_export_zip DRAINS the recorder and closes the browser, so the next
|
|
2208
|
+
// recording starts from an empty buffer. Nothing the agent can read says that,
|
|
2209
|
+
// and that run reasoned the opposite and stopped one call short of a clean
|
|
2210
|
+
// trace.
|
|
2211
|
+
const undeliveredUi = isPlanOnlyMode() || planUnreadable
|
|
2212
|
+
? []
|
|
2213
|
+
: undeliveredPlannedUiTests(stateData.plan?.plannedTests ?? [], dedupedNewTests);
|
|
2214
|
+
const plannedUiCount = planUnreadable
|
|
2215
|
+
? 0
|
|
2216
|
+
: plannedUiTests(stateData.plan?.plannedTests ?? []).length;
|
|
2217
|
+
// ATTEMPTS, so one more recording always answers a refusal and the gate opens
|
|
2218
|
+
// after at most `plannedUiCount` of them. The refusal cap is the second bound,
|
|
2219
|
+
// for the agent that resubmits having recorded nothing: this check is meant to
|
|
2220
|
+
// cost a recording, never the whole run.
|
|
2221
|
+
const recordings = traceExportAttempts();
|
|
2222
|
+
if (undeliveredUi.length > 0 &&
|
|
2223
|
+
recordings < plannedUiCount &&
|
|
2224
|
+
uiDeliveryRefusals() < MAX_UI_DELIVERY_REFUSALS) {
|
|
2225
|
+
const refusal = countUiDeliveryRefusal();
|
|
2226
|
+
const ids = undeliveredUi
|
|
2227
|
+
.map((test) => test.plannedTestId.trim())
|
|
2228
|
+
.join(", ");
|
|
2229
|
+
errorResult = toolError(`Cannot submit the report: ${undeliveredUi.length} registered UI plan test${undeliveredUi.length === 1 ? "" : "s"} ` +
|
|
2230
|
+
`${ids} ${undeliveredUi.length === 1 ? "has" : "have"} no entry in newTestsCreated, and this run recorded ` +
|
|
2231
|
+
`${recordings} trace${recordings === 1 ? "" : "s"} for ${plannedUiCount} planned UI test${plannedUiCount === 1 ? "" : "s"}. ` +
|
|
2232
|
+
"Do not skip because you believe you are low on context or time, or because you speculate that another attempt will not help. " +
|
|
2233
|
+
contextGaugeSentence() +
|
|
2234
|
+
"A successful skyramp_export_zip clears the recording buffer and closes the browser, so the next recording starts clean — " +
|
|
2235
|
+
"a trace that came out polluted is a reason to record again, not a reason to stop. " +
|
|
2236
|
+
"Record each remaining test in its own pass (browser_navigate to the start URL, the flow, skyramp_export_zip, skyramp_ui_test_generation), " +
|
|
2237
|
+
"add it to newTestsCreated with its plannedTestId, then call skyramp_submit_report again. " +
|
|
2238
|
+
`The plan and every answer you have given are in the state file and survive this refusal (refusal ${refusal} of ${MAX_UI_DELIVERY_REFUSALS}).`);
|
|
2239
|
+
return errorResult;
|
|
2240
|
+
}
|
|
2241
|
+
// A planned test dropped because the run says it was running out.
|
|
2242
|
+
//
|
|
2243
|
+
// `deliveredMatchesPlan` raises "planned but not delivered" and suggests
|
|
2244
|
+
// "add the test to the report, or record in the report why it was dropped".
|
|
2245
|
+
// Run 9dfc8405 recorded the reason seven times — "this run exhausted its
|
|
2246
|
+
// working budget" — and shipped 2 of 9 planned tests with the report
|
|
2247
|
+
// ACCEPTED. It wrote that at 46% of a 1M context window with no warning from
|
|
2248
|
+
// anywhere, and the word appears nowhere in its reasoning: the budget was not
|
|
2249
|
+
// a finding, it was a sentence that closed an objection.
|
|
2250
|
+
//
|
|
2251
|
+
// This refuses that ONE sentence, not a budget. A test blocked by something
|
|
2252
|
+
// outside the run answers and ships exactly as before, which is why the
|
|
2253
|
+
// message says what a reason that stands looks like.
|
|
2254
|
+
//
|
|
2255
|
+
// Answers from earlier calls count too: one that closed the objection on
|
|
2256
|
+
// submit N still closes it on N+1 without being re-sent, and a gate reading
|
|
2257
|
+
// only this call would be walked past by not repeating it.
|
|
2258
|
+
const answerText = new Map();
|
|
2259
|
+
const carriedAnswers = Array.isArray(stateData.reportObjections?.answeredObjections)
|
|
2260
|
+
? stateData.reportObjections.answeredObjections
|
|
2261
|
+
: [];
|
|
2262
|
+
for (const entry of carriedAnswers) {
|
|
2263
|
+
const id = entry
|
|
2264
|
+
?.objection?.objectionId;
|
|
2265
|
+
const answer = entry?.answer;
|
|
2266
|
+
if (typeof id === "string" && typeof answer === "string")
|
|
2267
|
+
answerText.set(id, answer);
|
|
2268
|
+
}
|
|
2269
|
+
for (const entry of params.answers ?? []) {
|
|
2270
|
+
if (typeof entry?.objectionId === "string" &&
|
|
2271
|
+
typeof entry?.answer === "string")
|
|
2272
|
+
answerText.set(entry.objectionId, entry.answer);
|
|
2273
|
+
}
|
|
2274
|
+
const droppedForBudget = isPlanOnlyMode() || planUnreadable
|
|
2275
|
+
? []
|
|
2276
|
+
: undeliveredPlannedTests(stateData.plan?.plannedTests ?? [], dedupedNewTests).filter((test) => citesBudget(answerText.get(`deliveredMatchesPlan:${test.plannedTestId.trim()}`)));
|
|
2277
|
+
if (droppedForBudget.length > 0 &&
|
|
2278
|
+
budgetExcuseRefusals() < MAX_BUDGET_EXCUSE_REFUSALS) {
|
|
2279
|
+
const refusal = countBudgetExcuseRefusal();
|
|
2280
|
+
const ids = droppedForBudget
|
|
2281
|
+
.map((test) => test.plannedTestId.trim())
|
|
2282
|
+
.join(", ");
|
|
2283
|
+
const plural = droppedForBudget.length === 1;
|
|
2284
|
+
errorResult = toolError(`Cannot submit the report: ${droppedForBudget.length} planned test${plural ? "" : "s"} ` +
|
|
2285
|
+
`${ids} ${plural ? "is" : "are"} missing from newTestsCreated, and the answer given for ` +
|
|
2286
|
+
`${plural ? "it" : "each"} is that the run was short of budget, context or time. ` +
|
|
2287
|
+
"That is not a reason this report accepts. " +
|
|
2288
|
+
contextGaugeSentence() +
|
|
2289
|
+
"A reason that stands names something outside the run that stopped the test: a service that is not " +
|
|
2290
|
+
"running, a branch that no longer exists, a credential this run does not hold, a behaviour the " +
|
|
2291
|
+
"application will not reach. Running low is not one of those — it is a reason to write the test now, " +
|
|
2292
|
+
"with the rest of the report already built. " +
|
|
2293
|
+
`Write the missing test${plural ? "" : "s"}, add ${plural ? "it" : "them"} to newTestsCreated with the ` +
|
|
2294
|
+
"plannedTestId, and call skyramp_submit_report again — or replace the answer with what actually " +
|
|
2295
|
+
"stopped it. " +
|
|
2296
|
+
`The plan, the report and every answer you have given are in the state file and survive this refusal ` +
|
|
2297
|
+
`(refusal ${refusal} of ${MAX_BUDGET_EXCUSE_REFUSALS}).`);
|
|
2298
|
+
return errorResult;
|
|
2299
|
+
}
|
|
2172
2300
|
// Report-time checks on what the customer will read. They need no plan, so a
|
|
2173
2301
|
// run without one still gets them.
|
|
2174
2302
|
const reportChecks = [
|
|
@@ -13,6 +13,9 @@ import { REBASELINE_SNAPSHOT_NAME_RE, looksLikeOnDiskBaselineName, } from "../..
|
|
|
13
13
|
import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRebaselineStrategy, buildRegenerateStrategy, buildDeleteStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
|
|
14
14
|
import { buildModularizeFirstNextSteps, isMaintenanceRegenerable, isModularizeFirstHandOffTarget, regenerateCodeReuse, } from "../../prompts/reuse-hand-off.js";
|
|
15
15
|
import { recordReuseHandOff } from "../code-refactor/reuse-state.js";
|
|
16
|
+
import { isPlanOnlyMode } from "../../utils/planOnlyMode.js";
|
|
17
|
+
import { runTestCall } from "../../skills/runTestSkill.js";
|
|
18
|
+
import { formatOwedFiles, testsOwedBeforeRun } from "./testsOwedBeforeRun.js";
|
|
16
19
|
/**
|
|
17
20
|
* Compute a suggested new filename when an endpoint is renamed.
|
|
18
21
|
*/
|
|
@@ -442,6 +445,34 @@ export function registerActionsTool(server) {
|
|
|
442
445
|
if (renamed)
|
|
443
446
|
r._suggestedNewFile = renamed;
|
|
444
447
|
}
|
|
448
|
+
// ── Before-run gate on the tests this call will edit ──────────────
|
|
449
|
+
// Drift on the repository's own test must rest on a run, not on a
|
|
450
|
+
// reading of its source: without the pre-edit result the report cannot
|
|
451
|
+
// say what the edit changed, and the test lands at afterStatus=Unknown.
|
|
452
|
+
// The subject is what this call edits, and nothing wider: the gate used
|
|
453
|
+
// to sit in skyramp_analyze_test_health and demand a run for every
|
|
454
|
+
// external test in state, which is the first hundred files discovery
|
|
455
|
+
// found in glob order.
|
|
456
|
+
//
|
|
457
|
+
// A DELETE or a REGENERATE on an external test is report-only (the run
|
|
458
|
+
// edits nothing), and a related repo's entry is already a VERIFY by
|
|
459
|
+
// here, so neither owes a run.
|
|
460
|
+
const { owed, exemptLanguage } = testsOwedBeforeRun(recommendations, catalogByFile);
|
|
461
|
+
if (exemptLanguage > 0) {
|
|
462
|
+
logger.info(`Before-run gate: ${exemptLanguage} recommended external test(s) owe no run — skyramp_execute_test cannot run their language.`);
|
|
463
|
+
}
|
|
464
|
+
// A plan-only run has no application to run the tests against.
|
|
465
|
+
if (owed.length > 0 && !isPlanOnlyMode()) {
|
|
466
|
+
const filesList = formatOwedFiles(owed);
|
|
467
|
+
errorResult = toolError(`${owed.length} test${owed.length === 1 ? "" : "s"} you recommend editing ${owed.length === 1 ? "has" : "have"} no phase: "before" result recorded — never run, or a run was started and recorded no result: ${filesList}. ` +
|
|
468
|
+
`Run each file through skyramp_execute_test with phase: "before", stateFile: "${args.stateFile}"${args.repository ? `, repository: "${args.repository}"` : ""}: ${runTestCall()}. Then call ${TOOL_NAME} again. Nothing was recorded by this call.`);
|
|
469
|
+
return errorResult;
|
|
470
|
+
}
|
|
471
|
+
else if (owed.length > 0) {
|
|
472
|
+
// planOnlyMode is process-global and captured at prompt-render time, so
|
|
473
|
+
// a skip must be visible: no run means no evidence behind these edits.
|
|
474
|
+
logger.warning(`Before-run gate skipped in a plan-only run: ${owed.length} test(s) you recommend editing have no phase: "before" result recorded.`);
|
|
475
|
+
}
|
|
445
476
|
const recommendedPaths = new Set(recommendations.map((r) => r.testFilePath));
|
|
446
477
|
const ignoredTestFiles = testAnalysisResults
|
|
447
478
|
.map((t) => t.testFile)
|
|
@@ -131,8 +131,8 @@ export declare const analyzeChangesOutputSchema: {
|
|
|
131
131
|
analysedWorkspaceFile: z.ZodString;
|
|
132
132
|
}, "strip", z.ZodTypeAny, {
|
|
133
133
|
message: string;
|
|
134
|
-
kind: "workspace-disagreement";
|
|
135
134
|
serviceName: string;
|
|
135
|
+
kind: "workspace-disagreement";
|
|
136
136
|
field: "authHeader" | "authScheme" | "authType";
|
|
137
137
|
primaryWorkspaceFile: string;
|
|
138
138
|
analysedWorkspaceFile: string;
|
|
@@ -140,8 +140,8 @@ export declare const analyzeChangesOutputSchema: {
|
|
|
140
140
|
analysed: string;
|
|
141
141
|
}, {
|
|
142
142
|
message: string;
|
|
143
|
-
kind: "workspace-disagreement";
|
|
144
143
|
serviceName: string;
|
|
144
|
+
kind: "workspace-disagreement";
|
|
145
145
|
field: "authHeader" | "authScheme" | "authType";
|
|
146
146
|
primaryWorkspaceFile: string;
|
|
147
147
|
analysedWorkspaceFile: string;
|
|
@@ -156,8 +156,8 @@ export declare const analyzeChangesOutputSchema: {
|
|
|
156
156
|
openApiSpecPath?: string | undefined;
|
|
157
157
|
authFindings?: {
|
|
158
158
|
message: string;
|
|
159
|
-
kind: "workspace-disagreement";
|
|
160
159
|
serviceName: string;
|
|
160
|
+
kind: "workspace-disagreement";
|
|
161
161
|
field: "authHeader" | "authScheme" | "authType";
|
|
162
162
|
primaryWorkspaceFile: string;
|
|
163
163
|
analysedWorkspaceFile: string;
|
|
@@ -172,8 +172,8 @@ export declare const analyzeChangesOutputSchema: {
|
|
|
172
172
|
openApiSpecPath?: string | undefined;
|
|
173
173
|
authFindings?: {
|
|
174
174
|
message: string;
|
|
175
|
-
kind: "workspace-disagreement";
|
|
176
175
|
serviceName: string;
|
|
176
|
+
kind: "workspace-disagreement";
|
|
177
177
|
field: "authHeader" | "authScheme" | "authType";
|
|
178
178
|
primaryWorkspaceFile: string;
|
|
179
179
|
analysedWorkspaceFile: string;
|
|
@@ -1,13 +1,2 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
/**
|
|
3
|
-
* Names of services whose workspace.yml records a runnable external-test suite
|
|
4
|
-
* (a reader-backed framework — playwright/pytest/jest/vitest/mocha — +
|
|
5
|
-
* a `runtimeDetails.testSuites` entry with a testRunCommand) — i.e. what skyramp_run_existing_tests can
|
|
6
|
-
* actually run. Used to scope the confirm gate so it only fires on repos that
|
|
7
|
-
* went through Test Environment Setup. Checks each service's suites via
|
|
8
|
-
* `resolveSuites` (rather than calling resolveRunConfig) so every
|
|
9
|
-
* `runtimeDetails.testSuites` entry is recognized. Returns [] on any read/parse
|
|
10
|
-
* error so a config problem can never block drift analysis.
|
|
11
|
-
*/
|
|
12
|
-
export declare function runnableTestEnvServices(repositoryPath: string): Promise<string[]>;
|
|
13
2
|
export declare function registerAnalyzeTestHealthTool(server: McpServer): void;
|
|
@@ -5,9 +5,6 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
|
5
5
|
import { buildDriftAnalysisPrompt } from "../../prompts/test-maintenance/drift-analysis-prompt.js";
|
|
6
6
|
import { TestSource } from "../../types/TestAnalysis.js";
|
|
7
7
|
import { toolError } from "../../utils/utils.js";
|
|
8
|
-
import { WorkspaceConfigManager } from "../../workspace/workspace.js";
|
|
9
|
-
import { resolveSuites } from "../../workspace/testSuites.js";
|
|
10
|
-
import { frameworkHasReader } from "../runExistingTestsTool.js";
|
|
11
8
|
const TOOL_NAME = "skyramp_analyze_test_health";
|
|
12
9
|
// UI test frameworks. "playwright-ui" and "cypress-ui" are the browser-interaction
|
|
13
10
|
// variants detected when the file uses the page fixture (not just the request fixture).
|
|
@@ -38,27 +35,6 @@ function isUiTest(t) {
|
|
|
38
35
|
UI_TEST_EXTENSIONS.test(t.testFile) ||
|
|
39
36
|
isUiSnap);
|
|
40
37
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Names of services whose workspace.yml records a runnable external-test suite
|
|
43
|
-
* (a reader-backed framework — playwright/pytest/jest/vitest/mocha — +
|
|
44
|
-
* a `runtimeDetails.testSuites` entry with a testRunCommand) — i.e. what skyramp_run_existing_tests can
|
|
45
|
-
* actually run. Used to scope the confirm gate so it only fires on repos that
|
|
46
|
-
* went through Test Environment Setup. Checks each service's suites via
|
|
47
|
-
* `resolveSuites` (rather than calling resolveRunConfig) so every
|
|
48
|
-
* `runtimeDetails.testSuites` entry is recognized. Returns [] on any read/parse
|
|
49
|
-
* error so a config problem can never block drift analysis.
|
|
50
|
-
*/
|
|
51
|
-
export async function runnableTestEnvServices(repositoryPath) {
|
|
52
|
-
try {
|
|
53
|
-
const config = await new WorkspaceConfigManager(repositoryPath).read();
|
|
54
|
-
return (config.services ?? [])
|
|
55
|
-
.filter((s) => resolveSuites(s).some((suite) => frameworkHasReader(suite.framework) && !!suite.testRunCommand))
|
|
56
|
-
.map((s) => s.serviceName);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
38
|
export function registerAnalyzeTestHealthTool(server) {
|
|
63
39
|
server.registerTool(TOOL_NAME, {
|
|
64
40
|
annotations: {
|
|
@@ -111,44 +87,12 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
111
87
|
const skyrampCount = existingTests.filter((t) => t.source !== TestSource.External).length;
|
|
112
88
|
const externalCount = existingTests.length - skyrampCount;
|
|
113
89
|
logger.info(`Loaded ${skyrampCount} Skyramp + ${externalCount} relevant external tests from state file`);
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
// and still skipped the confirm, leaving the repo's own tests at
|
|
121
|
-
// afterStatus=Unknown — so enforce ordering here: refuse to produce the
|
|
122
|
-
// drift prompt until a confirm run has been recorded. Loop-safe:
|
|
123
|
-
// skyramp_run_existing_tests persists an externalTestResults record for
|
|
124
|
-
// EVERY outcome (pass/fail/skip/unhealthy), so one confirm call clears the
|
|
125
|
-
// gate. Only a confirm-mode record counts — a verify run (or any future
|
|
126
|
-
// record type) must not bypass the required confirm pass.
|
|
127
|
-
// Scoped by runnableTestEnvServices so it never fires on repos without
|
|
128
|
-
// test-env setup, and fail-open on any config error.
|
|
129
|
-
const confirmAttempted = (stateData.externalTestResults ?? []).some((r) => r.mode === "confirm");
|
|
130
|
-
const runnableServices = externalCount > 0 && !confirmAttempted
|
|
131
|
-
? await runnableTestEnvServices(repositoryPath)
|
|
132
|
-
: [];
|
|
133
|
-
if (runnableServices.length > 0) {
|
|
134
|
-
const externalFiles = existingTests
|
|
135
|
-
.filter((t) => t.source === TestSource.External)
|
|
136
|
-
.map((t) => t.testFile);
|
|
137
|
-
// Cap the enumerated examples — a broad diff can mark dozens of external
|
|
138
|
-
// tests, and the agent already has the full [external] list from
|
|
139
|
-
// skyramp_analyze_changes.
|
|
140
|
-
const shown = externalFiles.slice(0, 8);
|
|
141
|
-
const filesList = shown.map((f) => `"${f}"`).join(", ") +
|
|
142
|
-
(externalFiles.length > shown.length
|
|
143
|
-
? `, …(+${externalFiles.length - shown.length} more)`
|
|
144
|
-
: "");
|
|
145
|
-
// With >1 runnable service the tool needs an explicit `service`.
|
|
146
|
-
const serviceHint = runnableServices.length > 1
|
|
147
|
-
? ` Multiple runnable test services are configured — pass service: "${runnableServices[0]}".`
|
|
148
|
-
: "";
|
|
149
|
-
return toolError(`This repository has a configured test environment (runtimeDetails.test*) and ${externalCount} external test${externalCount === 1 ? "" : "s"} relevant to this diff, but skyramp_run_existing_tests has not run yet. Run the repo's OWN tests BEFORE assessing drift so the assessment is grounded in real pass/fail (otherwise these tests are left at status Unknown). ` +
|
|
150
|
-
`Call skyramp_run_existing_tests with mode: "confirm", stateFile: "${stateManager.getStatePath()}"${args.repository ? `, repository: "${args.repository}"` : ""}, and testSelectors set to the [external] test files skyramp_analyze_changes marked (e.g. ${filesList}).${serviceHint} Then call skyramp_analyze_test_health again.`);
|
|
151
|
-
}
|
|
90
|
+
// The repository's own tests, for the run results the drift prompt
|
|
91
|
+
// renders. A file with no `phase: "before"` run reaches the prompt as
|
|
92
|
+
// not run: the gate that demanded a run for every one of them moved to
|
|
93
|
+
// skyramp_actions, where the recommendations say which tests this run
|
|
94
|
+
// will edit.
|
|
95
|
+
const externalTests = existingTests.filter((t) => t.source === TestSource.External);
|
|
152
96
|
// Delete stale diff files only — state files must remain for the caller's skyramp_actions call.
|
|
153
97
|
try {
|
|
154
98
|
await StateManager.cleanupOldFiles(24, undefined, []);
|
|
@@ -186,7 +130,7 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
186
130
|
const allRepoPaths = [
|
|
187
131
|
...new Set((await stateManager.listRepoCheckouts()).map((c) => c.root)),
|
|
188
132
|
];
|
|
189
|
-
const promptText = buildDriftAnalysisPrompt(stateManager.getStatePath(), apiTests.map((t) => ({ testFile: t.testFile, source: t.source })), uiDriftParams, allRepoPaths,
|
|
133
|
+
const promptText = buildDriftAnalysisPrompt(stateManager.getStatePath(), apiTests.map((t) => ({ testFile: t.testFile, source: t.source })), uiDriftParams, allRepoPaths, externalTests.flatMap((t) => t.executionBefore ? [t.executionBefore] : []));
|
|
190
134
|
return {
|
|
191
135
|
structuredContent: { prompt: promptText },
|
|
192
136
|
content: [
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type DriftRecommendation, type TestAnalysisResult } from "../../types/TestAnalysis.js";
|
|
2
|
+
/** One catalogued test, narrowed to what the gate reads. */
|
|
3
|
+
export type GateCatalogRow = Pick<TestAnalysisResult, "testFile" | "language" | "source" | "executionBefore">;
|
|
4
|
+
/** One recommendation, narrowed to what the gate reads. */
|
|
5
|
+
export type GateRecommendation = Pick<DriftRecommendation, "testFilePath" | "action" | "reportOnly">;
|
|
6
|
+
export interface TestsOwedBeforeRun {
|
|
7
|
+
/** Files this call would edit that have no usable pre-edit run. */
|
|
8
|
+
owed: string[];
|
|
9
|
+
/** Owed no run because skyramp_execute_test cannot run that language. */
|
|
10
|
+
exemptLanguage: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The tests this call edits that have no `phase: "before"` result.
|
|
14
|
+
*
|
|
15
|
+
* Only UPDATE reaches here. An external REGENERATE or DELETE is turned into a
|
|
16
|
+
* report-only recommendation before the gate runs, and a report-only row edits
|
|
17
|
+
* nothing, so neither owes a run.
|
|
18
|
+
*
|
|
19
|
+
* A recommendation with no catalog row is deliberately NOT gated. `skyramp_actions`
|
|
20
|
+
* accepts a real test file discovery missed and records a verdict for it rather than
|
|
21
|
+
* dropping it (SKYR-3906/SKYR-3938, the B1 test). Refusing it here for want of a
|
|
22
|
+
* before-run would reverse that: it has no catalog row, so it never has a recorded
|
|
23
|
+
* run, and the gate would refuse it on every call. Gating it is a change to B1's
|
|
24
|
+
* contract, not to this one.
|
|
25
|
+
*/
|
|
26
|
+
export declare function testsOwedBeforeRun(recommendations: readonly GateRecommendation[], catalogByFile: ReadonlyMap<string, GateCatalogRow>): TestsOwedBeforeRun;
|
|
27
|
+
/** Every file, quoted, one list — the agent must run all of them. */
|
|
28
|
+
export declare function formatOwedFiles(owed: readonly string[]): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DriftAction, TestSource, } from "../../types/TestAnalysis.js";
|
|
2
|
+
import { ProgrammingLanguage } from "../../types/TestTypes.js";
|
|
3
|
+
import { TestExecutionStatus } from "../../types/TestExecution.js";
|
|
4
|
+
/**
|
|
5
|
+
* The tests this call edits that have no `phase: "before"` result.
|
|
6
|
+
*
|
|
7
|
+
* Only UPDATE reaches here. An external REGENERATE or DELETE is turned into a
|
|
8
|
+
* report-only recommendation before the gate runs, and a report-only row edits
|
|
9
|
+
* nothing, so neither owes a run.
|
|
10
|
+
*
|
|
11
|
+
* A recommendation with no catalog row is deliberately NOT gated. `skyramp_actions`
|
|
12
|
+
* accepts a real test file discovery missed and records a verdict for it rather than
|
|
13
|
+
* dropping it (SKYR-3906/SKYR-3938, the B1 test). Refusing it here for want of a
|
|
14
|
+
* before-run would reverse that: it has no catalog row, so it never has a recorded
|
|
15
|
+
* run, and the gate would refuse it on every call. Gating it is a change to B1's
|
|
16
|
+
* contract, not to this one.
|
|
17
|
+
*/
|
|
18
|
+
export function testsOwedBeforeRun(recommendations, catalogByFile) {
|
|
19
|
+
const runnableLanguages = Object.values(ProgrammingLanguage);
|
|
20
|
+
let exemptLanguage = 0;
|
|
21
|
+
const owed = recommendations
|
|
22
|
+
.filter((rec) => {
|
|
23
|
+
if (rec.reportOnly)
|
|
24
|
+
return false;
|
|
25
|
+
if (rec.action !== DriftAction.Update)
|
|
26
|
+
return false;
|
|
27
|
+
const row = catalogByFile.get(rec.testFilePath);
|
|
28
|
+
if (!row || row.source !== TestSource.External)
|
|
29
|
+
return false;
|
|
30
|
+
// Unknown is the placeholder skyramp_execute_test reserves before it
|
|
31
|
+
// spawns the runner. A run that died there left no evidence, so the
|
|
32
|
+
// placeholder must not read as a recorded baseline.
|
|
33
|
+
if (row.executionBefore &&
|
|
34
|
+
row.executionBefore.status !== TestExecutionStatus.Unknown)
|
|
35
|
+
return false;
|
|
36
|
+
// A language skyramp_execute_test does not accept can never record a
|
|
37
|
+
// before-run. No name check follows it: discovery admits an external file
|
|
38
|
+
// only when its name identifies a test or it is a colocated snapshot, and
|
|
39
|
+
// a .snap is exempt by language here, so a second rule would be a copy
|
|
40
|
+
// that can go out of sync.
|
|
41
|
+
if (!runnableLanguages.includes(row.language)) {
|
|
42
|
+
exemptLanguage++;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
})
|
|
47
|
+
.map((rec) => rec.testFilePath);
|
|
48
|
+
return { owed, exemptLanguage };
|
|
49
|
+
}
|
|
50
|
+
/** Every file, quoted, one list — the agent must run all of them. */
|
|
51
|
+
export function formatOwedFiles(owed) {
|
|
52
|
+
return owed.map((f) => `"${f}"`).join(", ");
|
|
53
|
+
}
|
|
@@ -142,7 +142,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
|
|
|
142
142
|
const sessionAppendix = savedSession
|
|
143
143
|
? `\n\nPlaywright session storage saved to: ${savedSession}\nRe-use it by:\n` +
|
|
144
144
|
`• Pass \`playwrightStoragePath: "${savedSession}"\` to skyramp_start_trace_collection for future recordings (skips login).\n` +
|
|
145
|
-
`• Generated tests that reference \`storageState: "${savedSession}"\`
|
|
145
|
+
`• Generated tests that reference \`storageState: "${savedSession}"\` read it from that path when run via skyramp_execute_test.`
|
|
146
146
|
: "";
|
|
147
147
|
errorResult = {
|
|
148
148
|
content: [
|