@skyramp/mcp 0.1.8 → 0.2.0-rc.2
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 +4 -2
- package/build/playwright/registerPlaywrightTools.js +12 -0
- package/build/playwright/traceRecordingPrompt.js +15 -0
- package/build/prompts/code-reuse.js +106 -7
- package/build/prompts/pom-aware-code-reuse.js +106 -7
- package/build/prompts/startTraceCollectionPrompts.js +37 -15
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
- package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
- package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
- package/build/prompts/test-recommendation/promptPlan.js +290 -0
- package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
- package/build/prompts/test-recommendation/recommendationSections.js +4 -3
- package/build/prompts/test-recommendation/recommendationShared.js +23 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
- package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
- package/build/prompts/testbot/testbot-prompts.js +73 -13
- package/build/prompts/testbot/testbot-prompts.test.js +114 -1
- package/build/resources/testbotResource.js +1 -1
- package/build/services/ScenarioGenerationService.integration.test.js +158 -0
- package/build/services/ScenarioGenerationService.js +47 -4
- package/build/services/ScenarioGenerationService.test.js +158 -22
- package/build/services/TestExecutionService.js +73 -15
- package/build/services/TestExecutionService.test.js +105 -0
- package/build/services/TestGenerationService.js +11 -1
- package/build/tools/executeSkyrampTestTool.js +1 -10
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +2 -0
- package/build/tools/test-management/actionsTool.js +152 -63
- package/build/tools/test-management/analyzeChangesTool.js +178 -64
- package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
- package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
- package/build/tools/trace/resolveSaveStoragePath.js +16 -0
- package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
- package/build/tools/trace/resolveSessionPaths.js +39 -0
- package/build/tools/trace/resolveSessionPaths.test.js +103 -0
- package/build/tools/trace/sessionState.js +14 -0
- package/build/tools/trace/sessionState.test.js +17 -0
- package/build/tools/trace/startTraceCollectionTool.js +84 -14
- package/build/tools/trace/stopTraceCollectionTool.js +9 -2
- package/build/types/TestAnalysis.js +50 -0
- package/build/types/TestRecommendation.js +6 -58
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.js +22 -11
- package/build/utils/branchDiff.js +11 -2
- package/build/utils/docker.test.js +1 -1
- package/build/utils/gitStaging.js +52 -3
- package/build/utils/gitStaging.test.js +19 -1
- package/build/utils/repoScanner.js +18 -10
- package/build/utils/repoScanner.test.js +92 -0
- package/build/utils/routeParsers.js +180 -25
- package/build/utils/routeParsers.test.js +180 -1
- package/build/utils/scenarioDrafting.js +220 -17
- package/build/utils/scenarioDrafting.test.js +182 -9
- package/build/utils/sourceRouteExtractor.js +806 -0
- package/build/utils/sourceRouteExtractor.test.js +565 -0
- package/build/utils/uiPageEnumerator.js +319 -0
- package/build/utils/uiPageEnumerator.test.js +422 -0
- package/build/utils/utils.js +27 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.js +33 -4
- package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
- package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
- package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
- package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
- package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
- package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
- package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
- package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
- package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
- package/node_modules/playwright/package.json +1 -1
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
- package/package.json +3 -3
- package/build/services/TestHealthService.js +0 -694
- package/build/services/TestHealthService.test.js +0 -241
- package/build/types/TestDriftAnalysis.js +0 -1
- package/build/types/TestHealth.js +0 -4
|
@@ -203,6 +203,8 @@ describe("uiCredentials in getTestbotPrompt", () => {
|
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
describe("drift analysis inline embedding", () => {
|
|
206
|
+
beforeAll(() => { process.env.SKYRAMP_FEATURE_TESTBOT = "1"; });
|
|
207
|
+
afterAll(() => { delete process.env.SKYRAMP_FEATURE_TESTBOT; });
|
|
206
208
|
function basePrompt() {
|
|
207
209
|
return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
208
210
|
}
|
|
@@ -226,11 +228,60 @@ describe("drift analysis inline embedding", () => {
|
|
|
226
228
|
expect(rulesPos).toBeGreaterThan(task1Pos);
|
|
227
229
|
expect(rulesPos).toBeLessThan(task2Pos);
|
|
228
230
|
});
|
|
229
|
-
it("Task 1 step
|
|
231
|
+
it("Task 1 step 3 prose references drift_analysis_rules tag", () => {
|
|
230
232
|
const prompt = basePrompt();
|
|
231
233
|
expect(prompt).toContain("rules in `<drift_analysis_rules>`");
|
|
232
234
|
});
|
|
233
235
|
});
|
|
236
|
+
describe("UI grounding via Task 2 capture-act-capture", () => {
|
|
237
|
+
it("surfaces uiContext as guidance, not a contract", () => {
|
|
238
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
239
|
+
// uiContext fields are explained inline so the agent knows what to do with
|
|
240
|
+
// them. Step 1 provides candidate URLs but gives fallback instructions
|
|
241
|
+
// ("navigate from the workspace baseUrl and explore") for 404s/redirects,
|
|
242
|
+
// treating candidates as guidance not a rigid contract.
|
|
243
|
+
expect(prompt).toContain("uiContext");
|
|
244
|
+
expect(prompt).toContain("candidateUiPages");
|
|
245
|
+
expect(prompt).toContain("changedFrontendFiles");
|
|
246
|
+
expect(prompt).toMatch(/navigate from the workspace baseUrl and explore/i);
|
|
247
|
+
});
|
|
248
|
+
it("step 5 enforces Blueprint Citation Invariant in natural prose", () => {
|
|
249
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
250
|
+
// Step 5 is the citation-invariant guardrail, not a "fill in tuples"
|
|
251
|
+
// post-processing step (slice 4 cleanup: recs are grounded upstream).
|
|
252
|
+
expect(prompt).toContain("Blueprint Citation Invariant");
|
|
253
|
+
// Reasoning must be natural prose, NOT internal-identifier syntax.
|
|
254
|
+
expect(prompt).toMatch(/natural prose/i);
|
|
255
|
+
expect(prompt).toMatch(/internal-identifier syntax/i);
|
|
256
|
+
});
|
|
257
|
+
it("Task 2 no longer instructs the agent to fill in tuples post-hoc", () => {
|
|
258
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
259
|
+
// After slice 4 cleanup: Task 2 captures are for trace recording's own
|
|
260
|
+
// assertions, not for retroactively rewriting recommendation reasoning.
|
|
261
|
+
// The phrase "fill in tuples" must NOT appear anywhere in the prompt.
|
|
262
|
+
expect(prompt).not.toMatch(/fill in tuples/i);
|
|
263
|
+
expect(prompt).not.toMatch(/return to step 5 and fill/i);
|
|
264
|
+
});
|
|
265
|
+
it("Task 2 step 5 mentions possibleAssertions as available, NOT as required (slice 5.5 softening)", () => {
|
|
266
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
267
|
+
// Slice 5: AFTER-action browser_blueprint response includes
|
|
268
|
+
// possibleAssertions[] — mechanically translated candidates.
|
|
269
|
+
expect(prompt).toContain("possibleAssertions");
|
|
270
|
+
// Slice 5.5: prompt explicitly tells the agent NOT to feel obligated.
|
|
271
|
+
// Two P09 runs with the prior "emit at least one" directive showed the
|
|
272
|
+
// agent over-using shallow visibility assertions at the expense of
|
|
273
|
+
// integration-test depth. The softened version says: read them, use
|
|
274
|
+
// when they happen to match what you'd write anyway, ignore otherwise.
|
|
275
|
+
expect(prompt).toMatch(/do not feel obligated/i);
|
|
276
|
+
expect(prompt).toMatch(/biased toward visibility/i);
|
|
277
|
+
// The candidate format is still documented.
|
|
278
|
+
expect(prompt).toMatch(/\bcode\b.*\brationale\b.*\btier\b/i);
|
|
279
|
+
// The pre-existing "at least one browser_assert per page navigated"
|
|
280
|
+
// rule should be preserved (it's about meaningful business-outcome
|
|
281
|
+
// assertions, not about possibleAssertions).
|
|
282
|
+
expect(prompt).toMatch(/at least one .browser_assert. per page navigated/i);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
234
285
|
describe("buildWorkspaceRecoveryPrefix", () => {
|
|
235
286
|
const { buildWorkspaceRecoveryPrefix } = require("./testbot-prompts.js");
|
|
236
287
|
it("includes repositoryPath in both init_scan and init_workspace instructions", () => {
|
|
@@ -248,3 +299,65 @@ describe("buildWorkspaceRecoveryPrefix", () => {
|
|
|
248
299
|
expect(prefix).toMatch(/^IMPORTANT:/);
|
|
249
300
|
});
|
|
250
301
|
});
|
|
302
|
+
describe("testsRepoDir in getTestbotPrompt", () => {
|
|
303
|
+
function callWithTestsRepoDir(testsRepoDir) {
|
|
304
|
+
return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
|
|
305
|
+
undefined, // maxRecommendations
|
|
306
|
+
undefined, // maxGenerate
|
|
307
|
+
undefined, // maxCritical
|
|
308
|
+
undefined, // prNumber
|
|
309
|
+
undefined, // userPrompt
|
|
310
|
+
undefined, // services
|
|
311
|
+
undefined, // stateOutputFile
|
|
312
|
+
undefined, // uiCredentials
|
|
313
|
+
testsRepoDir);
|
|
314
|
+
}
|
|
315
|
+
function callFollowUpWithTestsRepoDir(testsRepoDir) {
|
|
316
|
+
return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
|
|
317
|
+
undefined, // maxRecommendations
|
|
318
|
+
undefined, // maxGenerate
|
|
319
|
+
undefined, // maxCritical
|
|
320
|
+
undefined, // prNumber
|
|
321
|
+
"add more tests", // userPrompt — triggers follow-up path
|
|
322
|
+
undefined, // services
|
|
323
|
+
undefined, // stateOutputFile
|
|
324
|
+
undefined, // uiCredentials
|
|
325
|
+
testsRepoDir);
|
|
326
|
+
}
|
|
327
|
+
it("includes testsRepoDir in skyramp_analyze_changes call for first-run prompt", () => {
|
|
328
|
+
const dir = "/home/runner/work/_temp/skyramp/test-repo";
|
|
329
|
+
const prompt = callWithTestsRepoDir(dir);
|
|
330
|
+
expect(prompt).toContain(`\`testsRepoDir\`: "${dir}"`);
|
|
331
|
+
});
|
|
332
|
+
it("includes testsRepoDir in skyramp_analyze_changes call for follow-up prompt", () => {
|
|
333
|
+
const dir = "/home/runner/work/_temp/skyramp/test-repo";
|
|
334
|
+
const prompt = callFollowUpWithTestsRepoDir(dir);
|
|
335
|
+
expect(prompt).toContain(`\`testsRepoDir\`: "${dir}"`);
|
|
336
|
+
});
|
|
337
|
+
it("omits testsRepoDir when not provided", () => {
|
|
338
|
+
const prompt = callWithTestsRepoDir(undefined);
|
|
339
|
+
expect(prompt).not.toContain("testsRepoDir");
|
|
340
|
+
});
|
|
341
|
+
it("omits testsRepoDir from follow-up prompt when not provided", () => {
|
|
342
|
+
const prompt = callFollowUpWithTestsRepoDir(undefined);
|
|
343
|
+
expect(prompt).not.toContain("testsRepoDir");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
describe("testbot prompt blueprint-grounded recommendations (slice 4)", () => {
|
|
347
|
+
it("instructs the agent to call skyramp_ui_analyze_changes before skyramp_analyze_changes", () => {
|
|
348
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
349
|
+
const uiacIdx = prompt.indexOf("skyramp_ui_analyze_changes");
|
|
350
|
+
const acIdx = prompt.indexOf("skyramp_analyze_changes");
|
|
351
|
+
expect(uiacIdx).toBeGreaterThan(-1);
|
|
352
|
+
expect(acIdx).toBeGreaterThan(uiacIdx);
|
|
353
|
+
});
|
|
354
|
+
it("Task 1 step 1 instructs the agent to capture blueprints (without threading them through a param)", () => {
|
|
355
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
356
|
+
// Captures stay in tool-result history; analyze_changes returns the
|
|
357
|
+
// authoring rules and the agent supplies the captured vocabulary.
|
|
358
|
+
expect(prompt).toMatch(/browser_blueprint`?\s*to capture/i);
|
|
359
|
+
expect(prompt).toMatch(/tool-result history/i);
|
|
360
|
+
// Make sure we removed the old capturedBlueprints threading directive.
|
|
361
|
+
expect(prompt).not.toMatch(/capturedBlueprints/);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
@@ -25,7 +25,7 @@ export function registerTestbotResource(server) {
|
|
|
25
25
|
const maxCrit = parseInt(uri.searchParams.get("maxCritical") || "", 10);
|
|
26
26
|
const repositoryPath = param("repositoryPath", ".");
|
|
27
27
|
const services = await readWorkspaceServices(repositoryPath);
|
|
28
|
-
const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("stateOutputFile") || undefined, uri.searchParams.get("uiCredentials") || undefined);
|
|
28
|
+
const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("stateOutputFile") || undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined);
|
|
29
29
|
AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
|
|
30
30
|
// Return the original URI — clients may use it to re-fetch the resource,
|
|
31
31
|
// and the caller already has these params. Credentials never appear in
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: verifies query params survive the full pipeline
|
|
3
|
+
* TS (ScenarioGenerationService) → JSON file → Go binary → generated test code
|
|
4
|
+
*
|
|
5
|
+
* Requires: @skyramp/skyramp native dylib (skips if not available)
|
|
6
|
+
*/
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import { ScenarioGenerationService } from "./ScenarioGenerationService.js";
|
|
11
|
+
let SkyrampClient;
|
|
12
|
+
try {
|
|
13
|
+
SkyrampClient = require("@skyramp/skyramp").SkyrampClient;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
SkyrampClient = null;
|
|
17
|
+
}
|
|
18
|
+
// These tests require the native Go binary which is platform-specific.
|
|
19
|
+
// Skip in CI or when the binary isn't available for this OS/arch.
|
|
20
|
+
const isCI = Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
|
|
21
|
+
const describeIfBinary = SkyrampClient && !isCI ? describe : describe.skip;
|
|
22
|
+
function buildTrace(overrides) {
|
|
23
|
+
const service = new ScenarioGenerationService();
|
|
24
|
+
return service.generateTraceRequestFromInput({
|
|
25
|
+
scenarioName: "integration-qp-test",
|
|
26
|
+
destination: "localhost",
|
|
27
|
+
baseURL: "http://localhost:8080",
|
|
28
|
+
method: "GET",
|
|
29
|
+
path: "/api/v1/items",
|
|
30
|
+
outputDir: os.tmpdir(),
|
|
31
|
+
authHeader: "",
|
|
32
|
+
authScheme: "",
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
describeIfBinary("QueryParams integration: TS → JSON → Go binary → generated code", () => {
|
|
37
|
+
let tmpDir;
|
|
38
|
+
let outputDir;
|
|
39
|
+
let client;
|
|
40
|
+
// Go dylib cold-starts can take a few seconds
|
|
41
|
+
jest.setTimeout(15000);
|
|
42
|
+
beforeAll(() => {
|
|
43
|
+
client = new SkyrampClient();
|
|
44
|
+
});
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
client?.close?.();
|
|
47
|
+
});
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-qp-integration-"));
|
|
50
|
+
outputDir = path.join(tmpDir, "output");
|
|
51
|
+
fs.mkdirSync(outputDir);
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
async function generateFromTrace(trace) {
|
|
57
|
+
const traceFile = path.join(tmpDir, "scenario.json");
|
|
58
|
+
fs.writeFileSync(traceFile, JSON.stringify([trace], null, 2));
|
|
59
|
+
await client.generateRestTest({
|
|
60
|
+
testType: "integration",
|
|
61
|
+
language: "python",
|
|
62
|
+
framework: "pytest",
|
|
63
|
+
outputDir,
|
|
64
|
+
force: true,
|
|
65
|
+
traceFilePath: traceFile,
|
|
66
|
+
});
|
|
67
|
+
const files = fs.readdirSync(outputDir).filter((f) => f.endsWith(".py"));
|
|
68
|
+
if (files.length === 0)
|
|
69
|
+
throw new Error("No generated test file found");
|
|
70
|
+
return fs.readFileSync(path.join(outputDir, files[0]), "utf8");
|
|
71
|
+
}
|
|
72
|
+
it("bracket-notation keys pass through to generated test code", async () => {
|
|
73
|
+
// Simulates: GET /api/v1/items?filter[status][_eq]=published&limit=25
|
|
74
|
+
const trace = buildTrace({
|
|
75
|
+
queryParams: '{"filter[status][_eq]":"published","limit":25}',
|
|
76
|
+
});
|
|
77
|
+
expect(trace).not.toBeNull();
|
|
78
|
+
expect(trace.QueryParams).toEqual({
|
|
79
|
+
"filter[status][_eq]": ["published"],
|
|
80
|
+
"limit": ["25"],
|
|
81
|
+
});
|
|
82
|
+
const code = await generateFromTrace(trace);
|
|
83
|
+
expect(code).toContain("filter[status][_eq]");
|
|
84
|
+
expect(code).toContain("published");
|
|
85
|
+
expect(code).toContain("limit");
|
|
86
|
+
expect(code).toContain("25");
|
|
87
|
+
});
|
|
88
|
+
it("simple flat params pass through to generated test code", async () => {
|
|
89
|
+
// Simulates: GET /api/v1/items?page=2&status=active
|
|
90
|
+
const trace = buildTrace({
|
|
91
|
+
queryParams: '{"page":2,"status":"active"}',
|
|
92
|
+
});
|
|
93
|
+
expect(trace).not.toBeNull();
|
|
94
|
+
expect(trace.QueryParams).toEqual({
|
|
95
|
+
page: ["2"],
|
|
96
|
+
status: ["active"],
|
|
97
|
+
});
|
|
98
|
+
const code = await generateFromTrace(trace);
|
|
99
|
+
expect(code).toContain("page");
|
|
100
|
+
expect(code).toContain("2");
|
|
101
|
+
expect(code).toContain("status");
|
|
102
|
+
expect(code).toContain("active");
|
|
103
|
+
});
|
|
104
|
+
it("JSON-encoded string value (Elasticsearch-style) passes through intact", async () => {
|
|
105
|
+
// Simulates: GET /search?source={"query":{"match":{"name":"bear"}}}
|
|
106
|
+
const trace = buildTrace({
|
|
107
|
+
queryParams: '{"source":"{\\"query\\":{\\"match\\":{\\"name\\":\\"bear\\"}}}"}',
|
|
108
|
+
});
|
|
109
|
+
expect(trace).not.toBeNull();
|
|
110
|
+
expect(trace.QueryParams).toEqual({
|
|
111
|
+
source: ['{"query":{"match":{"name":"bear"}}}'],
|
|
112
|
+
});
|
|
113
|
+
const code = await generateFromTrace(trace);
|
|
114
|
+
expect(code).toContain("source");
|
|
115
|
+
expect(code).toMatch(/query.*match.*name.*bear/);
|
|
116
|
+
});
|
|
117
|
+
it("array params are comma-joined into a single value in generated code", async () => {
|
|
118
|
+
// Simulates: GET /products?tags=sale,new
|
|
119
|
+
const trace = buildTrace({
|
|
120
|
+
queryParams: '{"tags":["sale","new"]}',
|
|
121
|
+
});
|
|
122
|
+
expect(trace).not.toBeNull();
|
|
123
|
+
expect(trace.QueryParams).toEqual({
|
|
124
|
+
tags: ["sale,new"],
|
|
125
|
+
});
|
|
126
|
+
const code = await generateFromTrace(trace);
|
|
127
|
+
expect(code).toContain("tags");
|
|
128
|
+
expect(code).toContain("sale,new");
|
|
129
|
+
});
|
|
130
|
+
it("null params are omitted — not present in generated code", async () => {
|
|
131
|
+
// null means "don't include this param" — same as axios/fetch omitting undefined
|
|
132
|
+
const trace = buildTrace({
|
|
133
|
+
queryParams: '{"limit":10,"cursor":null}',
|
|
134
|
+
});
|
|
135
|
+
expect(trace).not.toBeNull();
|
|
136
|
+
expect(trace.QueryParams).toEqual({
|
|
137
|
+
limit: ["10"],
|
|
138
|
+
});
|
|
139
|
+
const code = await generateFromTrace(trace);
|
|
140
|
+
expect(code).toContain("limit");
|
|
141
|
+
expect(code).toContain("10");
|
|
142
|
+
expect(code).not.toContain("cursor");
|
|
143
|
+
});
|
|
144
|
+
it("nested object fallback produces valid JSON string in generated code", async () => {
|
|
145
|
+
// LLM mistakenly sends nested object — service JSON-stringifies it
|
|
146
|
+
const trace = buildTrace({
|
|
147
|
+
queryParams: '{"filter":{"status":"active"}}',
|
|
148
|
+
});
|
|
149
|
+
expect(trace).not.toBeNull();
|
|
150
|
+
expect(trace.QueryParams).toEqual({
|
|
151
|
+
filter: ['{"status":"active"}'],
|
|
152
|
+
});
|
|
153
|
+
const code = await generateFromTrace(trace);
|
|
154
|
+
expect(code).toContain("filter");
|
|
155
|
+
expect(code).not.toContain("[object Object]");
|
|
156
|
+
expect(code).not.toContain("map[");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -2,9 +2,14 @@ import { AUTH_PLACEHOLDER_TOKEN } from "../types/TestTypes.js";
|
|
|
2
2
|
import { isAuthorizationHeaderName } from "../utils/workspaceAuth.js";
|
|
3
3
|
import { inferExpectedStatus } from "../utils/httpDefaults.js";
|
|
4
4
|
import { logger } from "../utils/logger.js";
|
|
5
|
-
import { stageGeneratedPaths } from "../utils/gitStaging.js";
|
|
5
|
+
import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
|
|
6
|
+
import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
|
|
6
7
|
import fs from "fs";
|
|
7
8
|
import path from "path";
|
|
9
|
+
// Keys that trigger built-in prototype setters when used as bracket-notation
|
|
10
|
+
// property names on a plain object — guard against prototype pollution from
|
|
11
|
+
// LLM-controlled or user-controlled JSON input.
|
|
12
|
+
const PROTO_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
8
13
|
export class ScenarioGenerationService {
|
|
9
14
|
async parseScenario(params) {
|
|
10
15
|
try {
|
|
@@ -23,6 +28,15 @@ export class ScenarioGenerationService {
|
|
|
23
28
|
isError: true,
|
|
24
29
|
};
|
|
25
30
|
}
|
|
31
|
+
// In cross-repo mode, redirect outputDir to the test repo clone.
|
|
32
|
+
const resolved = resolveOutputDir(params.outputDir, getTestsRepoDir());
|
|
33
|
+
if (resolved !== params.outputDir) {
|
|
34
|
+
logger.info("Cross-repo: redirecting scenario outputDir to test repo", {
|
|
35
|
+
original: params.outputDir,
|
|
36
|
+
redirected: resolved,
|
|
37
|
+
});
|
|
38
|
+
params.outputDir = resolved;
|
|
39
|
+
}
|
|
26
40
|
const scenarioName = params.scenarioName.replace(/ /g, "-").toLowerCase();
|
|
27
41
|
const fileName = `scenario_${scenarioName}.json`;
|
|
28
42
|
const filePath = path.join(params.outputDir, fileName);
|
|
@@ -40,6 +54,12 @@ export class ScenarioGenerationService {
|
|
|
40
54
|
existingRequests = [];
|
|
41
55
|
}
|
|
42
56
|
}
|
|
57
|
+
if (existingRequests.length > 0) {
|
|
58
|
+
const lastTimestamp = existingRequests[existingRequests.length - 1].Timestamp;
|
|
59
|
+
const lastMs = new Date(lastTimestamp).getTime();
|
|
60
|
+
const newMs = Math.max(lastMs + 1000, Date.now());
|
|
61
|
+
traceRequest.Timestamp = new Date(newMs).toISOString();
|
|
62
|
+
}
|
|
43
63
|
existingRequests.push(traceRequest);
|
|
44
64
|
fs.writeFileSync(filePath, JSON.stringify(existingRequests, null, 2), "utf8");
|
|
45
65
|
// Stage so testbot includes the generated files in its output commit.
|
|
@@ -139,15 +159,38 @@ ${JSON.stringify(traceRequest, null, 2)}
|
|
|
139
159
|
const requestHeaders = {
|
|
140
160
|
"Content-Type": ["application/json"],
|
|
141
161
|
};
|
|
162
|
+
// Go backend expects url.Values (map[string][]string). We always produce
|
|
163
|
+
// single-element arrays — the Go codegen normalises multi-element string arrays
|
|
164
|
+
// into a JSON array string (e.g. ["sale","new"]), which is not what most APIs
|
|
165
|
+
// expect. Comma-joining covers the common case; APIs needing true repeated keys
|
|
166
|
+
// (e.g. ?tags=sale&tags=new) are not supported by the current Go codegen path.
|
|
142
167
|
const queryParams = {};
|
|
143
168
|
if (params.queryParams) {
|
|
144
169
|
try {
|
|
145
170
|
const parsed = JSON.parse(params.queryParams);
|
|
146
171
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
147
172
|
for (const [k, v] of Object.entries(parsed)) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
173
|
+
if (PROTO_KEYS.has(k))
|
|
174
|
+
continue;
|
|
175
|
+
if (v === null)
|
|
176
|
+
continue;
|
|
177
|
+
let value;
|
|
178
|
+
if (Array.isArray(v)) {
|
|
179
|
+
const items = v
|
|
180
|
+
.filter((item) => item !== null)
|
|
181
|
+
.map((item) => (typeof item === "object" ? JSON.stringify(item) : String(item)));
|
|
182
|
+
if (items.length === 0)
|
|
183
|
+
continue;
|
|
184
|
+
value = items.join(",");
|
|
185
|
+
}
|
|
186
|
+
else if (typeof v === "object") {
|
|
187
|
+
logger.warning("Nested object in queryParams — JSON-stringifying as fallback; LLM should provide flat keys", { key: k });
|
|
188
|
+
value = JSON.stringify(v);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
value = String(v);
|
|
192
|
+
}
|
|
193
|
+
queryParams[k] = [value];
|
|
151
194
|
}
|
|
152
195
|
}
|
|
153
196
|
else {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
1
4
|
import { ScenarioGenerationService } from "./ScenarioGenerationService.js";
|
|
2
5
|
import { AUTH_PLACEHOLDER_TOKEN } from "../types/TestTypes.js";
|
|
3
6
|
const BASE_PARAMS = {
|
|
@@ -197,39 +200,145 @@ describe("ScenarioGenerationService — auth header flavors", () => {
|
|
|
197
200
|
});
|
|
198
201
|
});
|
|
199
202
|
describe("ScenarioGenerationService — queryParams handling", () => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
// --- Primitive values (strings, numbers, booleans) ---
|
|
204
|
+
it("coerces string values into single-element arrays", () => {
|
|
205
|
+
const trace = generateTrace({ queryParams: '{"status":"active"}' });
|
|
206
|
+
expect(trace.QueryParams).toEqual({ status: ["active"] });
|
|
203
207
|
});
|
|
204
|
-
it("
|
|
205
|
-
const trace = generateTrace({ queryParams: '{"
|
|
206
|
-
expect(trace.QueryParams).toEqual({
|
|
208
|
+
it("coerces numeric values to string", () => {
|
|
209
|
+
const trace = generateTrace({ queryParams: '{"limit":10,"offset":0}' });
|
|
210
|
+
expect(trace.QueryParams).toEqual({ limit: ["10"], offset: ["0"] });
|
|
207
211
|
});
|
|
208
|
-
it("
|
|
209
|
-
const trace = generateTrace({ queryParams: '{"
|
|
210
|
-
expect(trace).
|
|
211
|
-
const filterVal = trace.QueryParams["filter"][0];
|
|
212
|
-
expect(filterVal).not.toBe("[object Object]");
|
|
213
|
-
expect(filterVal).toBe('{"status":"active","min_price":10}');
|
|
212
|
+
it("coerces boolean values to string", () => {
|
|
213
|
+
const trace = generateTrace({ queryParams: '{"active":true,"deleted":false}' });
|
|
214
|
+
expect(trace.QueryParams).toEqual({ active: ["true"], deleted: ["false"] });
|
|
214
215
|
});
|
|
215
|
-
it("
|
|
216
|
-
const trace = generateTrace({ queryParams: '{"
|
|
217
|
-
expect(trace).
|
|
218
|
-
|
|
216
|
+
it("coerces empty string to single-element array", () => {
|
|
217
|
+
const trace = generateTrace({ queryParams: '{"q":""}' });
|
|
218
|
+
expect(trace.QueryParams).toEqual({ q: [""] });
|
|
219
|
+
});
|
|
220
|
+
it("passes bracket-notation keys through verbatim (Directus regression)", () => {
|
|
221
|
+
// Root cause of the original P0: filter[title][_neq] must reach the Go backend
|
|
222
|
+
// as the literal key string, not as a nested object or [object Object].
|
|
223
|
+
const trace = generateTrace({
|
|
224
|
+
queryParams: '{"filter[status][_eq]":"published","filter[date_created][_gte]":"2024-01-01","limit":25}',
|
|
225
|
+
});
|
|
226
|
+
expect(trace.QueryParams["filter[status][_eq]"]).toEqual(["published"]);
|
|
227
|
+
expect(trace.QueryParams["filter[date_created][_gte]"]).toEqual(["2024-01-01"]);
|
|
228
|
+
expect(trace.QueryParams["limit"]).toEqual(["25"]);
|
|
229
|
+
});
|
|
230
|
+
// --- Null omission ---
|
|
231
|
+
it("omits null values entirely (prevents ?status=null in URL)", () => {
|
|
232
|
+
// null means "omit this param" — same convention as axios/fetch omitting undefined params
|
|
233
|
+
const trace = generateTrace({ queryParams: '{"cursor":null,"limit":20}' });
|
|
234
|
+
expect(trace.QueryParams["cursor"]).toBeUndefined();
|
|
235
|
+
expect(trace.QueryParams["limit"]).toEqual(["20"]);
|
|
236
|
+
});
|
|
237
|
+
it("omits all-null object leaving only non-null keys", () => {
|
|
238
|
+
const trace = generateTrace({ queryParams: '{"a":null,"b":null,"c":"keep"}' });
|
|
239
|
+
expect(trace.QueryParams).toEqual({ c: ["keep"] });
|
|
240
|
+
});
|
|
241
|
+
// --- Array values: filter nulls, comma-join into single element ---
|
|
242
|
+
it("array of strings joins into single comma-separated value", () => {
|
|
243
|
+
const trace = generateTrace({ queryParams: '{"tags":["sale","new","featured"]}' });
|
|
244
|
+
expect(trace.QueryParams["tags"]).toEqual(["sale,new,featured"]);
|
|
245
|
+
});
|
|
246
|
+
it("array of mixed primitives coerces each to string then joins", () => {
|
|
247
|
+
const trace = generateTrace({ queryParams: '{"ids":[1,2,"three",true,0,false]}' });
|
|
248
|
+
expect(trace.QueryParams["ids"]).toEqual(["1,2,three,true,0,false"]);
|
|
249
|
+
});
|
|
250
|
+
it("array with null items filters nulls before joining", () => {
|
|
251
|
+
const trace = generateTrace({ queryParams: '{"tags":[null,"active",null,"pending"]}' });
|
|
252
|
+
expect(trace.QueryParams["tags"]).toEqual(["active,pending"]);
|
|
253
|
+
});
|
|
254
|
+
it("array with all-null items omits the key entirely", () => {
|
|
255
|
+
const trace = generateTrace({ queryParams: '{"tags":[null,null],"limit":5}' });
|
|
256
|
+
expect(trace.QueryParams["tags"]).toBeUndefined();
|
|
257
|
+
expect(trace.QueryParams["limit"]).toEqual(["5"]);
|
|
219
258
|
});
|
|
220
|
-
it("
|
|
221
|
-
const trace = generateTrace({ queryParams: '{"
|
|
222
|
-
expect(trace.QueryParams["
|
|
259
|
+
it("empty array omits the key (no items after filtering)", () => {
|
|
260
|
+
const trace = generateTrace({ queryParams: '{"ids":[],"limit":10}' });
|
|
261
|
+
expect(trace.QueryParams["ids"]).toBeUndefined();
|
|
262
|
+
expect(trace.QueryParams["limit"]).toEqual(["10"]);
|
|
223
263
|
});
|
|
224
|
-
it("
|
|
264
|
+
it("array containing objects JSON-stringifies each then joins", () => {
|
|
265
|
+
const trace = generateTrace({
|
|
266
|
+
queryParams: '{"filters":[{"field":"status","value":"active"},{"field":"price","op":"gte","value":"10"}]}',
|
|
267
|
+
});
|
|
268
|
+
expect(trace.QueryParams["filters"]).toEqual([
|
|
269
|
+
'{"field":"status","value":"active"},{"field":"price","op":"gte","value":"10"}',
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
it("array mixing objects, primitives, and nulls: filters nulls, stringifies objects, joins all", () => {
|
|
273
|
+
const trace = generateTrace({
|
|
274
|
+
queryParams: '{"mixed":[null,{"nested":true},"plain",42,null]}',
|
|
275
|
+
});
|
|
276
|
+
expect(trace.QueryParams["mixed"]).toEqual(['{"nested":true},plain,42']);
|
|
277
|
+
});
|
|
278
|
+
// --- Nested object fallback: JSON.stringify ---
|
|
279
|
+
it("nested object is JSON-stringified as fallback", () => {
|
|
280
|
+
const trace = generateTrace({
|
|
281
|
+
queryParams: '{"filter":{"status":"active","min_price":10}}',
|
|
282
|
+
});
|
|
283
|
+
expect(trace.QueryParams["filter"]).toEqual(['{"status":"active","min_price":10}']);
|
|
284
|
+
});
|
|
285
|
+
it("deeply nested object is fully JSON-stringified", () => {
|
|
286
|
+
const trace = generateTrace({
|
|
287
|
+
queryParams: '{"filter":{"title":{"_neq":"not-a-number"}}}',
|
|
288
|
+
});
|
|
289
|
+
expect(trace.QueryParams["filter"]).toEqual(['{"title":{"_neq":"not-a-number"}}']);
|
|
290
|
+
});
|
|
291
|
+
it("empty nested object is JSON-stringified to '{}'", () => {
|
|
292
|
+
const trace = generateTrace({ queryParams: '{"filter":{}}' });
|
|
293
|
+
expect(trace.QueryParams["filter"]).toEqual(["{}"]);
|
|
294
|
+
});
|
|
295
|
+
// --- JSON parsing and object validation ---
|
|
296
|
+
it("omitted queryParams produces empty QueryParams", () => {
|
|
225
297
|
const trace = generateTrace({});
|
|
226
298
|
expect(trace.QueryParams).toEqual({});
|
|
227
299
|
});
|
|
228
|
-
it("produces empty QueryParams
|
|
229
|
-
const trace = generateTrace({ queryParams: "not-valid-json" });
|
|
300
|
+
it("invalid JSON produces empty QueryParams without throwing", () => {
|
|
301
|
+
const trace = generateTrace({ queryParams: "not-valid-json{" });
|
|
230
302
|
expect(trace).not.toBeNull();
|
|
231
303
|
expect(trace.QueryParams).toEqual({});
|
|
232
304
|
});
|
|
305
|
+
it("JSON array (not object) produces empty QueryParams", () => {
|
|
306
|
+
const trace = generateTrace({ queryParams: '["limit","10"]' });
|
|
307
|
+
expect(trace.QueryParams).toEqual({});
|
|
308
|
+
});
|
|
309
|
+
it("JSON null produces empty QueryParams", () => {
|
|
310
|
+
const trace = generateTrace({ queryParams: "null" });
|
|
311
|
+
expect(trace.QueryParams).toEqual({});
|
|
312
|
+
});
|
|
313
|
+
it("JSON number produces empty QueryParams", () => {
|
|
314
|
+
const trace = generateTrace({ queryParams: "42" });
|
|
315
|
+
expect(trace.QueryParams).toEqual({});
|
|
316
|
+
});
|
|
317
|
+
it("JSON string produces empty QueryParams", () => {
|
|
318
|
+
const trace = generateTrace({ queryParams: '"just a string"' });
|
|
319
|
+
expect(trace.QueryParams).toEqual({});
|
|
320
|
+
});
|
|
321
|
+
it("empty object produces empty QueryParams", () => {
|
|
322
|
+
const trace = generateTrace({ queryParams: '{}' });
|
|
323
|
+
expect(trace.QueryParams).toEqual({});
|
|
324
|
+
});
|
|
325
|
+
// --- Security: PROTO_KEYS guard ---
|
|
326
|
+
it("skips __proto__ key to prevent prototype pollution via setter", () => {
|
|
327
|
+
const trace = generateTrace({
|
|
328
|
+
queryParams: '{"__proto__":{"polluted":true},"limit":10}',
|
|
329
|
+
});
|
|
330
|
+
expect(Object.hasOwn(trace.QueryParams, "__proto__")).toBe(false);
|
|
331
|
+
expect(Object.getPrototypeOf(trace.QueryParams)).toBe(Object.prototype);
|
|
332
|
+
expect(trace.QueryParams["limit"]).toEqual(["10"]);
|
|
333
|
+
});
|
|
334
|
+
it("skips constructor and prototype keys", () => {
|
|
335
|
+
const trace = generateTrace({
|
|
336
|
+
queryParams: '{"constructor":"pwned","prototype":"evil","valid":"yes"}',
|
|
337
|
+
});
|
|
338
|
+
expect(Object.hasOwn(trace.QueryParams, "constructor")).toBe(false);
|
|
339
|
+
expect(Object.hasOwn(trace.QueryParams, "prototype")).toBe(false);
|
|
340
|
+
expect(trace.QueryParams["valid"]).toEqual(["yes"]);
|
|
341
|
+
});
|
|
233
342
|
});
|
|
234
343
|
describe("ScenarioGenerationService — baseURL parsing", () => {
|
|
235
344
|
it("parses http baseURL correctly", () => {
|
|
@@ -262,3 +371,30 @@ describe("ScenarioGenerationService — baseURL parsing", () => {
|
|
|
262
371
|
expect(trace.Port).toBe(80);
|
|
263
372
|
});
|
|
264
373
|
});
|
|
374
|
+
describe("ScenarioGenerationService — timestamp chaining", () => {
|
|
375
|
+
it("assigns strictly increasing timestamps across multiple parseScenario calls", async () => {
|
|
376
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-ts-test-"));
|
|
377
|
+
const service = new ScenarioGenerationService();
|
|
378
|
+
const baseParams = {
|
|
379
|
+
scenarioName: "timestamp-chain-test",
|
|
380
|
+
destination: "api.example.com",
|
|
381
|
+
method: "GET",
|
|
382
|
+
path: "/api/v1/items",
|
|
383
|
+
outputDir: tmpDir,
|
|
384
|
+
baseURL: "http://localhost:8000",
|
|
385
|
+
};
|
|
386
|
+
await service.parseScenario({ ...baseParams, method: "POST", path: "/api/v1/items" });
|
|
387
|
+
await service.parseScenario({ ...baseParams, method: "GET", path: "/api/v1/items/1" });
|
|
388
|
+
await service.parseScenario({ ...baseParams, method: "DELETE", path: "/api/v1/items/1" });
|
|
389
|
+
const fileName = "scenario_timestamp-chain-test.json";
|
|
390
|
+
const filePath = path.join(tmpDir, fileName);
|
|
391
|
+
const requests = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
392
|
+
expect(requests).toHaveLength(3);
|
|
393
|
+
for (let i = 1; i < requests.length; i++) {
|
|
394
|
+
const prev = new Date(requests[i - 1].Timestamp).getTime();
|
|
395
|
+
const curr = new Date(requests[i].Timestamp).getTime();
|
|
396
|
+
expect(curr).toBeGreaterThan(prev);
|
|
397
|
+
}
|
|
398
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
399
|
+
});
|
|
400
|
+
});
|