@skyramp/mcp 0.3.0-rc.1 → 0.3.0
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/prompts/test-maintenance/actionsInstructions.js +4 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +9 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +8 -7
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -5
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +11 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +22 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +23 -31
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +7 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +20 -6
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +10 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +27 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.js +35 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +5 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +61 -0
- package/build/prompts/testbot/testbot-prompts.js +7 -5
- package/build/prompts/testbot/testbot-prompts.test.js +1 -1
- package/build/services/AnalyticsService.js +37 -2
- package/build/services/TestDiscoveryService.d.ts +38 -4
- package/build/services/TestDiscoveryService.js +187 -16
- package/build/services/TestDiscoveryService.test.js +232 -10
- package/build/services/TestExecutionService.d.ts +12 -1
- package/build/services/TestExecutionService.js +20 -41
- package/build/services/TestGenerationService.d.ts +8 -0
- package/build/services/TestGenerationService.js +62 -17
- package/build/services/TestGenerationService.test.js +67 -1
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +1 -1
- package/build/tools/executeSkyrampTestTool.js +30 -1
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/one-click/oneClickTool.js +1 -1
- package/build/tools/test-management/actionsTool.js +76 -41
- package/build/tools/test-management/actionsTool.test.js +104 -38
- package/build/tools/test-management/analyzeChangesTool.js +102 -4
- package/build/tools/test-management/analyzeChangesTool.test.js +3 -1
- package/build/tools/test-management/analyzeTestHealthTool.js +9 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +1 -1
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +6 -6
- package/build/types/TestAnalysis.d.ts +13 -0
- package/build/utils/dartRouteExtractor.js +8 -30
- package/build/utils/docker.test.js +1 -1
- package/build/utils/fileWalk.d.ts +71 -0
- package/build/utils/fileWalk.js +79 -0
- package/build/utils/fileWalk.test.d.ts +1 -0
- package/build/utils/fileWalk.test.js +252 -0
- package/build/utils/frontendIntegration.js +20 -36
- package/build/utils/frontendSelectors.d.ts +34 -0
- package/build/utils/frontendSelectors.js +124 -0
- package/build/utils/frontendSelectors.test.d.ts +1 -0
- package/build/utils/frontendSelectors.test.js +118 -0
- package/build/utils/importerHop.d.ts +18 -0
- package/build/utils/importerHop.js +48 -3
- package/build/utils/importerHop.test.js +71 -1
- package/build/utils/pythonMountPrefixes.d.ts +25 -0
- package/build/utils/pythonMountPrefixes.js +347 -0
- package/build/utils/pythonMountPrefixes.test.d.ts +1 -0
- package/build/utils/pythonMountPrefixes.test.js +113 -0
- package/build/utils/repoScanner.js +30 -23
- package/build/utils/repoScanner.test.js +98 -0
- package/build/utils/routeParsers.d.ts +7 -1
- package/build/utils/routeParsers.js +32 -18
- package/build/utils/sourceRouteExtractor.js +17 -40
- package/build/utils/trace-parser.js +7 -19
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/walkerCharacterization.test.d.ts +1 -0
- package/build/utils/walkerCharacterization.test.js +233 -0
- package/node_modules/playwright/lib/common/config.js +1 -1
- package/node_modules/playwright/lib/common/configLoader.js +1 -1
- package/node_modules/playwright/lib/common/fixtures.js +1 -1
- package/node_modules/playwright/lib/common/testType.js +1 -1
- package/node_modules/playwright/lib/index.js +2 -2
- package/node_modules/playwright/lib/isomorphic/testTree.js +1 -1
- package/node_modules/playwright/lib/matchers/expect.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/tracing.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +7 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +3 -1
- package/node_modules/playwright/lib/reporters/base.js +2 -2
- package/node_modules/playwright/lib/reporters/list.js +1 -1
- package/node_modules/playwright/lib/reporters/teleEmitter.js +1 -1
- package/node_modules/playwright/lib/worker/testInfo.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Cqp3cwEJ.js → codeMirrorModule-aszq5EdG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-CYf9adZh.js → defaultSettingsView-BxS7Jm4s.js} +94 -94
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.d4gkVSou.js → index.D4JTTy4R.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.u4_8VnCV.js → uiMode.DaRMQKOI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/._cli.js +0 -0
- package/node_modules/playwright/._index.d.ts +0 -0
- package/node_modules/playwright/._index.js +0 -0
- package/node_modules/playwright/._index.mjs +0 -0
- package/node_modules/playwright/._jsx-runtime.js +0 -0
- package/node_modules/playwright/._jsx-runtime.mjs +0 -0
- package/node_modules/playwright/._lib +0 -0
- package/node_modules/playwright/._node_modules +0 -0
- package/node_modules/playwright/._package.json +0 -0
- package/node_modules/playwright/._test.d.ts +0 -0
- package/node_modules/playwright/._test.js +0 -0
- package/node_modules/playwright/._test.mjs +0 -0
- package/node_modules/playwright/._types +0 -0
- package/node_modules/playwright/._vitest.config.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.npmignore +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._README.md +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bin +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._browsers.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bundles +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._cli.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.d.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.mjs +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._lib +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._package.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._src +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._types +0 -0
|
@@ -50,7 +50,7 @@ Logout from Skyramp platform to end your authenticated session.`,
|
|
|
50
50
|
const recordParams = {
|
|
51
51
|
prompt: params.prompt,
|
|
52
52
|
};
|
|
53
|
-
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
|
|
53
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
}
|
|
@@ -102,7 +102,7 @@ export function registerCodeReuseTool(server) {
|
|
|
102
102
|
testFile: params.testFile,
|
|
103
103
|
language: params.language,
|
|
104
104
|
framework: params.framework,
|
|
105
|
-
});
|
|
105
|
+
}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
108
|
}
|
|
@@ -120,7 +120,7 @@ After modularization, if errors remain, call skyramp_fix_errors.
|
|
|
120
120
|
testFile: params.testFile,
|
|
121
121
|
language: params.language ?? "",
|
|
122
122
|
testType: params.testType ?? "",
|
|
123
|
-
});
|
|
123
|
+
}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
124
124
|
}
|
|
125
125
|
});
|
|
126
126
|
}
|
|
@@ -8,6 +8,7 @@ import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
|
8
8
|
import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
|
|
9
9
|
import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
|
|
10
10
|
import { StateManager } from "../utils/AnalysisStateManager.js";
|
|
11
|
+
import { TestSource } from "../types/TestAnalysis.js";
|
|
11
12
|
import { logger } from "../utils/logger.js";
|
|
12
13
|
import { toolError } from "../utils/utils.js";
|
|
13
14
|
const TOOL_NAME = "skyramp_execute_test";
|
|
@@ -77,6 +78,34 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
77
78
|
errorResult = toolError(`testFile must be an absolute path, got: ${params.testFile}`);
|
|
78
79
|
return errorResult;
|
|
79
80
|
}
|
|
81
|
+
// Deterministic external-test guard (SKYR-3924): this tool runs Skyramp-generated
|
|
82
|
+
// tests in the executor and cannot run a repo's native (user-written) suite, so a
|
|
83
|
+
// run on an external test only errors (e.g. pytest import/collection failure).
|
|
84
|
+
// If the stateFile records this test as external, skip execution — the prompt asks
|
|
85
|
+
// the agent to exclude them, and this enforces it. Best-effort and conservative:
|
|
86
|
+
// only skip on a positive external match; any state-read issue falls through to
|
|
87
|
+
// normal execution, and a not-found test (e.g. a newly generated one) is not skipped.
|
|
88
|
+
if (params.stateFile) {
|
|
89
|
+
try {
|
|
90
|
+
const stateManager = StateManager.fromStatePath(params.stateFile);
|
|
91
|
+
const stateData = await stateManager.readRepoData(params.repository);
|
|
92
|
+
const entry = stateData?.existingTests?.find((t) => t.testFile === params.testFile);
|
|
93
|
+
if (entry?.source === TestSource.External) {
|
|
94
|
+
logger.info(`Skipping execution of external test ${params.testFile} — native suites are not run by ${TOOL_NAME}`);
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: `Skipped execution: ${params.testFile} is marked external in the state file. ${TOOL_NAME} runs Skyramp-generated tests only; external (native) suites are not executed here.`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
logger.warning(`External-test guard could not read stateFile (${err.message}); proceeding with execution`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
80
109
|
// Send initial progress
|
|
81
110
|
await sendProgress(0, 100, "Starting test execution...");
|
|
82
111
|
// Always resolve workspace config for dockerNetwork (host networking)
|
|
@@ -187,7 +216,7 @@ If this failed with a 401 status code, ask the user for the authentication token
|
|
|
187
216
|
workspacePath: params.workspacePath,
|
|
188
217
|
language: params.language,
|
|
189
218
|
testType: params.testType,
|
|
190
|
-
});
|
|
219
|
+
}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
191
220
|
}
|
|
192
221
|
});
|
|
193
222
|
}
|
|
@@ -64,7 +64,7 @@ export function registerFixErrorTool(server) {
|
|
|
64
64
|
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
65
65
|
prompt: params.prompt,
|
|
66
66
|
testFile: params.testFile,
|
|
67
|
-
});
|
|
67
|
+
}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
}
|
|
@@ -184,7 +184,7 @@ export function registerOneClickTool(server) {
|
|
|
184
184
|
finally {
|
|
185
185
|
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
186
186
|
workflow: params.workflow,
|
|
187
|
-
});
|
|
187
|
+
}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
}
|
|
@@ -6,7 +6,8 @@ import { TestType } from "../../types/TestTypes.js";
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
9
|
-
import { toolError
|
|
9
|
+
import { toolError } from "../../utils/utils.js";
|
|
10
|
+
import { isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
10
11
|
import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRegenerateStrategy, buildDeleteStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
|
|
11
12
|
/**
|
|
12
13
|
* Compute a suggested new filename when an endpoint is renamed.
|
|
@@ -58,9 +59,9 @@ const recommendationSchema = z.object({
|
|
|
58
59
|
action: z
|
|
59
60
|
.nativeEnum(DriftAction)
|
|
60
61
|
.refine((a) => a !== DriftAction.Ignore, {
|
|
61
|
-
message: "
|
|
62
|
+
message: "Omit IGNORE entries entirely — a discovered test you don't recommend is automatically treated as needing no action.",
|
|
62
63
|
})
|
|
63
|
-
.describe("
|
|
64
|
+
.describe("The maintenance action for this test (UPDATE, REGENERATE, DELETE, or VERIFY). Do not send IGNORE — omit no-action tests from `recommendations` entirely; anything you leave out is recorded as needing no action."),
|
|
64
65
|
testType: z
|
|
65
66
|
.nativeEnum(TestType)
|
|
66
67
|
.describe("Type of this test — carried through to the maintenance report."),
|
|
@@ -89,6 +90,18 @@ const recommendationSchema = z.object({
|
|
|
89
90
|
"auth changes, new request params, removed fields, or other drift-related changes. " +
|
|
90
91
|
"Example: 'Added stock_count: int (ge=0, default=0) to ProductBase. " +
|
|
91
92
|
"Test hits GET /products — assert stock_count is present and non-negative.'"),
|
|
93
|
+
editSites: z
|
|
94
|
+
.array(z.object({
|
|
95
|
+
line: z.number().int().min(1).describe("1-based line number of the edit site in the current file."),
|
|
96
|
+
anchor: z
|
|
97
|
+
.string()
|
|
98
|
+
.min(1)
|
|
99
|
+
.describe("A short verbatim snippet from that line (copied exactly from the file you read) so the edit can be located without re-scanning."),
|
|
100
|
+
change: z.string().optional().describe("What to change at this site (e.g. 'add work_pool_name to this payload')."),
|
|
101
|
+
}))
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("For a large UPDATE file, the concrete per-site edits you already located while reading it — one entry per place that must change, with a verbatim anchor. " +
|
|
104
|
+
"Surfaced to the apply step as a checklist so it edits each site directly instead of re-grepping/re-paging the file. Provide this whenever an UPDATE touches more than a couple of sites."),
|
|
92
105
|
renamedEndpoints: z
|
|
93
106
|
.array(z.object({
|
|
94
107
|
oldPath: z.string().describe("Previous endpoint path"),
|
|
@@ -109,17 +122,9 @@ const actionsSchema = {
|
|
|
109
122
|
.describe("The owner/repo whose analysis section to act on (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for the primary repo's tests, or a related repo's owner/repo for that repo's section — within the run-scoped stateFile. Omit only for a single-repo run."),
|
|
110
123
|
recommendations: z
|
|
111
124
|
.array(recommendationSchema)
|
|
112
|
-
.optional()
|
|
113
|
-
.describe("Drift recommendations from skyramp_analyze_test_health. Omit IGNORE entries. Omitting this field results in no maintenance actions."),
|
|
114
|
-
ignoredTestFiles: z
|
|
115
|
-
.array(z.string().refine((p) => path.isAbsolute(p), {
|
|
116
|
-
message: "ignoredTestFiles entries must be absolute paths — use the exact path reported by skyramp_analyze_changes or skyramp_analyze_test_health. A bare filename is ambiguous when multiple discovered tests share that basename.",
|
|
117
|
-
}))
|
|
118
|
-
.optional()
|
|
119
125
|
.default([])
|
|
120
|
-
.describe("
|
|
121
|
-
"
|
|
122
|
-
"otherwise this call fails, naming the test(s) you didn't account for."),
|
|
126
|
+
.describe("Drift recommendations from skyramp_analyze_test_health — include only tests that need an action (UPDATE, REGENERATE, DELETE, or VERIFY). " +
|
|
127
|
+
"Any discovered test you don't list here is automatically treated as needing no action, so never add IGNORE entries or enumerate the no-action tests yourself. Defaults to an empty array."),
|
|
123
128
|
};
|
|
124
129
|
const TOOL_NAME = "skyramp_actions";
|
|
125
130
|
export function registerActionsTool(server) {
|
|
@@ -130,7 +135,7 @@ export function registerActionsTool(server) {
|
|
|
130
135
|
idempotentHint: false,
|
|
131
136
|
openWorldHint: true,
|
|
132
137
|
},
|
|
133
|
-
description: `Execute test maintenance actions — final step of the unified Test Health Analysis Flow. Call this once per run even when no tests need action — it is the only
|
|
138
|
+
description: `Execute test maintenance actions — final step of the unified Test Health Analysis Flow. Call this once per run even when no tests need action (pass \`recommendations: []\`) — it is where the maintenance verdicts are persisted before the run moves on. Supply only the tests that need an action; any discovered test you omit from \`recommendations\` is treated as needing no action and recorded automatically, so you never enumerate the no-action tests yourself.`,
|
|
134
139
|
inputSchema: actionsSchema,
|
|
135
140
|
}, async (args) => {
|
|
136
141
|
let errorResult;
|
|
@@ -195,10 +200,26 @@ export function registerActionsTool(server) {
|
|
|
195
200
|
// Catalog membership is the safety boundary — prevents editing non-test files.
|
|
196
201
|
const isRelevantExternal = (externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFilePath));
|
|
197
202
|
const isInAnyKnownCatalog = isSkyramp || isRelevantExternal;
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
// Discovery is relevance-scoped, not exhaustive — it can miss a real test file the
|
|
204
|
+
// agent correctly identifies from the diff (e.g. an e2e Playwright spec/POM under a
|
|
205
|
+
// dir discovery didn't rank relevant). Rejecting those as "hallucinated" silently
|
|
206
|
+
// drops correct maintenance verdicts, leaving report.testMaintenance empty even
|
|
207
|
+
// though the edit was applied (SKYR — D2 bucket B1). Accept an uncataloged
|
|
208
|
+
// recommendation when its testFilePath is a REAL file on disk, inside the repo root,
|
|
209
|
+
// whose name matches a test-file pattern. The path-traversal guard above already
|
|
210
|
+
// bounds it to repoRoot; isTestFile bounds it to test files (never app source), so
|
|
211
|
+
// this preserves the anti-hallucination guarantee (an invented path fails existsSync).
|
|
212
|
+
const isRealUncatalogedTest = !isInAnyKnownCatalog &&
|
|
213
|
+
isTestFile(resolvedFile) &&
|
|
214
|
+
(!repoRoot || resolvedFile.startsWith(repoRoot + path.sep) || resolvedFile === repoRoot) &&
|
|
215
|
+
fs.existsSync(resolvedFile);
|
|
216
|
+
if (!isInAnyKnownCatalog && !isRealUncatalogedTest) {
|
|
217
|
+
logger.warning(`Skipping ${rec.action} for unknown test (not in scanned catalog, not a real test file): ${rec.testFilePath}`);
|
|
200
218
|
return;
|
|
201
219
|
}
|
|
220
|
+
if (isRealUncatalogedTest) {
|
|
221
|
+
logger.info(`Accepting uncataloged but real test file discovery missed: ${rec.testFilePath}`);
|
|
222
|
+
}
|
|
202
223
|
const isActionable = [DriftAction.Update, DriftAction.Regenerate, DriftAction.Delete].includes(rec.action);
|
|
203
224
|
// No write access to a related repo's checkout — convert any edit-requiring
|
|
204
225
|
// action to VERIFY so the finding still surfaces without attempting a write that
|
|
@@ -217,21 +238,27 @@ export function registerActionsTool(server) {
|
|
|
217
238
|
});
|
|
218
239
|
return;
|
|
219
240
|
}
|
|
220
|
-
if (isActionable && !isSkyramp && !isRelevantExternal) {
|
|
241
|
+
if (isActionable && !isSkyramp && !isRelevantExternal && !isRealUncatalogedTest) {
|
|
221
242
|
logger.warning(`Skipping ${rec.action} for irrelevant external test: ${rec.testFilePath}`);
|
|
222
243
|
return;
|
|
223
244
|
}
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
245
|
+
// A real-but-uncataloged test is treated as external (user-authored) for policy.
|
|
246
|
+
const isExternalTarget = isRelevantExternal || isRealUncatalogedTest;
|
|
247
|
+
// REGENERATE and DELETE on external tests are report-only — the file is never
|
|
248
|
+
// touched, but the verdict keeps its REAL action (not flattened to VERIFY) so the
|
|
249
|
+
// report renders it as a deletion/regeneration RECOMMENDATION the developer applies
|
|
250
|
+
// manually. `reportOnly: true` makes the apply loops below skip it (B2 — a flattened
|
|
251
|
+
// VERIFY read to the D2 judge as "uncertain", hiding a correct DELETE call).
|
|
252
|
+
if (isExternalTarget && !isSkyramp &&
|
|
227
253
|
(rec.action === DriftAction.Regenerate || rec.action === DriftAction.Delete)) {
|
|
228
254
|
recommendations.push({
|
|
229
255
|
testFilePath: resolvedFile,
|
|
230
|
-
action:
|
|
256
|
+
action: rec.action,
|
|
257
|
+
reportOnly: true,
|
|
231
258
|
testType: rec.testType,
|
|
232
259
|
endpoint: rec.endpoint,
|
|
233
260
|
priority: rec.priority ?? RecommendationPriority.Medium,
|
|
234
|
-
rationale: `[external test —
|
|
261
|
+
rationale: `[external test — report-only ${rec.action}, apply manually] ${rec.rationale}`,
|
|
235
262
|
estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
|
|
236
263
|
updateInstructions: "",
|
|
237
264
|
renamedEndpoints: [],
|
|
@@ -261,32 +288,31 @@ export function registerActionsTool(server) {
|
|
|
261
288
|
estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
|
|
262
289
|
updateInstructions: rec.updateInstructions ?? "",
|
|
263
290
|
renamedEndpoints: rec.renamedEndpoints ?? [],
|
|
291
|
+
...(rec.editSites?.length ? { editSites: rec.editSites } : {}),
|
|
264
292
|
});
|
|
265
293
|
});
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
294
|
+
// Derive the no-action set here instead of asking the LLM to enumerate it.
|
|
295
|
+
// The caller supplies only the tests that need an action (`recommendations`); every
|
|
296
|
+
// other discovered test is, by definition, no-action. The tool already holds the full
|
|
297
|
+
// catalog (`testAnalysisResults`), so the ignored set is just the complement — computed
|
|
298
|
+
// in O(catalog) with zero LLM tokens. The previous design required the LLM to echo back
|
|
299
|
+
// an absolute path for every discovered test and hard-rejected otherwise (SKYR-3906);
|
|
300
|
+
// on a 1,000+ test monorepo the LLM cannot round-trip that list reliably, so it never
|
|
301
|
+
// converged and the run timed out (SKYR-3938). The report is built solely from
|
|
302
|
+
// `maintenanceVerdicts` (the validated recommendations), persisted below at
|
|
303
|
+
// decision-time — that persistence is what prevents a decided-upon entry from silently
|
|
304
|
+
// vanishing at report time; the ignored set is a complete, accurate record for free.
|
|
305
|
+
const recommendedPaths = new Set(recommendations.map((r) => r.testFilePath));
|
|
306
|
+
const ignoredTestFiles = testAnalysisResults
|
|
276
307
|
.map((t) => t.testFile)
|
|
277
|
-
.filter((testFile) => !
|
|
278
|
-
if (unaccountedFor.length > 0) {
|
|
279
|
-
errorResult = toolError(`${unaccountedFor.length} existing test(s) assessed by skyramp_analyze_test_health are not accounted for: ${unaccountedFor.join(", ")}. ` +
|
|
280
|
-
"Add a recommendation for each one that needs UPDATE/REGENERATE/DELETE/VERIFY, or add its path to ignoredTestFiles if it needs no action.");
|
|
281
|
-
return errorResult;
|
|
282
|
-
}
|
|
308
|
+
.filter((testFile) => !recommendedPaths.has(testFile));
|
|
283
309
|
// So skyramp_submit_report can read this back instead of re-asking the LLM.
|
|
284
310
|
// Must fail loud — edits below apply regardless, so a swallowed failure here
|
|
285
311
|
// would let edits ship with no testMaintenance in the final report.
|
|
286
312
|
try {
|
|
287
313
|
await stateManager.writeRepoData({
|
|
288
314
|
...stateData,
|
|
289
|
-
ignoredTestFiles
|
|
315
|
+
ignoredTestFiles,
|
|
290
316
|
maintenanceVerdicts: recommendations.map((r) => ({
|
|
291
317
|
testFilePath: r.testFilePath,
|
|
292
318
|
action: r.action,
|
|
@@ -317,6 +343,10 @@ export function registerActionsTool(server) {
|
|
|
317
343
|
const regenerateByFile = new Map();
|
|
318
344
|
const deleteByFile = new Map();
|
|
319
345
|
for (const rec of recommendations) {
|
|
346
|
+
// Report-only verdicts (external DELETE/REGENERATE) are recorded for the report
|
|
347
|
+
// but never applied — keep them out of every apply map.
|
|
348
|
+
if (rec.reportOnly)
|
|
349
|
+
continue;
|
|
320
350
|
if (rec.action === DriftAction.Delete) {
|
|
321
351
|
const existing = deleteByFile.get(rec.testFilePath);
|
|
322
352
|
if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
|
|
@@ -494,7 +524,12 @@ export function registerActionsTool(server) {
|
|
|
494
524
|
.map((rec) => {
|
|
495
525
|
const editTarget = rec.pomFile ?? rec.testFilePath;
|
|
496
526
|
const current_content = testFileContentMap.get(editTarget);
|
|
497
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
file: editTarget,
|
|
529
|
+
context: rec.updateInstructions,
|
|
530
|
+
...(rec.editSites?.length ? { edit_sites: rec.editSites } : {}),
|
|
531
|
+
...(current_content !== undefined && { current_content }),
|
|
532
|
+
};
|
|
498
533
|
});
|
|
499
534
|
if (updateContext.length > 0) {
|
|
500
535
|
llmInstructionsObj.update_context = updateContext;
|
|
@@ -549,7 +584,7 @@ export function registerActionsTool(server) {
|
|
|
549
584
|
}
|
|
550
585
|
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
551
586
|
repositoryPath,
|
|
552
|
-
});
|
|
587
|
+
}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
553
588
|
}
|
|
554
589
|
});
|
|
555
590
|
}
|
|
@@ -51,7 +51,7 @@ async function writeStateFile(dir, existingTests, repositoryAnalysis) {
|
|
|
51
51
|
}), "utf-8");
|
|
52
52
|
return stateFile;
|
|
53
53
|
}
|
|
54
|
-
describe("registerActionsTool —
|
|
54
|
+
describe("registerActionsTool — verdict reconciliation & persistence (SKYR-3906, SKYR-3938)", () => {
|
|
55
55
|
let handler;
|
|
56
56
|
let tmpDirs = [];
|
|
57
57
|
beforeAll(() => {
|
|
@@ -85,32 +85,25 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
85
85
|
expect(result.content).toBeDefined();
|
|
86
86
|
expect(result.isError).toBeUndefined();
|
|
87
87
|
});
|
|
88
|
-
it("
|
|
88
|
+
it("derives the ignored set as the catalog complement of recommendations — no LLM enumeration (SKYR-3938)", async () => {
|
|
89
|
+
// The caller supplies only actionable recommendations; every other discovered test is
|
|
90
|
+
// recorded as ignored automatically. The LLM payload is O(recommendations) — independent of
|
|
91
|
+
// catalog size — which is the fix for the large-repo timeout where the LLM had to echo back
|
|
92
|
+
// every path. (Server-side derivation is still O(catalog): filter the catalog by a
|
|
93
|
+
// recommendations Set.)
|
|
89
94
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
90
95
|
tmpDirs.push(tmpDir);
|
|
91
96
|
const testFile = path.join(tmpDir, "unrelated_test.py");
|
|
92
97
|
await fs.writeFile(testFile, "# test");
|
|
93
98
|
const stateFile = await writeStateFile(tmpDir, [{ testFile, source: TestSource.Skyramp }]);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ignoredTestFiles: [testFile],
|
|
97
|
-
});
|
|
99
|
+
// No recommendations at all — the sole discovered test is recorded as ignored.
|
|
100
|
+
const result = await handler({ stateFile, recommendations: [] });
|
|
98
101
|
expect(result.isError).toBeUndefined();
|
|
102
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
103
|
+
expect(written.ignoredTestFiles).toEqual([testFile]);
|
|
104
|
+
expect(written.maintenanceVerdicts).toEqual([]);
|
|
99
105
|
});
|
|
100
|
-
it("rejects a
|
|
101
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
102
|
-
tmpDirs.push(tmpDir);
|
|
103
|
-
const testFile = path.join(tmpDir, "unrelated_test.py");
|
|
104
|
-
await fs.writeFile(testFile, "# test");
|
|
105
|
-
const stateFile = await writeStateFile(tmpDir, [{ testFile, source: TestSource.Skyramp }]);
|
|
106
|
-
const result = await handler({
|
|
107
|
-
stateFile,
|
|
108
|
-
ignoredTestFiles: ["unrelated_test.py"],
|
|
109
|
-
});
|
|
110
|
-
expect(result.isError).toBe(true);
|
|
111
|
-
expect(result.content[0].text).toContain("must be absolute paths");
|
|
112
|
-
});
|
|
113
|
-
it("rejects a recommendation with action: IGNORE at the schema level — must use ignoredTestFiles", async () => {
|
|
106
|
+
it("rejects a recommendation with action: IGNORE at the schema level — must simply be omitted", async () => {
|
|
114
107
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
115
108
|
tmpDirs.push(tmpDir);
|
|
116
109
|
const testFile = path.join(tmpDir, "unrelated_test.py");
|
|
@@ -119,12 +112,16 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
119
112
|
const result = await handler({
|
|
120
113
|
stateFile,
|
|
121
114
|
recommendations: [{ testFilePath: testFile, action: DriftAction.Ignore, rationale: "No change needed", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
122
|
-
//
|
|
115
|
+
// IGNORE must not silently persist via recommendations — it must simply be omitted.
|
|
123
116
|
});
|
|
124
117
|
expect(result.isError).toBe(true);
|
|
125
|
-
expect(result.content[0].text).toContain("
|
|
118
|
+
expect(result.content[0].text).toContain("Omit IGNORE entries entirely");
|
|
126
119
|
});
|
|
127
|
-
it("
|
|
120
|
+
it("treats an existing test omitted from recommendations as needing no action — no completeness gate (SKYR-3938)", async () => {
|
|
121
|
+
// The prior hard-reject forced the LLM to echo back every discovered test, which is
|
|
122
|
+
// infeasible on large repos (1,000+ tests) and caused the run to spin until CI timeout.
|
|
123
|
+
// An omitted test is now silently no-action: the call succeeds, only the covered file
|
|
124
|
+
// lands in the verdicts, and the dropped file is recorded in the derived ignored set.
|
|
128
125
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
129
126
|
tmpDirs.push(tmpDir);
|
|
130
127
|
const coveredFile = path.join(tmpDir, "covered_test.py");
|
|
@@ -138,13 +135,16 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
138
135
|
const result = await handler({
|
|
139
136
|
stateFile,
|
|
140
137
|
recommendations: [{ testFilePath: coveredFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
141
|
-
// droppedFile intentionally absent from
|
|
138
|
+
// droppedFile intentionally absent from recommendations
|
|
142
139
|
});
|
|
143
|
-
expect(result.isError).
|
|
144
|
-
|
|
145
|
-
expect(
|
|
140
|
+
expect(result.isError).toBeUndefined();
|
|
141
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
142
|
+
expect(written.maintenanceVerdicts).toEqual([
|
|
143
|
+
{ testFilePath: coveredFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" },
|
|
144
|
+
]);
|
|
145
|
+
expect(written.ignoredTestFiles).toEqual([droppedFile]);
|
|
146
146
|
});
|
|
147
|
-
it("persists
|
|
147
|
+
it("persists maintenanceVerdicts + a derived ignoredTestFiles record to the state file", async () => {
|
|
148
148
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
149
149
|
tmpDirs.push(tmpDir);
|
|
150
150
|
const updatedFile = path.join(tmpDir, "orders_contract_test.py");
|
|
@@ -155,10 +155,10 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
155
155
|
{ testFile: updatedFile, source: TestSource.Skyramp },
|
|
156
156
|
{ testFile: ignoredFile, source: TestSource.Skyramp },
|
|
157
157
|
]);
|
|
158
|
+
// Only the actionable test is supplied; ignoredFile is derived as the catalog complement.
|
|
158
159
|
await handler({
|
|
159
160
|
stateFile,
|
|
160
161
|
recommendations: [{ testFilePath: updatedFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
161
|
-
ignoredTestFiles: [ignoredFile],
|
|
162
162
|
});
|
|
163
163
|
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
164
164
|
expect(written.ignoredTestFiles).toEqual([ignoredFile]);
|
|
@@ -168,6 +168,67 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
168
168
|
// Original existingTests must survive the write-back untouched.
|
|
169
169
|
expect(written.existingTests).toHaveLength(2);
|
|
170
170
|
});
|
|
171
|
+
it("accepts a real test file discovery missed (uncataloged) instead of dropping it (B1)", async () => {
|
|
172
|
+
// Discovery is relevance-scoped, so a real e2e spec/POM the agent correctly identifies
|
|
173
|
+
// can be absent from the catalog. Before B1 its verdict was dropped as "hallucinated",
|
|
174
|
+
// leaving report.testMaintenance empty even though the edit was applied.
|
|
175
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
176
|
+
tmpDirs.push(tmpDir);
|
|
177
|
+
const cataloged = path.join(tmpDir, "orders_contract_test.py");
|
|
178
|
+
await fs.writeFile(cataloged, "# test");
|
|
179
|
+
const e2eDir = path.join(tmpDir, "e2e", "specs");
|
|
180
|
+
await fs.mkdir(e2eDir, { recursive: true });
|
|
181
|
+
const uncataloged = path.join(e2eDir, "drafts.spec.ts"); // real on disk, NOT in existingTests
|
|
182
|
+
await fs.writeFile(uncataloged, "import { test } from '@playwright/test';\ntest('x', async () => {});");
|
|
183
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile: cataloged, source: TestSource.Skyramp }]);
|
|
184
|
+
const result = await handler({
|
|
185
|
+
stateFile,
|
|
186
|
+
recommendations: [
|
|
187
|
+
{ testFilePath: uncataloged, action: DriftAction.Update, rationale: "testid moved, re-scope locator", testType: TestType.E2E, endpoint: "/drafts", updateInstructions: "re-scope the badge locator" },
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
expect(result.isError).toBeUndefined();
|
|
191
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
192
|
+
expect(written.maintenanceVerdicts.map((v) => v.testFilePath)).toContain(uncataloged);
|
|
193
|
+
});
|
|
194
|
+
it("still rejects a recommendation for a non-existent (hallucinated) test path (B1 boundary)", async () => {
|
|
195
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
196
|
+
tmpDirs.push(tmpDir);
|
|
197
|
+
const cataloged = path.join(tmpDir, "orders_contract_test.py");
|
|
198
|
+
await fs.writeFile(cataloged, "# test");
|
|
199
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile: cataloged, source: TestSource.Skyramp }]);
|
|
200
|
+
const ghost = path.join(tmpDir, "e2e", "does_not_exist.spec.ts"); // matches test pattern but NOT on disk
|
|
201
|
+
const result = await handler({
|
|
202
|
+
stateFile,
|
|
203
|
+
recommendations: [
|
|
204
|
+
{ testFilePath: ghost, action: DriftAction.Update, rationale: "hallucinated", testType: TestType.E2E, endpoint: "/x", updateInstructions: "y" },
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
expect(result.isError).toBeUndefined();
|
|
208
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
209
|
+
expect(written.maintenanceVerdicts.map((v) => v.testFilePath)).not.toContain(ghost);
|
|
210
|
+
});
|
|
211
|
+
it("records external DELETE as a report-only verdict (real action, file untouched) — no VERIFY flattening (B2)", async () => {
|
|
212
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
213
|
+
tmpDirs.push(tmpDir);
|
|
214
|
+
const extTest = path.join(tmpDir, "whats_new.spec.ts");
|
|
215
|
+
await fs.writeFile(extTest, "import { test } from '@playwright/test';\ntest('whats new', async () => {});");
|
|
216
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile: extTest, source: TestSource.External }]);
|
|
217
|
+
const result = await handler({
|
|
218
|
+
stateFile,
|
|
219
|
+
recommendations: [
|
|
220
|
+
{ testFilePath: extTest, action: DriftAction.Delete, rationale: "What's New feature removed in this PR", testType: TestType.E2E, endpoint: "/whats-new" },
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
expect(result.isError).toBeUndefined();
|
|
224
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
225
|
+
const verdict = written.maintenanceVerdicts.find((v) => v.testFilePath === extTest);
|
|
226
|
+
expect(verdict).toBeDefined();
|
|
227
|
+
// The verdict keeps the real DELETE action — NOT flattened to VERIFY (the D2-judge bug).
|
|
228
|
+
expect(verdict.action).toBe(DriftAction.Delete);
|
|
229
|
+
// Report-only: the external test file must still exist on disk.
|
|
230
|
+
await expect(fs.access(extTest)).resolves.toBeUndefined();
|
|
231
|
+
});
|
|
171
232
|
it("fails loud when persisting maintenanceVerdicts to state fails, instead of silently applying edits anyway", async () => {
|
|
172
233
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
173
234
|
tmpDirs.push(tmpDir);
|
|
@@ -270,9 +331,8 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
270
331
|
});
|
|
271
332
|
// ── Multi-repo (`repository` param) ─────────────────────────────────────────
|
|
272
333
|
// A run-scoped state file holds the primary repo at the root and each related
|
|
273
|
-
// repo under `relatedRepos[<owner/repo>]`. `args.repository` must scope
|
|
274
|
-
//
|
|
275
|
-
// to the matching section only.
|
|
334
|
+
// repo under `relatedRepos[<owner/repo>]`. `args.repository` must scope the
|
|
335
|
+
// maintenanceVerdicts/ignoredTestFiles write-back to the matching section only.
|
|
276
336
|
async function writeMultiRepoStateFile(stateFile, primary, related) {
|
|
277
337
|
const now = "2026-01-01T00:00:00.000Z";
|
|
278
338
|
await fs.writeFile(stateFile, JSON.stringify({
|
|
@@ -295,7 +355,7 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
295
355
|
},
|
|
296
356
|
}), "utf-8");
|
|
297
357
|
}
|
|
298
|
-
it("scopes the
|
|
358
|
+
it("scopes the write-back to a related (non-primary) repo section, leaving the primary section untouched", async () => {
|
|
299
359
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
300
360
|
tmpDirs.push(tmpDir);
|
|
301
361
|
const primaryDir = path.join(tmpDir, "primary");
|
|
@@ -344,7 +404,7 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
344
404
|
{ testFilePath: secondaryTestFile, action: DriftAction.Verify, rationale: "[related repo — no write access, needs manual review] Endpoint removed", testType: TestType.CONTRACT, endpoint: "DELETE /api/v1/reset" },
|
|
345
405
|
]);
|
|
346
406
|
});
|
|
347
|
-
it("
|
|
407
|
+
it("scopes the write-back to the related repo section, writing an empty verdict record when nothing needs action (SKYR-3938)", async () => {
|
|
348
408
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
349
409
|
tmpDirs.push(tmpDir);
|
|
350
410
|
const primaryDir = path.join(tmpDir, "primary");
|
|
@@ -357,14 +417,20 @@ describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
|
357
417
|
await fs.writeFile(secondaryTestFile, "# test");
|
|
358
418
|
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
359
419
|
await writeMultiRepoStateFile(stateFile, { repositoryPath: primaryDir, existingTests: [{ testFile: primaryTestFile, source: TestSource.Skyramp }] }, { repo: "org/secondary", repositoryPath: secondaryDir, existingTests: [{ testFile: secondaryTestFile, source: TestSource.Skyramp }] });
|
|
360
|
-
//
|
|
361
|
-
// the
|
|
420
|
+
// With no completeness gate, a related-repo test the LLM left unlisted is simply
|
|
421
|
+
// no-action: the call succeeds and writes an empty maintenanceVerdicts to the related
|
|
422
|
+
// section only, leaving the primary section untouched.
|
|
362
423
|
const result = await handler({
|
|
363
424
|
stateFile,
|
|
364
425
|
repository: "org/secondary",
|
|
365
426
|
recommendations: [],
|
|
366
427
|
});
|
|
367
|
-
expect(result.isError).
|
|
368
|
-
|
|
428
|
+
expect(result.isError).toBeUndefined();
|
|
429
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
430
|
+
expect(written.relatedRepos["org/secondary"].data.maintenanceVerdicts).toEqual([]);
|
|
431
|
+
// The related section's lone test is recorded as ignored (derived complement), scoped
|
|
432
|
+
// to that section — the primary section stays untouched.
|
|
433
|
+
expect(written.relatedRepos["org/secondary"].data.ignoredTestFiles).toEqual([secondaryTestFile]);
|
|
434
|
+
expect(written.maintenanceVerdicts).toBeUndefined();
|
|
369
435
|
});
|
|
370
436
|
});
|