@skyramp/mcp 0.2.7 → 0.2.8
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/index.js +5 -2
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
- package/build/prompts/sut-setup/shared.d.ts +39 -0
- package/build/prompts/sut-setup/shared.js +132 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +76 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +91 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +16 -1
- package/build/prompts/testbot/testbot-prompts.js +113 -22
- package/build/prompts/testbot/testbot-prompts.test.js +95 -45
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.js +2 -2
- package/build/services/TestDiscoveryService.d.ts +2 -2
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestExecutionService.d.ts +10 -1
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.js +306 -0
- package/build/tools/executeSkyrampTestTool.js +26 -7
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/submitReportTool.d.ts +10 -0
- package/build/tools/submitReportTool.js +106 -39
- package/build/tools/submitReportTool.test.js +296 -50
- package/build/tools/test-management/actionsTool.js +21 -7
- package/build/tools/test-management/analyzeChangesTool.d.ts +1 -1
- package/build/tools/test-management/analyzeChangesTool.js +106 -41
- package/build/tools/test-management/analyzeChangesTool.test.js +21 -41
- package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
- package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
- package/build/tools/workspace/serviceUpsert.d.ts +12 -0
- package/build/tools/workspace/serviceUpsert.js +23 -0
- package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
- package/build/tools/workspace/serviceUpsert.test.js +50 -0
- package/build/types/TestAnalysis.d.ts +2 -1
- package/build/types/TestExecution.d.ts +12 -2
- package/build/types/TestExecution.js +11 -1
- package/build/types/TestbotReport.d.ts +68 -0
- package/build/types/TestbotReport.js +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.js +3 -0
- package/build/utils/AnalysisStateManager.d.ts +61 -1
- package/build/utils/AnalysisStateManager.js +144 -3
- package/build/utils/AnalysisStateManager.test.js +98 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/routeParsers.d.ts +0 -10
- package/build/utils/routeParsers.js +31 -38
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +23 -1
- package/build/utils/utils.d.ts +7 -1
- package/build/utils/utils.js +10 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +10 -10
- package/build/workspace/workspace.js +7 -7
- package/build/workspace/workspace.test.js +6 -6
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- 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/server/recorder/recorderApp.js +79 -19
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.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.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
- 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/server/recorder/recorderApp.ts +129 -43
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { DriftAction } from "./TestAnalysis.js";
|
|
2
|
+
import type { TestExecutionStatus } from "./TestExecution.js";
|
|
3
|
+
import type { HttpMethod, TestType } from "./TestTypes.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shape of the JSON report written by skyramp_submit_report and read by testbot
|
|
6
|
+
* for rendering as Markdown. All fields mirror the corresponding Zod schemas in
|
|
7
|
+
* submitReportTool.ts — keep the two in sync.
|
|
8
|
+
*/
|
|
9
|
+
export interface TestbotReport {
|
|
10
|
+
businessCaseAnalysis: string;
|
|
11
|
+
newTestsCreated: {
|
|
12
|
+
testId: string;
|
|
13
|
+
testType: TestType;
|
|
14
|
+
endpoint: string;
|
|
15
|
+
fileName: string;
|
|
16
|
+
reasoning: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
scenarioFile?: string;
|
|
19
|
+
traceFile?: string;
|
|
20
|
+
frontendTrace?: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** Omitted when no existing tests were found (agent omits the field). */
|
|
23
|
+
testMaintenance?: {
|
|
24
|
+
testType: TestType;
|
|
25
|
+
endpoint: string;
|
|
26
|
+
fileName: string;
|
|
27
|
+
description: string;
|
|
28
|
+
action: DriftAction;
|
|
29
|
+
beforeStatus: TestExecutionStatus;
|
|
30
|
+
beforeDetails: string;
|
|
31
|
+
afterStatus: TestExecutionStatus;
|
|
32
|
+
afterDetails: string;
|
|
33
|
+
}[];
|
|
34
|
+
testResults: {
|
|
35
|
+
testType: TestType;
|
|
36
|
+
endpoint: string;
|
|
37
|
+
status: "Pass" | "Fail" | "Skipped";
|
|
38
|
+
details: string;
|
|
39
|
+
videoPath?: string;
|
|
40
|
+
}[];
|
|
41
|
+
additionalRecommendations?: {
|
|
42
|
+
testId: string;
|
|
43
|
+
testType: TestType;
|
|
44
|
+
category?: string;
|
|
45
|
+
primaryEndpoint?: string;
|
|
46
|
+
scenarioName?: string;
|
|
47
|
+
steps: {
|
|
48
|
+
method?: HttpMethod;
|
|
49
|
+
path?: string;
|
|
50
|
+
description: string;
|
|
51
|
+
expectedStatusCode?: number;
|
|
52
|
+
requestBody?: Record<string, unknown>;
|
|
53
|
+
responseBody?: Record<string, unknown>;
|
|
54
|
+
}[];
|
|
55
|
+
description: string;
|
|
56
|
+
priority: "high" | "medium" | "low";
|
|
57
|
+
reasoning: string;
|
|
58
|
+
openApiSpec?: string;
|
|
59
|
+
backendTrace?: string;
|
|
60
|
+
frontendTrace?: string;
|
|
61
|
+
}[];
|
|
62
|
+
issuesFound: {
|
|
63
|
+
description: string;
|
|
64
|
+
severity?: "critical" | "high" | "medium" | "low";
|
|
65
|
+
}[];
|
|
66
|
+
nextSteps: string[];
|
|
67
|
+
commitMessage: string;
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -6,6 +6,10 @@ import type { CandidateUiPage } from "./uiPageEnumerator.js";
|
|
|
6
6
|
export type { CandidateUiPage } from "./uiPageEnumerator.js";
|
|
7
7
|
export declare function setTestsRepoDir(dir: string | undefined): void;
|
|
8
8
|
export declare function getTestsRepoDir(): string | undefined;
|
|
9
|
+
export declare function setActiveRunStatePath(stateFilePath: string): void;
|
|
10
|
+
export declare function getActiveRunStatePath(): string | undefined;
|
|
11
|
+
/** Reset the run anchor — for tests, and as a safety hook between runs. */
|
|
12
|
+
export declare function clearActiveRunStatePath(): void;
|
|
9
13
|
export declare function registerSession(sessionId: string, stateFilePath: string): void;
|
|
10
14
|
export declare function getSessionFilePath(sessionId: string): string | undefined;
|
|
11
15
|
export declare function getRegisteredSessions(): ReadonlyMap<string, string>;
|
|
@@ -96,17 +100,39 @@ interface StateMetadata {
|
|
|
96
100
|
sessionId: string;
|
|
97
101
|
stateType: StateType;
|
|
98
102
|
repositoryPath?: string;
|
|
103
|
+
/** owner/repo of the PRIMARY repo, recorded on the root so related-repo writes
|
|
104
|
+
* can be distinguished from the primary. Absent only for legacy single-repo
|
|
105
|
+
* state files written before explicit tagging. */
|
|
106
|
+
repository?: string;
|
|
99
107
|
createdAt: string;
|
|
100
108
|
updatedAt: string;
|
|
101
109
|
step?: string;
|
|
102
110
|
}
|
|
103
111
|
/**
|
|
104
|
-
*
|
|
112
|
+
* One related repo's analysis section in a run-scoped (multi-repo) state file.
|
|
113
|
+
* The primary repo's analysis stays at the root for backward compatibility; each
|
|
114
|
+
* related repo's analysis is stored here keyed by its `owner/repo`.
|
|
115
|
+
*/
|
|
116
|
+
interface RepoStateSection<T> {
|
|
117
|
+
/** Analysis payload — same shape as the root (e.g. UnifiedAnalysisState). */
|
|
118
|
+
data: T;
|
|
119
|
+
/** Absolute checkout path of this repo (used to re-absolutize test paths). */
|
|
120
|
+
repositoryPath?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* State file format - data fields at root level with metadata.
|
|
105
124
|
* Example: { tests: [...], metadata: {...} }
|
|
106
125
|
* Example: { repositoryPath: "...", analysis: {...}, mapping: {...}, metadata: {...} }
|
|
126
|
+
*
|
|
127
|
+
* Multi-repo (run-scoped) files additionally carry `relatedRepos`: a map of
|
|
128
|
+
* `owner/repo` → that repo's analysis section. State is per-RUN, not per-repo —
|
|
129
|
+
* one file holds the primary (at root) plus every related repo's section, so
|
|
130
|
+
* there is exactly one stateFile per Testbot run. A file with no `relatedRepos`
|
|
131
|
+
* key is a single-repo file and behaves exactly as before.
|
|
107
132
|
*/
|
|
108
133
|
type StateFile<T> = T & {
|
|
109
134
|
metadata: StateMetadata;
|
|
135
|
+
relatedRepos?: Record<string, RepoStateSection<T>>;
|
|
110
136
|
};
|
|
111
137
|
/**
|
|
112
138
|
* Generic State Manager for persisting workflow data
|
|
@@ -150,7 +176,41 @@ export declare class StateManager<T = any> {
|
|
|
150
176
|
writeData(data: T, options?: {
|
|
151
177
|
repositoryPath?: string;
|
|
152
178
|
step?: string;
|
|
179
|
+
/** owner/repo recorded on the root metadata — identifies the PRIMARY repo
|
|
180
|
+
* so later section routing (isPrimaryRepo) can compare against it.
|
|
181
|
+
* Preserved when omitted. */
|
|
182
|
+
repository?: string;
|
|
183
|
+
}): Promise<void>;
|
|
184
|
+
/** Is `repo` the primary section? Primary when `repo` is falsy (single-repo /
|
|
185
|
+
* legacy callers), when no state file exists yet (the FIRST write of a run is
|
|
186
|
+
* the primary — the testbot prompt mandates the primary is analyzed first), or
|
|
187
|
+
* when `repo` matches the `repo` recorded on the root metadata. A non-empty
|
|
188
|
+
* `repo` that doesn't match an already-recorded root is a RELATED repo. */
|
|
189
|
+
private isPrimaryRepo;
|
|
190
|
+
/**
|
|
191
|
+
* Write one repo's analysis into the run-scoped state file. The primary repo
|
|
192
|
+
* (repo absent, or matching the root's recorded repo, or the first write of the
|
|
193
|
+
* run) is written to the root via writeData(); a related repo is upserted into
|
|
194
|
+
* `relatedRepos[repo]`, preserving the primary + other sections. Used by
|
|
195
|
+
* skyramp_analyze_changes — every repo in the run writes the SAME stateFile path.
|
|
196
|
+
*/
|
|
197
|
+
writeRepoData(data: T, options: {
|
|
198
|
+
repo?: string;
|
|
199
|
+
repositoryPath?: string;
|
|
200
|
+
step?: string;
|
|
153
201
|
}): Promise<void>;
|
|
202
|
+
/**
|
|
203
|
+
* Read one repo's analysis payload from the run-scoped state file. `repo`
|
|
204
|
+
* undefined → the primary (root) section (identical to readData()).
|
|
205
|
+
*/
|
|
206
|
+
readRepoData(repo?: string): Promise<T | null>;
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the checkout path for a repo's section — used to re-absolutize test
|
|
209
|
+
* paths. `repo` undefined → the primary's metadata.repositoryPath.
|
|
210
|
+
*/
|
|
211
|
+
getRepoRepositoryPath(repo?: string): Promise<string | undefined>;
|
|
212
|
+
/** List the related repo keys present in the run-scoped file (empty if single-repo). */
|
|
213
|
+
listRelatedRepos(): Promise<string[]>;
|
|
154
214
|
getStatePath(): string;
|
|
155
215
|
getSessionId(): string;
|
|
156
216
|
getStateType(): StateType;
|
|
@@ -22,6 +22,29 @@ export function setTestsRepoDir(dir) {
|
|
|
22
22
|
export function getTestsRepoDir() {
|
|
23
23
|
return _testsRepoDir;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Multi-repo run anchor: the path of the run-scoped analysis state file for the
|
|
27
|
+
* current Testbot run. One Testbot run = one MCP server process, so a single
|
|
28
|
+
* module-level value is naturally run-scoped (same pattern as _testsRepoDir and
|
|
29
|
+
* processSessionRegistry above).
|
|
30
|
+
*
|
|
31
|
+
* skyramp_analyze_changes sets this on the FIRST call of a multi-repo run (the one
|
|
32
|
+
* carrying a `repository`) and REUSES it on subsequent related-repo calls, so every
|
|
33
|
+
* repo's analysis lands in ONE state file with per-repo sections — without the agent
|
|
34
|
+
* ever passing a file path (which previously caused filename hallucinations).
|
|
35
|
+
* Single-repo / standalone calls (no `repository`) never set or read this.
|
|
36
|
+
*/
|
|
37
|
+
let _activeRunStatePath;
|
|
38
|
+
export function setActiveRunStatePath(stateFilePath) {
|
|
39
|
+
_activeRunStatePath = stateFilePath;
|
|
40
|
+
}
|
|
41
|
+
export function getActiveRunStatePath() {
|
|
42
|
+
return _activeRunStatePath;
|
|
43
|
+
}
|
|
44
|
+
/** Reset the run anchor — for tests, and as a safety hook between runs. */
|
|
45
|
+
export function clearActiveRunStatePath() {
|
|
46
|
+
_activeRunStatePath = undefined;
|
|
47
|
+
}
|
|
25
48
|
/**
|
|
26
49
|
* In-memory session store: sessionId → { data, storedAt }.
|
|
27
50
|
* Eliminates the need for the LLM to read/write state files on disk.
|
|
@@ -119,6 +142,15 @@ export class StateManager {
|
|
|
119
142
|
if (stateFilePath) {
|
|
120
143
|
this.stateFile = stateFilePath;
|
|
121
144
|
}
|
|
145
|
+
else if (stateType === "analysis" && process.env.RUNNER_TEMP) {
|
|
146
|
+
// In CI (testbot action), RUNNER_TEMP is a job-isolated dir that both the
|
|
147
|
+
// action and this tool can independently derive — no LLM input needed.
|
|
148
|
+
// TODO: interim step — remove this branch when stateFile and summaryOutputFile are merged
|
|
149
|
+
// (see getTestbotPrompt TODO). RUNNER_TEMP is GitHub Actions-specific; other CI systems
|
|
150
|
+
// (Jenkins, GitLab, Buildkite) must set it explicitly. Once merged, path flows through
|
|
151
|
+
// summaryOutputFile which is already testbot-controlled and CI-agnostic.
|
|
152
|
+
this.stateFile = path.join(process.env.RUNNER_TEMP, "skyramp", "analyze-changes-state.json");
|
|
153
|
+
}
|
|
122
154
|
else {
|
|
123
155
|
const baseDir = stateDir || os.tmpdir();
|
|
124
156
|
const prefix = STATE_FILE_PREFIXES[stateType];
|
|
@@ -161,7 +193,11 @@ export class StateManager {
|
|
|
161
193
|
}
|
|
162
194
|
try {
|
|
163
195
|
const content = await fs.promises.readFile(this.stateFile, "utf-8");
|
|
164
|
-
|
|
196
|
+
// Strip both metadata and the multi-repo relatedRepos section so callers
|
|
197
|
+
// get only the PRIMARY repo's analysis payload (unchanged single-repo shape).
|
|
198
|
+
const { metadata, relatedRepos, ...data } = JSON.parse(content);
|
|
199
|
+
void metadata;
|
|
200
|
+
void relatedRepos;
|
|
165
201
|
logger.debug(`Read data from state file: ${this.stateFile}`);
|
|
166
202
|
return data;
|
|
167
203
|
}
|
|
@@ -192,11 +228,14 @@ export class StateManager {
|
|
|
192
228
|
*/
|
|
193
229
|
async writeData(data, options) {
|
|
194
230
|
try {
|
|
195
|
-
// Read existing metadata if file exists
|
|
231
|
+
// Read existing metadata + any related-repo sections if file exists, so
|
|
232
|
+
// rewriting the primary (root) payload preserves the multi-repo sections.
|
|
196
233
|
let existingMetadata;
|
|
234
|
+
let existingRelated;
|
|
197
235
|
if (this.exists()) {
|
|
198
236
|
const existing = await this.readFullState();
|
|
199
237
|
existingMetadata = existing?.metadata;
|
|
238
|
+
existingRelated = existing?.relatedRepos;
|
|
200
239
|
}
|
|
201
240
|
const state = {
|
|
202
241
|
...data,
|
|
@@ -204,10 +243,12 @@ export class StateManager {
|
|
|
204
243
|
sessionId: this.sessionId,
|
|
205
244
|
stateType: this.stateType,
|
|
206
245
|
repositoryPath: options?.repositoryPath || existingMetadata?.repositoryPath,
|
|
246
|
+
repository: options?.repository ?? existingMetadata?.repository,
|
|
207
247
|
createdAt: existingMetadata?.createdAt || new Date().toISOString(),
|
|
208
248
|
updatedAt: new Date().toISOString(),
|
|
209
249
|
step: options?.step,
|
|
210
250
|
},
|
|
251
|
+
...(existingRelated ? { relatedRepos: existingRelated } : {}),
|
|
211
252
|
};
|
|
212
253
|
await fs.promises.mkdir(path.dirname(this.stateFile), { recursive: true });
|
|
213
254
|
await fs.promises.writeFile(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
@@ -218,6 +259,106 @@ export class StateManager {
|
|
|
218
259
|
throw new Error(`Failed to write state file: ${error.message}`);
|
|
219
260
|
}
|
|
220
261
|
}
|
|
262
|
+
// -----------------------------------------------------------------------
|
|
263
|
+
// Multi-repo (run-scoped) section access
|
|
264
|
+
//
|
|
265
|
+
// State is per-RUN: one file holds the primary repo at the root plus each
|
|
266
|
+
// related repo under `relatedRepos[<owner/repo>]`. `repo` undefined (or
|
|
267
|
+
// matching the root's metadata.repo) targets the PRIMARY section, so all
|
|
268
|
+
// single-repo behavior is unchanged.
|
|
269
|
+
// -----------------------------------------------------------------------
|
|
270
|
+
/** Is `repo` the primary section? Primary when `repo` is falsy (single-repo /
|
|
271
|
+
* legacy callers), when no state file exists yet (the FIRST write of a run is
|
|
272
|
+
* the primary — the testbot prompt mandates the primary is analyzed first), or
|
|
273
|
+
* when `repo` matches the `repo` recorded on the root metadata. A non-empty
|
|
274
|
+
* `repo` that doesn't match an already-recorded root is a RELATED repo. */
|
|
275
|
+
async isPrimaryRepo(repo) {
|
|
276
|
+
if (!repo)
|
|
277
|
+
return true;
|
|
278
|
+
// First write of the run is the primary (the prompt mandates the primary is
|
|
279
|
+
// analyzed first), even when it carries an explicit owner/repo.
|
|
280
|
+
if (!this.exists())
|
|
281
|
+
return true;
|
|
282
|
+
const full = await this.readFullState();
|
|
283
|
+
// File already exists: primary only if `repo` matches the repo recorded on
|
|
284
|
+
// the root. An untagged root means the primary was written without a repo
|
|
285
|
+
// (legacy/single-repo), so any non-empty `repo` here is a RELATED repo.
|
|
286
|
+
return full?.metadata.repository === repo;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Write one repo's analysis into the run-scoped state file. The primary repo
|
|
290
|
+
* (repo absent, or matching the root's recorded repo, or the first write of the
|
|
291
|
+
* run) is written to the root via writeData(); a related repo is upserted into
|
|
292
|
+
* `relatedRepos[repo]`, preserving the primary + other sections. Used by
|
|
293
|
+
* skyramp_analyze_changes — every repo in the run writes the SAME stateFile path.
|
|
294
|
+
*/
|
|
295
|
+
async writeRepoData(data, options) {
|
|
296
|
+
if (await this.isPrimaryRepo(options.repo)) {
|
|
297
|
+
await this.writeData(data, {
|
|
298
|
+
repositoryPath: options.repositoryPath,
|
|
299
|
+
step: options.step,
|
|
300
|
+
// Record the primary's owner/repo on the root so later related-repo
|
|
301
|
+
// writes can distinguish it from a section by matching this value.
|
|
302
|
+
repository: options.repo,
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Related repo: read the current file (must already exist — primary is
|
|
307
|
+
// analyzed first), upsert this repo's section, write back.
|
|
308
|
+
const existing = (await this.readFullState());
|
|
309
|
+
const base = existing ??
|
|
310
|
+
{
|
|
311
|
+
metadata: {
|
|
312
|
+
sessionId: this.sessionId,
|
|
313
|
+
stateType: this.stateType,
|
|
314
|
+
createdAt: new Date().toISOString(),
|
|
315
|
+
updatedAt: new Date().toISOString(),
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
const relatedRepos = { ...(base.relatedRepos ?? {}) };
|
|
319
|
+
// Preserve a previously-stored repositoryPath when this write omits it.
|
|
320
|
+
// Callers that update a section without re-supplying the checkout path (e.g.
|
|
321
|
+
// skyramp_execute_test writing execution results) must not clobber the path
|
|
322
|
+
// that skyramp_analyze_changes recorded — getRepoRepositoryPath() relies on
|
|
323
|
+
// it to re-absolutize that repo's test paths.
|
|
324
|
+
const priorRepositoryPath = relatedRepos[options.repo]?.repositoryPath;
|
|
325
|
+
relatedRepos[options.repo] = {
|
|
326
|
+
data,
|
|
327
|
+
repositoryPath: options.repositoryPath ?? priorRepositoryPath,
|
|
328
|
+
};
|
|
329
|
+
base.relatedRepos = relatedRepos;
|
|
330
|
+
base.metadata = { ...base.metadata, updatedAt: new Date().toISOString() };
|
|
331
|
+
await fs.promises.mkdir(path.dirname(this.stateFile), { recursive: true });
|
|
332
|
+
await fs.promises.writeFile(this.stateFile, JSON.stringify(base, null, 2), "utf-8");
|
|
333
|
+
logger.debug(`Wrote ${options.repo} section to state file: ${this.stateFile}`);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Read one repo's analysis payload from the run-scoped state file. `repo`
|
|
337
|
+
* undefined → the primary (root) section (identical to readData()).
|
|
338
|
+
*/
|
|
339
|
+
async readRepoData(repo) {
|
|
340
|
+
if (await this.isPrimaryRepo(repo)) {
|
|
341
|
+
return this.readData();
|
|
342
|
+
}
|
|
343
|
+
const full = await this.readFullState();
|
|
344
|
+
return full?.relatedRepos?.[repo]?.data ?? null;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Resolve the checkout path for a repo's section — used to re-absolutize test
|
|
348
|
+
* paths. `repo` undefined → the primary's metadata.repositoryPath.
|
|
349
|
+
*/
|
|
350
|
+
async getRepoRepositoryPath(repo) {
|
|
351
|
+
const full = await this.readFullState();
|
|
352
|
+
if (await this.isPrimaryRepo(repo)) {
|
|
353
|
+
return full?.metadata.repositoryPath;
|
|
354
|
+
}
|
|
355
|
+
return full?.relatedRepos?.[repo]?.repositoryPath;
|
|
356
|
+
}
|
|
357
|
+
/** List the related repo keys present in the run-scoped file (empty if single-repo). */
|
|
358
|
+
async listRelatedRepos() {
|
|
359
|
+
const full = await this.readFullState();
|
|
360
|
+
return Object.keys(full?.relatedRepos ?? {});
|
|
361
|
+
}
|
|
221
362
|
getStatePath() {
|
|
222
363
|
return this.stateFile;
|
|
223
364
|
}
|
|
@@ -260,7 +401,7 @@ export class StateManager {
|
|
|
260
401
|
* @returns Number of files deleted
|
|
261
402
|
*/
|
|
262
403
|
static async cleanupOldFiles(maxAgeHours = 24, stateDir, stateTypes) {
|
|
263
|
-
const baseDir = stateDir || os.tmpdir();
|
|
404
|
+
const baseDir = stateDir || (process.env.RUNNER_TEMP ? path.join(process.env.RUNNER_TEMP, "skyramp") : os.tmpdir());
|
|
264
405
|
const files = await fs.promises.readdir(baseDir).catch(() => []);
|
|
265
406
|
const statePrefixes = stateTypes
|
|
266
407
|
? stateTypes.map((t) => STATE_FILE_PREFIXES[t])
|
|
@@ -34,3 +34,101 @@ describe("StateManager.writeData", () => {
|
|
|
34
34
|
await fs.promises.rm(nestedDir, { recursive: true, force: true });
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
|
+
describe("StateManager — run-scoped per-repo sections (SKYR-3796)", () => {
|
|
38
|
+
let stateFile;
|
|
39
|
+
const mkState = (tag) => ({
|
|
40
|
+
existingTests: [{ testFile: `${tag}.spec.ts` }],
|
|
41
|
+
analysisScope: AnalysisScope.CurrentBranchDiff,
|
|
42
|
+
newEndpoints: [],
|
|
43
|
+
});
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
stateFile = path.join(os.tmpdir(), `skyramp-analysis-sections-${Date.now()}.json`);
|
|
46
|
+
});
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await fs.promises.rm(stateFile, { force: true });
|
|
49
|
+
});
|
|
50
|
+
it("keeps the primary at root and additional repos in relatedRepos, all in ONE file", async () => {
|
|
51
|
+
const mgr = StateManager.fromStatePath(stateFile);
|
|
52
|
+
// Primary (repo omitted) → root; additional repo → section.
|
|
53
|
+
await mgr.writeRepoData(mkState("primary"), { repositoryPath: "/work/primary" });
|
|
54
|
+
await mgr.writeRepoData(mkState("api"), {
|
|
55
|
+
repo: "letsramp/api-insight",
|
|
56
|
+
repositoryPath: "/work/api-insight",
|
|
57
|
+
});
|
|
58
|
+
// Exactly one file on disk.
|
|
59
|
+
expect(fs.existsSync(stateFile)).toBe(true);
|
|
60
|
+
expect(fs.existsSync(`${stateFile}.1.json`)).toBe(false);
|
|
61
|
+
const raw = JSON.parse(await fs.promises.readFile(stateFile, "utf-8"));
|
|
62
|
+
// Primary analysis lives at root (backward-compatible single-repo shape).
|
|
63
|
+
expect(raw.existingTests[0].testFile).toBe("primary.spec.ts");
|
|
64
|
+
expect(raw.metadata.repositoryPath).toBe("/work/primary");
|
|
65
|
+
// Additional repo under relatedRepos, keyed by owner/repo.
|
|
66
|
+
expect(raw.relatedRepos["letsramp/api-insight"].data.existingTests[0].testFile).toBe("api.spec.ts");
|
|
67
|
+
expect(raw.relatedRepos["letsramp/api-insight"].repositoryPath).toBe("/work/api-insight");
|
|
68
|
+
});
|
|
69
|
+
it("reads back the correct section by repo (primary when omitted)", async () => {
|
|
70
|
+
const mgr = StateManager.fromStatePath(stateFile);
|
|
71
|
+
await mgr.writeRepoData(mkState("primary"), { repositoryPath: "/work/primary" });
|
|
72
|
+
await mgr.writeRepoData(mkState("api"), { repo: "letsramp/api-insight", repositoryPath: "/work/api-insight" });
|
|
73
|
+
expect((await mgr.readRepoData()).existingTests[0].testFile).toBe("primary.spec.ts");
|
|
74
|
+
expect((await mgr.readRepoData("letsramp/api-insight")).existingTests[0].testFile).toBe("api.spec.ts");
|
|
75
|
+
expect(await mgr.getRepoRepositoryPath()).toBe("/work/primary");
|
|
76
|
+
expect(await mgr.getRepoRepositoryPath("letsramp/api-insight")).toBe("/work/api-insight");
|
|
77
|
+
expect(await mgr.listRelatedRepos()).toEqual(["letsramp/api-insight"]);
|
|
78
|
+
});
|
|
79
|
+
it("preserves additional sections when the primary root is rewritten (writeData)", async () => {
|
|
80
|
+
const mgr = StateManager.fromStatePath(stateFile);
|
|
81
|
+
await mgr.writeRepoData(mkState("primary"), { repositoryPath: "/work/primary" });
|
|
82
|
+
await mgr.writeRepoData(mkState("api"), { repo: "letsramp/api-insight", repositoryPath: "/work/api-insight" });
|
|
83
|
+
// execute_test-style root rewrite must not drop the additional section.
|
|
84
|
+
const primary = (await mgr.readRepoData());
|
|
85
|
+
primary.existingTests[0].execution = { marker: "ran" };
|
|
86
|
+
await mgr.writeData(primary);
|
|
87
|
+
expect((await mgr.readRepoData("letsramp/api-insight")).existingTests[0].testFile).toBe("api.spec.ts");
|
|
88
|
+
expect((await mgr.readRepoData()).existingTests[0].execution.marker).toBe("ran");
|
|
89
|
+
});
|
|
90
|
+
it("preserves an additional repo's repositoryPath when a later write omits it", async () => {
|
|
91
|
+
const mgr = StateManager.fromStatePath(stateFile);
|
|
92
|
+
await mgr.writeRepoData(mkState("primary"), { repositoryPath: "/work/primary" });
|
|
93
|
+
// analyze_changes records the section WITH its checkout path.
|
|
94
|
+
await mgr.writeRepoData(mkState("api"), {
|
|
95
|
+
repo: "letsramp/api-insight",
|
|
96
|
+
repositoryPath: "/work/api-insight",
|
|
97
|
+
});
|
|
98
|
+
// A later write to the same section (e.g. execute_test) omits repositoryPath.
|
|
99
|
+
await mgr.writeRepoData(mkState("api-ran"), { repo: "letsramp/api-insight" });
|
|
100
|
+
// The path must survive so getRepoRepositoryPath can still re-absolutize.
|
|
101
|
+
expect(await mgr.getRepoRepositoryPath("letsramp/api-insight")).toBe("/work/api-insight");
|
|
102
|
+
expect((await mgr.readRepoData("letsramp/api-insight")).existingTests[0].testFile).toBe("api-ran.spec.ts");
|
|
103
|
+
});
|
|
104
|
+
it("routes the primary to root and a related repo to a section when BOTH pass an explicit repo", async () => {
|
|
105
|
+
const mgr = StateManager.fromStatePath(stateFile);
|
|
106
|
+
// The agent now always passes an explicit owner/repo, including for the
|
|
107
|
+
// primary. The FIRST write is the primary → root (tagged on metadata).
|
|
108
|
+
await mgr.writeRepoData(mkState("primary"), {
|
|
109
|
+
repo: "letsramp/primary-app",
|
|
110
|
+
repositoryPath: "/work/primary",
|
|
111
|
+
});
|
|
112
|
+
await mgr.writeRepoData(mkState("api"), {
|
|
113
|
+
repo: "letsramp/api-insight",
|
|
114
|
+
repositoryPath: "/work/api-insight",
|
|
115
|
+
});
|
|
116
|
+
const raw = JSON.parse(await fs.promises.readFile(stateFile, "utf-8"));
|
|
117
|
+
expect(raw.metadata.repository).toBe("letsramp/primary-app");
|
|
118
|
+
expect(raw.existingTests[0].testFile).toBe("primary.spec.ts");
|
|
119
|
+
expect(raw.relatedRepos["letsramp/api-insight"].data.existingTests[0].testFile).toBe("api.spec.ts");
|
|
120
|
+
// The primary is reachable BOTH by its explicit repo and by omitting it.
|
|
121
|
+
expect((await mgr.readRepoData("letsramp/primary-app")).existingTests[0].testFile).toBe("primary.spec.ts");
|
|
122
|
+
expect((await mgr.readRepoData()).existingTests[0].testFile).toBe("primary.spec.ts");
|
|
123
|
+
expect(await mgr.getRepoRepositoryPath("letsramp/primary-app")).toBe("/work/primary");
|
|
124
|
+
expect(await mgr.listRelatedRepos()).toEqual(["letsramp/api-insight"]);
|
|
125
|
+
});
|
|
126
|
+
it("reads a legacy single-section file (no relatedRepos) as the primary", async () => {
|
|
127
|
+
const mgr = StateManager.fromStatePath(stateFile);
|
|
128
|
+
// Legacy write path — no repo, no sections.
|
|
129
|
+
await mgr.writeData(mkState("legacy"), { repositoryPath: "/work/legacy" });
|
|
130
|
+
expect((await mgr.readRepoData()).existingTests[0].testFile).toBe("legacy.spec.ts");
|
|
131
|
+
expect(await mgr.getRepoRepositoryPath()).toBe("/work/legacy");
|
|
132
|
+
expect(await mgr.listRelatedRepos()).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -55,7 +55,7 @@ describe("dockerImageExistsLocally", () => {
|
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
describe("pullDockerImage", () => {
|
|
58
|
-
const IMAGE = "skyramp/executor:v1.3.
|
|
58
|
+
const IMAGE = "skyramp/executor:v1.3.29";
|
|
59
59
|
beforeEach(() => jest.clearAllMocks());
|
|
60
60
|
describe("on amd64 host", () => {
|
|
61
61
|
const originalArch = process.arch;
|
|
@@ -50,14 +50,4 @@ export interface ClassifiedEndpoints {
|
|
|
50
50
|
baseBranch: string;
|
|
51
51
|
affectedServices: string[];
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Classify endpoints by cross-referencing `changedFiles` against `scannedEndpoints[].sourceFile`.
|
|
55
|
-
*
|
|
56
|
-
* This replaces `parseEndpointsFromDiff` — instead of regex-parsing diff hunks for route
|
|
57
|
-
* annotations (fragile for 15+ frameworks), it uses the already-scanned endpoint catalog
|
|
58
|
-
* which has full, resolved paths and concrete HTTP methods.
|
|
59
|
-
*
|
|
60
|
-
* For deleted files, the caller must supply endpoints recovered from the base branch
|
|
61
|
-
* (via `parseFileEndpoints` on `git show base:<file>`) in `deletedFileEndpoints`.
|
|
62
|
-
*/
|
|
63
53
|
export declare function classifyEndpointsByChangedFiles(diffData: BranchDiffData, scannedEndpoints: ScannedEndpoint[], deletedFileEndpoints?: ScannedEndpoint[]): ClassifiedEndpoints;
|
|
@@ -406,21 +406,36 @@ const GENERIC_ROUTE_BASENAMES = new Set(["api", "app", "handler", "handlers", "i
|
|
|
406
406
|
function normalizeSourcePath(filePath) {
|
|
407
407
|
return filePath.replace(/\\/g, "/");
|
|
408
408
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
409
|
+
function staticPathSegments(urlPath) {
|
|
410
|
+
return urlPath.split("/").filter(s => s && !/^[:{[]/.test(s)).map(s => s.toLowerCase());
|
|
411
|
+
}
|
|
412
|
+
function endpointSourceResource(ep) {
|
|
413
|
+
const sourceSegments = ep.sourceFile.split(/[\\/]/).filter(Boolean);
|
|
414
|
+
const filename = sourceSegments.at(-1)?.replace(/\.[^.]+$/, "") ?? "";
|
|
415
|
+
const normalizedFilename = filename.replace(/^(?:old|new)[-_]/i, "").toLowerCase();
|
|
416
|
+
const isDynamic = /^\[.*\]$/.test(normalizedFilename) || /^\{.*\}$/.test(normalizedFilename);
|
|
417
|
+
if (!isDynamic && !GENERIC_ROUTE_BASENAMES.has(normalizedFilename) && normalizedFilename) {
|
|
418
|
+
return normalizedFilename;
|
|
419
|
+
}
|
|
420
|
+
else if (GENERIC_ROUTE_BASENAMES.has(normalizedFilename)) {
|
|
421
|
+
// Generic filename: walk up the directory tree for a more specific ancestor name.
|
|
422
|
+
const parentSegments = sourceSegments.slice(0, -1).reverse();
|
|
423
|
+
const contextSegment = parentSegments
|
|
424
|
+
.map(s => s.replace(/\.[^.]+$/, "").replace(/^(?:old|new)[-_]/i, "").toLowerCase())
|
|
425
|
+
.find(s => s && !GENERIC_ROUTE_BASENAMES.has(s) && !/^\[.*\]$/.test(s) && !/^\{.*\}$/.test(s));
|
|
426
|
+
if (contextSegment)
|
|
427
|
+
return `${contextSegment}/${normalizedFilename}`;
|
|
428
|
+
}
|
|
429
|
+
// Fall back to the last static segment of the endpoint's URL path.
|
|
430
|
+
const segments = staticPathSegments(ep.path);
|
|
431
|
+
return segments.at(-1) ?? normalizedFilename;
|
|
432
|
+
}
|
|
433
|
+
function endpointMovedKey(path, resource, method) {
|
|
434
|
+
return method ? `${path}::${resource}::${method}` : `${path}::${resource}`;
|
|
435
|
+
}
|
|
419
436
|
export function classifyEndpointsByChangedFiles(diffData, scannedEndpoints, deletedFileEndpoints) {
|
|
420
437
|
const newFileSet = new Set(diffData.newFiles.map(normalizeSourcePath));
|
|
421
438
|
const deletedFileSet = new Set(diffData.deletedFiles.map(normalizeSourcePath));
|
|
422
|
-
// Build sourceFile -> ScannedEndpoint[] map from the post-change catalog.
|
|
423
|
-
// Git diffs use POSIX separators; scanner output can reflect OS-native paths.
|
|
424
439
|
const bySourceFile = new Map();
|
|
425
440
|
for (const ep of scannedEndpoints) {
|
|
426
441
|
const sourceKey = normalizeSourcePath(ep.sourceFile);
|
|
@@ -435,7 +450,6 @@ export function classifyEndpointsByChangedFiles(diffData, scannedEndpoints, dele
|
|
|
435
450
|
const unmatchedFiles = [];
|
|
436
451
|
for (const file of diffData.changedFiles) {
|
|
437
452
|
const fileKey = normalizeSourcePath(file);
|
|
438
|
-
// Deleted files won't appear in scanned catalog — handled separately below
|
|
439
453
|
if (deletedFileSet.has(fileKey))
|
|
440
454
|
continue;
|
|
441
455
|
const eps = bySourceFile.get(fileKey);
|
|
@@ -454,47 +468,26 @@ export function classifyEndpointsByChangedFiles(diffData, scannedEndpoints, dele
|
|
|
454
468
|
// Filter out endpoints that still exist in the current catalog as moved/refactored.
|
|
455
469
|
// Same router-relative paths can exist in sibling files, so only treat them as moved
|
|
456
470
|
// when the source resource also matches.
|
|
457
|
-
function endpointSourceResource(ep) {
|
|
458
|
-
const sourceSegments = ep.sourceFile.split(/[\\/]/).filter(Boolean);
|
|
459
|
-
const base = sourceSegments.at(-1)?.replace(/\.[^.]+$/, "") ?? "";
|
|
460
|
-
const normalizedBase = base.replace(/^(?:old|new)[-_]/i, "").toLowerCase();
|
|
461
|
-
const isDynamicBase = /^\[.*\]$/.test(normalizedBase) || /^\{.*\}$/.test(normalizedBase);
|
|
462
|
-
if (normalizedBase && !isDynamicBase && !GENERIC_ROUTE_BASENAMES.has(normalizedBase)) {
|
|
463
|
-
return normalizedBase;
|
|
464
|
-
}
|
|
465
|
-
if (normalizedBase && GENERIC_ROUTE_BASENAMES.has(normalizedBase)) {
|
|
466
|
-
const contextSegment = [...sourceSegments]
|
|
467
|
-
.slice(0, -1)
|
|
468
|
-
.reverse()
|
|
469
|
-
.map((segment) => segment.replace(/\.[^.]+$/, "").replace(/^(?:old|new)[-_]/i, "").toLowerCase())
|
|
470
|
-
.find((segment) => segment && !GENERIC_ROUTE_BASENAMES.has(segment) && !/^\[.*\]$/.test(segment) && !/^\{.*\}$/.test(segment));
|
|
471
|
-
if (contextSegment)
|
|
472
|
-
return `${contextSegment}/${normalizedBase}`;
|
|
473
|
-
}
|
|
474
|
-
const staticSegments = ep.path.split("/").filter(Boolean).filter((segment) => !/^[:{[]/.test(segment));
|
|
475
|
-
return staticSegments[staticSegments.length - 1]?.toLowerCase() ?? normalizedBase;
|
|
476
|
-
}
|
|
477
471
|
const currentMethodKeys = new Set();
|
|
478
472
|
const currentPathResourceKeys = new Set();
|
|
479
473
|
const currentMultiKeys = new Set();
|
|
480
|
-
const movedKey = (path, resource, method) => method ? `${path}::${resource}::${method}` : `${path}::${resource}`;
|
|
481
474
|
for (const current of scannedEndpoints) {
|
|
482
475
|
const currentResource = endpointSourceResource(current);
|
|
483
|
-
const pathResourceKey =
|
|
476
|
+
const pathResourceKey = endpointMovedKey(current.path, currentResource);
|
|
484
477
|
currentPathResourceKeys.add(pathResourceKey);
|
|
485
478
|
for (const method of current.methods) {
|
|
486
479
|
if (method === "MULTI")
|
|
487
480
|
currentMultiKeys.add(pathResourceKey);
|
|
488
|
-
currentMethodKeys.add(
|
|
481
|
+
currentMethodKeys.add(endpointMovedKey(current.path, currentResource, method));
|
|
489
482
|
}
|
|
490
483
|
}
|
|
491
484
|
function isMovedEndpoint(deleted, method) {
|
|
492
485
|
const deletedResource = endpointSourceResource(deleted);
|
|
493
|
-
const pathResourceKey =
|
|
486
|
+
const pathResourceKey = endpointMovedKey(deleted.path, deletedResource);
|
|
494
487
|
if (method === "MULTI")
|
|
495
488
|
return currentPathResourceKeys.has(pathResourceKey);
|
|
496
489
|
return currentMultiKeys.has(pathResourceKey) ||
|
|
497
|
-
currentMethodKeys.has(
|
|
490
|
+
currentMethodKeys.has(endpointMovedKey(deleted.path, deletedResource, method));
|
|
498
491
|
}
|
|
499
492
|
const removedEndpoints = (deletedFileEndpoints ?? [])
|
|
500
493
|
.map((ep) => ({
|
|
@@ -41,7 +41,7 @@ export type ResourceRelationships = Map<string, Set<string>>;
|
|
|
41
41
|
* the caller to fall back to heuristic pairing.
|
|
42
42
|
*/
|
|
43
43
|
export declare function inferResourceRelationships(endpoints: Array<EndpointInput>): ResourceRelationships;
|
|
44
|
-
export declare function draftScenariosFromEndpoints(endpoints: Array<EndpointInput>, newEndpoints?: Array<EndpointRef>, wsAuthType?: WorkspaceAuthType, options?: ScenarioDraftOptions): DraftedScenario[];
|
|
44
|
+
export declare function draftScenariosFromEndpoints(endpoints: Array<EndpointInput>, newEndpoints?: Array<EndpointRef>, wsAuthType?: WorkspaceAuthType, options?: ScenarioDraftOptions, removedEndpoints?: Array<EndpointRef>): DraftedScenario[];
|
|
45
45
|
/**
|
|
46
46
|
* Draft scenarios that directly test each new endpoint in the branch diff.
|
|
47
47
|
*
|