@skyramp/mcp 0.1.5 → 0.1.6
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 +6 -5
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -7
- package/build/prompts/personas.js +2 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +28 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +72 -14
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -0
- package/build/prompts/test-recommendation/recommendationSections.js +4 -2
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +20 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -8
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -6
- package/build/prompts/testbot/testbot-prompts.js +7 -5
- package/build/prompts/testbot/testbot-prompts.test.js +2 -2
- package/build/resources/analysisResources.js +1 -0
- package/build/services/ScenarioGenerationService.js +2 -1
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +123 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +205 -9
- package/build/tools/generate-tests/generateContractRestTool.js +19 -19
- package/build/tools/generate-tests/generateIntegrationRestTool.js +9 -2
- package/build/tools/generate-tests/generateUIRestTool.js +23 -8
- package/build/tools/test-management/analyzeChangesTool.js +218 -2
- package/build/tools/test-management/analyzeChangesTool.test.js +233 -1
- package/build/utils/featureFlags.js +7 -0
- package/build/utils/featureFlags.test.js +81 -0
- package/build/utils/httpDefaults.js +17 -0
- package/build/utils/httpDefaults.test.js +21 -0
- package/build/utils/scenarioDrafting.js +37 -15
- package/build/utils/scenarioDrafting.test.js +66 -0
- package/build/utils/telemetry.js +2 -1
- package/build/utils/utils.js +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { resolveServiceDetailsRef } from "./utils.js";
|
|
2
|
+
import { isTestbotEnabled } from "./featureFlags.js";
|
|
3
|
+
// Capture and restore the original env var so these tests don't pollute other
|
|
4
|
+
// test suites or a developer's shell environment where the var may already be set.
|
|
5
|
+
let originalTestbotEnv;
|
|
6
|
+
beforeAll(() => { originalTestbotEnv = process.env.SKYRAMP_FEATURE_TESTBOT; });
|
|
7
|
+
afterAll(() => {
|
|
8
|
+
if (originalTestbotEnv === undefined) {
|
|
9
|
+
delete process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
process.env.SKYRAMP_FEATURE_TESTBOT = originalTestbotEnv;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
describe("isTestbotEnabled", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
delete process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
18
|
+
});
|
|
19
|
+
it('returns true when SKYRAMP_FEATURE_TESTBOT is "1"', () => {
|
|
20
|
+
process.env.SKYRAMP_FEATURE_TESTBOT = "1";
|
|
21
|
+
expect(isTestbotEnabled()).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("returns false when SKYRAMP_FEATURE_TESTBOT is unset", () => {
|
|
24
|
+
expect(isTestbotEnabled()).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('returns false when SKYRAMP_FEATURE_TESTBOT is "0"', () => {
|
|
27
|
+
process.env.SKYRAMP_FEATURE_TESTBOT = "0";
|
|
28
|
+
expect(isTestbotEnabled()).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it('returns false when SKYRAMP_FEATURE_TESTBOT is "true"', () => {
|
|
31
|
+
process.env.SKYRAMP_FEATURE_TESTBOT = "true";
|
|
32
|
+
expect(isTestbotEnabled()).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("resolveServiceDetailsRef", () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
delete process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
38
|
+
});
|
|
39
|
+
describe("testbot mode (SKYRAMP_FEATURE_TESTBOT=1)", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
process.env.SKYRAMP_FEATURE_TESTBOT = "1";
|
|
42
|
+
});
|
|
43
|
+
it("returns <services> block reference for testDirRef", () => {
|
|
44
|
+
expect(resolveServiceDetailsRef().testDirRef).toContain("<output_dir>");
|
|
45
|
+
expect(resolveServiceDetailsRef().testDirRef).toContain("<services>");
|
|
46
|
+
});
|
|
47
|
+
it("returns <services> block reference for frontendTestDirRef", () => {
|
|
48
|
+
expect(resolveServiceDetailsRef().frontendTestDirRef).toContain("<output_dir>");
|
|
49
|
+
expect(resolveServiceDetailsRef().frontendTestDirRef).toContain("<services>");
|
|
50
|
+
});
|
|
51
|
+
it("returns <services> block reference for authSourceRef", () => {
|
|
52
|
+
expect(resolveServiceDetailsRef().authSourceRef).toContain("<services>");
|
|
53
|
+
});
|
|
54
|
+
it("returns <services> block reference for baseUrlRef", () => {
|
|
55
|
+
expect(resolveServiceDetailsRef().baseUrlRef).toContain("<base_url>");
|
|
56
|
+
});
|
|
57
|
+
it("does NOT reference workspace.yml in any field", () => {
|
|
58
|
+
const refs = resolveServiceDetailsRef();
|
|
59
|
+
for (const val of Object.values(refs)) {
|
|
60
|
+
expect(val).not.toContain("workspace.yml");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("MCP mode (SKYRAMP_FEATURE_TESTBOT unset)", () => {
|
|
65
|
+
it("returns workspace.yml reference for testDirRef", () => {
|
|
66
|
+
expect(resolveServiceDetailsRef().testDirRef).toContain("workspace.yml");
|
|
67
|
+
});
|
|
68
|
+
it("returns workspace.yml reference for frontendTestDirRef", () => {
|
|
69
|
+
expect(resolveServiceDetailsRef().frontendTestDirRef).toContain("workspace.yml");
|
|
70
|
+
});
|
|
71
|
+
it("returns workspace.yml reference for authSourceRef", () => {
|
|
72
|
+
expect(resolveServiceDetailsRef().authSourceRef).toContain("workspace.yml");
|
|
73
|
+
});
|
|
74
|
+
it("does NOT reference <services> in any field", () => {
|
|
75
|
+
const refs = resolveServiceDetailsRef();
|
|
76
|
+
for (const val of Object.values(refs)) {
|
|
77
|
+
expect(val).not.toContain("<services>");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP method defaults used across scenario drafting and test generation.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Returns the conventional success status code for a given HTTP method.
|
|
6
|
+
* POST → 201 Created
|
|
7
|
+
* DELETE → 204 No Content
|
|
8
|
+
* other → 200 OK
|
|
9
|
+
*/
|
|
10
|
+
export function inferExpectedStatus(method) {
|
|
11
|
+
const m = method.toUpperCase();
|
|
12
|
+
if (m === "POST")
|
|
13
|
+
return 201;
|
|
14
|
+
if (m === "DELETE")
|
|
15
|
+
return 204;
|
|
16
|
+
return 200;
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { inferExpectedStatus } from "./httpDefaults.js";
|
|
2
|
+
describe("inferExpectedStatus", () => {
|
|
3
|
+
it("returns 201 for POST", () => {
|
|
4
|
+
expect(inferExpectedStatus("POST")).toBe(201);
|
|
5
|
+
expect(inferExpectedStatus("post")).toBe(201);
|
|
6
|
+
});
|
|
7
|
+
it("returns 204 for DELETE", () => {
|
|
8
|
+
expect(inferExpectedStatus("DELETE")).toBe(204);
|
|
9
|
+
expect(inferExpectedStatus("delete")).toBe(204);
|
|
10
|
+
});
|
|
11
|
+
it("returns 200 for GET", () => {
|
|
12
|
+
expect(inferExpectedStatus("GET")).toBe(200);
|
|
13
|
+
expect(inferExpectedStatus("get")).toBe(200);
|
|
14
|
+
});
|
|
15
|
+
it("returns 200 for PUT", () => {
|
|
16
|
+
expect(inferExpectedStatus("PUT")).toBe(200);
|
|
17
|
+
});
|
|
18
|
+
it("returns 200 for PATCH", () => {
|
|
19
|
+
expect(inferExpectedStatus("PATCH")).toBe(200);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { ScenarioSource } from "../types/RepositoryAnalysis.js";
|
|
2
2
|
import { TestType } from "../types/TestTypes.js";
|
|
3
3
|
import { CATEGORY_PRIORITY } from "../types/TestRecommendation.js";
|
|
4
|
-
|
|
5
|
-
* Cap on drafted scenarios per category to prevent prompt bloat on large APIs.
|
|
6
|
-
* Value of 4 balances coverage breadth (enough to surface patterns across
|
|
7
|
-
* resources) with token budget (each scenario adds ~15-20 lines to the prompt).
|
|
8
|
-
*/
|
|
9
|
-
const MAX_DRAFTED_SCENARIOS_PER_CATEGORY = 4;
|
|
4
|
+
import { inferExpectedStatus } from "./httpDefaults.js";
|
|
10
5
|
const ACTION_PATTERN = /^(me|merge|archive|search|login|logout|verify|forgot|reset|config|dashboard|webhook|migration|favicon|payment|health|status|ping|metrics|callback|confirm|activate|deactivate|subscribe|unsubscribe|unknown)$/i;
|
|
11
6
|
const ACTION_VERB_HYPHEN = /^(forgot-|reset-|verify-|confirm-|send-|check-|get-|set-|update-|delete-|create-|trigger-|start-|stop-)/i;
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when `a` starts with `b` at a path-segment boundary.
|
|
9
|
+
* Plain `startsWith` is not sufficient — "/orders-archive".startsWith("/orders") is true
|
|
10
|
+
* even though they are different resources. Requiring the next character to be "/" or
|
|
11
|
+
* end-of-string ensures only genuine path prefixes are matched.
|
|
12
|
+
*/
|
|
13
|
+
const isSegmentPrefix = (a, b) => a.startsWith(b) && (a[b.length] === "/" || a[b.length] === undefined);
|
|
12
14
|
export function isRealResource(r) {
|
|
13
15
|
return !ACTION_PATTERN.test(r) && !ACTION_VERB_HYPHEN.test(r);
|
|
14
16
|
}
|
|
@@ -142,13 +144,27 @@ export function draftScenariosFromEndpoints(endpoints, newEndpoints = []) {
|
|
|
142
144
|
}
|
|
143
145
|
const existing = resourceGroups.get(resource);
|
|
144
146
|
if (existing) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
147
|
+
// Only merge if the paths are related at a segment boundary.
|
|
148
|
+
const related = isSegmentPrefix(basePath, existing.basePath) || isSegmentPrefix(existing.basePath, basePath);
|
|
149
|
+
if (related) {
|
|
150
|
+
for (const m of ep.methods)
|
|
151
|
+
existing.methods.add(typeof m === "string" ? m : m.method);
|
|
152
|
+
if (basePath.split("/").length < existing.basePath.split("/").length) {
|
|
153
|
+
existing.basePath = basePath;
|
|
154
|
+
}
|
|
155
|
+
if (paramPath && (!existing.paramPath || paramPath.split("/").length < existing.paramPath.split("/").length)) {
|
|
156
|
+
existing.paramPath = paramPath;
|
|
157
|
+
}
|
|
149
158
|
}
|
|
150
|
-
|
|
151
|
-
|
|
159
|
+
else {
|
|
160
|
+
// Use the raw basePath as the disambiguating suffix — replacing "/" with "_" could
|
|
161
|
+
// produce collisions between e.g. /orders/search and /orders_search.
|
|
162
|
+
const disambiguatedKey = `${resource}::${basePath}`;
|
|
163
|
+
resourceGroups.set(disambiguatedKey, {
|
|
164
|
+
basePath,
|
|
165
|
+
methods: new Set(ep.methods.map((m) => typeof m === "string" ? m : m.method)),
|
|
166
|
+
paramPath,
|
|
167
|
+
});
|
|
152
168
|
}
|
|
153
169
|
}
|
|
154
170
|
else {
|
|
@@ -264,7 +280,7 @@ function diffDirectIntegration(method, resource, singular, group) {
|
|
|
264
280
|
};
|
|
265
281
|
}
|
|
266
282
|
function diffDirectContract(method, path, resource) {
|
|
267
|
-
const expectedStatus = method
|
|
283
|
+
const expectedStatus = inferExpectedStatus(method);
|
|
268
284
|
return {
|
|
269
285
|
scenarioName: `${resource}-${method.toLowerCase()}-new-endpoint-contract`,
|
|
270
286
|
description: `Contract: ${method} ${path} returns a schema-conformant response`,
|
|
@@ -520,6 +536,9 @@ export function draftDiffDirectScenarios(newEndpoints, resourceGroups) {
|
|
|
520
536
|
for (const [epPath, methods] of grouped) {
|
|
521
537
|
let resource = extractResourceName(epPath);
|
|
522
538
|
let resolvedPath = epPath;
|
|
539
|
+
// The map key to use for the final resourceGroups lookup — may differ from `resource`
|
|
540
|
+
// when the entry was stored under a disambiguated "resource::basePath" key.
|
|
541
|
+
let resourceGroupKey = resource;
|
|
523
542
|
// When resource was found but epPath may be router-relative (e.g. "/orders"
|
|
524
543
|
// instead of "/api/v1/orders"), try to upgrade resolvedPath to the full path.
|
|
525
544
|
if (resource) {
|
|
@@ -553,11 +572,14 @@ export function draftDiffDirectScenarios(newEndpoints, resourceGroups) {
|
|
|
553
572
|
}
|
|
554
573
|
if (!bestMatch)
|
|
555
574
|
continue;
|
|
556
|
-
resource
|
|
575
|
+
// Keep the full map key for lookup — disambiguated keys use "resource::basePath" format.
|
|
576
|
+
// Use only the prefix before "::" as the resource name for scenario naming/singularization.
|
|
577
|
+
resourceGroupKey = bestMatch.name;
|
|
578
|
+
resource = resourceGroupKey.includes("::") ? resourceGroupKey.split("::")[0] : resourceGroupKey;
|
|
557
579
|
// For prefix matches (action sub-paths), keep original path; for suffix matches, use resolved path
|
|
558
580
|
resolvedPath = bestMatch.isPrefixMatch ? epPath : bestMatch.path;
|
|
559
581
|
}
|
|
560
|
-
const group = resourceGroups.get(
|
|
582
|
+
const group = resourceGroups.get(resourceGroupKey);
|
|
561
583
|
if (!group)
|
|
562
584
|
continue;
|
|
563
585
|
const singular = singularize(resource);
|
|
@@ -577,3 +577,69 @@ describe("draftScenariosFromEndpoints — capScenarios hard limit", () => {
|
|
|
577
577
|
expect(result.length).toBeGreaterThanOrEqual(4);
|
|
578
578
|
});
|
|
579
579
|
});
|
|
580
|
+
describe("path collision fix — resourceGroups disambiguation", () => {
|
|
581
|
+
it("unrelated paths sharing a last segment do not overwrite each other", () => {
|
|
582
|
+
// /api/v1/orders/search and /api/v1/products/search share 'search' as last segment.
|
|
583
|
+
// Before the fix, the second overwrote the first in resourceGroups.
|
|
584
|
+
// The scenario must reference the orders/search path specifically, not products/search.
|
|
585
|
+
const endpoints = [
|
|
586
|
+
{ path: "/api/v1/orders/search", methods: ["GET"] },
|
|
587
|
+
{ path: "/api/v1/products/search", methods: ["GET"] },
|
|
588
|
+
];
|
|
589
|
+
const newEndpoints = [{ method: "GET", path: "/api/v1/orders/search" }];
|
|
590
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
591
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
592
|
+
const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
|
|
593
|
+
// Must contain orders/search
|
|
594
|
+
expect(allPaths.some((p) => p.includes("orders/search"))).toBe(true);
|
|
595
|
+
// Must NOT contain products/search — wrong path from a collision would appear here
|
|
596
|
+
expect(allPaths.some((p) => p.includes("products/search"))).toBe(false);
|
|
597
|
+
});
|
|
598
|
+
it("related paths (nested resources) still merge correctly", () => {
|
|
599
|
+
// /api/v1/users and /api/v1/users/{id} are related — should still merge
|
|
600
|
+
const endpoints = [
|
|
601
|
+
{ path: "/api/v1/users", methods: ["GET", "POST"] },
|
|
602
|
+
{ path: "/api/v1/users/{id}", methods: ["GET", "PUT", "DELETE"] },
|
|
603
|
+
];
|
|
604
|
+
const newEndpoints = [{ method: "POST", path: "/api/v1/users" }];
|
|
605
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
606
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
607
|
+
});
|
|
608
|
+
it("new endpoint under a disambiguated group is not silently dropped", () => {
|
|
609
|
+
// When two paths share a last segment, both are stored under disambiguated keys
|
|
610
|
+
// (e.g. "search::/api/v1/orders/search"). The fallback in draftDiffDirectScenarios
|
|
611
|
+
// must use the full key for resourceGroups.get(), not the stripped "search" name
|
|
612
|
+
// (which has no entry), otherwise the scenario is silently skipped.
|
|
613
|
+
const endpoints = [
|
|
614
|
+
{ path: "/api/v1/orders/search", methods: ["GET"] },
|
|
615
|
+
{ path: "/api/v1/products/search", methods: ["GET"] },
|
|
616
|
+
{ path: "/api/v1/orders", methods: ["GET", "POST"] },
|
|
617
|
+
{ path: "/api/v1/products", methods: ["GET", "POST"] },
|
|
618
|
+
];
|
|
619
|
+
const newEndpoints = [
|
|
620
|
+
{ method: "GET", path: "/api/v1/orders/search" },
|
|
621
|
+
{ method: "GET", path: "/api/v1/products/search" },
|
|
622
|
+
];
|
|
623
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
624
|
+
// Both new endpoints must produce at least one scenario each — neither should be dropped
|
|
625
|
+
const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
|
|
626
|
+
expect(allPaths.some((p) => p.includes("orders/search") || p.includes("orders"))).toBe(true);
|
|
627
|
+
expect(allPaths.some((p) => p.includes("products/search") || p.includes("products"))).toBe(true);
|
|
628
|
+
});
|
|
629
|
+
it("paths sharing a string prefix but different resource names are not merged", () => {
|
|
630
|
+
// /api/v1/orders and /api/v1/orders-archive start with the same string
|
|
631
|
+
// but are different resources. startsWith alone would incorrectly merge them.
|
|
632
|
+
const endpoints = [
|
|
633
|
+
{ path: "/api/v1/orders", methods: ["GET", "POST"] },
|
|
634
|
+
{ path: "/api/v1/orders-archive", methods: ["GET"] },
|
|
635
|
+
];
|
|
636
|
+
const newEndpoints = [{ method: "POST", path: "/api/v1/orders" }];
|
|
637
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
638
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
639
|
+
const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
|
|
640
|
+
// orders scenario must be present
|
|
641
|
+
expect(allPaths.some((p) => p.includes("/orders"))).toBe(true);
|
|
642
|
+
// orders-archive must not appear in scenarios targeting /orders
|
|
643
|
+
expect(allPaths.some((p) => p.includes("orders-archive"))).toBe(false);
|
|
644
|
+
});
|
|
645
|
+
});
|
package/build/utils/telemetry.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* "testbot" when running as part of the GitHub Action test bot,
|
|
4
4
|
* "mcp" for regular IDE/MCP usage.
|
|
5
5
|
*/
|
|
6
|
+
import { isTestbotEnabled } from "./featureFlags.js";
|
|
6
7
|
export function getEntryPoint() {
|
|
7
|
-
return
|
|
8
|
+
return isTestbotEnabled() ? "testbot" : "mcp";
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
11
|
* Detects the CI/CD platform from environment variables.
|
package/build/utils/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import { isTestbotEnabled } from "./featureFlags.js";
|
|
2
3
|
import { logger } from "./logger.js";
|
|
3
4
|
export const OUTPUT_DIR_FIELD_NAME = "outputDir";
|
|
4
5
|
export const TRACE_OUTPUT_FILE_FIELD_NAME = "traceOutputFile";
|
|
@@ -134,3 +135,25 @@ export function generateSkyrampHeader(language) {
|
|
|
134
135
|
const commentPrefix = COMMENT_PREFIX_MAP[language] || "#";
|
|
135
136
|
return `${commentPrefix} ${SKYRAMP_UTILS_HEADER} on ${timestamp} ${timezone}\n\n`;
|
|
136
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Returns prompt phrasing for where the agent should find service details.
|
|
140
|
+
*
|
|
141
|
+
* - Testbot mode: agent receives a `<services>` XML block — no workspace.yml exists.
|
|
142
|
+
* - Normal MCP mode: agent reads `.skyramp/workspace.yml`.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveServiceDetailsRef() {
|
|
145
|
+
if (isTestbotEnabled()) {
|
|
146
|
+
return {
|
|
147
|
+
testDirRef: "the `<output_dir>` from the `<services>` block",
|
|
148
|
+
frontendTestDirRef: "the **frontend** service's `<output_dir>` from the `<services>` block",
|
|
149
|
+
baseUrlRef: "the `<base_url>` from the `<services>` block",
|
|
150
|
+
authSourceRef: "the `<services>` block",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
testDirRef: "the `testDirectory` from `.skyramp/workspace.yml`",
|
|
155
|
+
frontendTestDirRef: "the **frontend** service's `testDirectory` from `.skyramp/workspace.yml`",
|
|
156
|
+
baseUrlRef: "the `api.baseUrl` from `.skyramp/workspace.yml`",
|
|
157
|
+
authSourceRef: "`.skyramp/workspace.yml`",
|
|
158
|
+
};
|
|
159
|
+
}
|