@skyramp/mcp 0.3.6-rc.1 → 0.3.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/prompts/initialize-workspace/initializeWorkspacePrompt.js +2 -1
- package/build/services/TestGenerationService.js +13 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +25 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +10 -0
- package/build/tools/workspace/initializeWorkspaceTool.js +99 -27
- package/build/utils/executorWorkDir.d.ts +36 -0
- package/build/utils/executorWorkDir.js +77 -0
- package/package.json +1 -1
|
@@ -91,6 +91,7 @@ Create one service entry per deployable unit. You MUST include every backend/API
|
|
|
91
91
|
- Single service: set testDirectory to tests/skyramp.
|
|
92
92
|
- Multiple services or monorepos: set testDirectory to tests/skyramp/<serviceDirName>, where <serviceDirName> is the service directory name with path separators and whitespace replaced by hyphens.
|
|
93
93
|
Framework config takes precedence. Use the Skyramp deterministic fallback only when no framework-configured test directory is available.
|
|
94
|
+
NEVER set testDirectory to \`.skyramp\`, to any path inside it, or to \`.\` or the repo root, whatever the framework config or existing test files say. \`.skyramp\` is the executor's own working area: it holds the run videos and executor artefacts. skyramp_init_workspace rejects all of these, and the generation tools refuse to write there.
|
|
94
95
|
</basic_fields>
|
|
95
96
|
|
|
96
97
|
### API fields
|
|
@@ -160,7 +161,7 @@ Before calling skyramp_init_workspace, confirm all of the following:
|
|
|
160
161
|
4. Every service has api.baseUrl set to a valid, discoverable URL. Use localhost for local services or the actual deployment URL for cloud or external services. Never fabricate a URL.
|
|
161
162
|
5. Every service with authType apiKey has authHeader explicitly set to the actual custom header name (such as "X-API-Key" or "X-Admin-Key"). If you cannot find the header name in the source code, env vars, or README, do NOT use authType apiKey. Use authType none instead and add a YAML comment explaining auth is unresolved.
|
|
162
163
|
6. framework matches language (python uses pytest or robot, typescript or javascript uses playwright, java uses junit).
|
|
163
|
-
7. testDirectory follows the stable resolution rules above: framework config file when present (Playwright testDir in playwright.config.ts, pytest testpaths in pytest.ini or pyproject.toml, JUnit test source dir in pom.xml or build.gradle); otherwise the deterministic default (tests/skyramp for a single service, tests/skyramp/<serviceDirName> for multiple services).
|
|
164
|
+
7. testDirectory follows the stable resolution rules above: framework config file when present (Playwright testDir in playwright.config.ts, pytest testpaths in pytest.ini or pyproject.toml, JUnit test source dir in pom.xml or build.gradle); otherwise the deterministic default (tests/skyramp for a single service, tests/skyramp/<serviceDirName> for multiple services). No service uses \`.skyramp\`, \`.\` or the repo root.
|
|
164
165
|
8. If serverStartCommand is provided, it matches the runtime. If serverStopCommand is provided, runtime is "docker" and the command is a Docker command.
|
|
165
166
|
9. For services in docker-compose.yml: runtime MUST be "docker" and the command MUST be a docker command such as "docker compose up -d --build <service-name>" when the service has a build context. Always include it since it is derivable from the service name.
|
|
166
167
|
10. NEVER use application-level commands (uvicorn, npm, node, python, java, etc.) with runtime "docker".
|
|
@@ -11,6 +11,7 @@ import { TestType } from "../types/TestTypes.js";
|
|
|
11
11
|
import { logger } from "../utils/logger.js";
|
|
12
12
|
import { normalizeLanguageParams } from "../utils/normalizeParams.js";
|
|
13
13
|
import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
|
|
14
|
+
import { isInsideExecutorWorkDir, executorWorkDirRefusal, generationTargets, } from "../utils/executorWorkDir.js";
|
|
14
15
|
import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
|
|
15
16
|
import { recordReuseHandOff } from "../tools/code-refactor/reuse-state.js";
|
|
16
17
|
import { isModularizeFirstTarget } from "../utils/reuseRouting.js";
|
|
@@ -73,6 +74,18 @@ export class TestGenerationService {
|
|
|
73
74
|
});
|
|
74
75
|
params.outputDir = resolved;
|
|
75
76
|
}
|
|
77
|
+
// Every generation tool reaches the filesystem through this method, so one
|
|
78
|
+
// check here covers them all. Nothing written under `.skyramp` is delivered
|
|
79
|
+
// as a test — an observed run put 7 specs there because the only absolute
|
|
80
|
+
// path the prompt gave it was the trace-zip directory. The error names the
|
|
81
|
+
// field and the replacement so the agent can correct itself in one round trip.
|
|
82
|
+
const badTarget = generationTargets(params.outputDir, params.output).find(isInsideExecutorWorkDir);
|
|
83
|
+
if (badTarget) {
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: executorWorkDirRefusal(badTarget) }],
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
76
89
|
// Log prompt parameter using reusable utility
|
|
77
90
|
logger.info("Generating test", {
|
|
78
91
|
prompt: params.prompt,
|
|
@@ -10,6 +10,7 @@ import { getRestMockMethodValidationError, normalizeRestMockMethod, } from "../.
|
|
|
10
10
|
import { getGrpcMockResponseValidationError } from "../../utils/grpcMockValidation.js";
|
|
11
11
|
import { validateMockCompatibility } from "../../utils/mockCompatibility.js";
|
|
12
12
|
import { buildTrafficConfigOptions, validateTrafficConfig, } from "./generateMockRestTool.js";
|
|
13
|
+
import { isInsideExecutorWorkDir, executorWorkDirRefusal, generationTargets, } from "../../utils/executorWorkDir.js";
|
|
13
14
|
export const BATCH_MOCK_PROTOCOL_EXAMPLES = `
|
|
14
15
|
<example protocol="rest">
|
|
15
16
|
Batch REST item. Required fields: protocol="rest", endpointURL, method unless apiSchema is provided.
|
|
@@ -361,6 +362,15 @@ function buildGenerateOptions(spec, shared) {
|
|
|
361
362
|
}
|
|
362
363
|
export async function executeBatchMockGeneration(params) {
|
|
363
364
|
const { mocks, outputDir } = params;
|
|
365
|
+
// This tool calls the client directly rather than through
|
|
366
|
+
// TestGenerationService, so it carries its own working-area check. mkdirSync
|
|
367
|
+
// below would otherwise create the directory before anything is validated.
|
|
368
|
+
if (isInsideExecutorWorkDir(outputDir)) {
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text: executorWorkDirRefusal(outputDir) }],
|
|
371
|
+
isError: true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
364
374
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
365
375
|
const client = new SkyrampClient();
|
|
366
376
|
const results = [];
|
|
@@ -402,6 +412,21 @@ export async function executeBatchMockGeneration(params) {
|
|
|
402
412
|
continue;
|
|
403
413
|
}
|
|
404
414
|
const resolvedOutputDir = spec.outputDir ?? outputDir;
|
|
415
|
+
// A per-mock outputDir or output name can point back into the working area
|
|
416
|
+
// even when the batch-level one does not.
|
|
417
|
+
const badTarget = generationTargets(resolvedOutputDir, spec.output).find(isInsideExecutorWorkDir);
|
|
418
|
+
if (badTarget) {
|
|
419
|
+
results.push({
|
|
420
|
+
index: i,
|
|
421
|
+
protocol,
|
|
422
|
+
endpointURL: spec.endpointURL,
|
|
423
|
+
method,
|
|
424
|
+
topic: spec.kafkaTopic,
|
|
425
|
+
success: false,
|
|
426
|
+
error: executorWorkDirRefusal(badTarget),
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
405
430
|
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
406
431
|
// Build options and generate
|
|
407
432
|
const options = buildGenerateOptions(spec, {
|
|
@@ -7,6 +7,7 @@ import { isAuthorizationHeaderName } from "../utils/workspaceAuth.js";
|
|
|
7
7
|
import { WORKER_CONTROL_PORT } from "../utils/versions.js";
|
|
8
8
|
import * as fs from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
|
+
import { isInsideExecutorWorkDir, executorWorkDirRefusal, generationTargets, } from "../utils/executorWorkDir.js";
|
|
10
11
|
const TOOL_NAME = "skyramp_generate_enriched_integration_test";
|
|
11
12
|
const enrichedIntegrationTestSchema = {
|
|
12
13
|
scenarioFile: z
|
|
@@ -132,6 +133,15 @@ export async function executeGenerateEnrichedIntegrationTest(params) {
|
|
|
132
133
|
if (resolvedAuthType !== undefined && isAuthorizationHeaderName(resolvedAuthHeader || "")) {
|
|
133
134
|
resolvedAuthHeader = undefined;
|
|
134
135
|
}
|
|
136
|
+
// This tool calls the client directly instead of going through
|
|
137
|
+
// TestGenerationService, so it needs its own copy of the working-area check.
|
|
138
|
+
const badTarget = generationTargets(outputDir, output).find(isInsideExecutorWorkDir);
|
|
139
|
+
if (badTarget) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: executorWorkDirRefusal(badTarget) }],
|
|
142
|
+
isError: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
135
145
|
const client = new SkyrampClient();
|
|
136
146
|
const result = await client.generateRestTest({
|
|
137
147
|
traceFilePath: scenarioFile,
|
|
@@ -1,12 +1,68 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { WorkspaceConfigManager, serviceSchema, } from "../../workspace/workspace.js";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
|
+
import fsSync from "fs";
|
|
5
|
+
import path from "path";
|
|
4
6
|
import yaml from "js-yaml";
|
|
5
7
|
import { logger } from "../../utils/logger.js";
|
|
6
8
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
7
9
|
import { SKYRAMP_IMAGE_VERSION } from "../../utils/versions.js";
|
|
8
10
|
import { validateAndConsumeScanToken } from "./initScanWorkspaceTool.js";
|
|
9
11
|
import { upsertServicesByRepo } from "./serviceUpsert.js";
|
|
12
|
+
import { isInsideDir } from "../../utils/gitStaging.js";
|
|
13
|
+
import { EXECUTOR_VIDEOS_DIR, EXECUTOR_WORK_DIR, isInsideExecutorWorkDir, } from "../../utils/executorWorkDir.js";
|
|
14
|
+
// Test delivery stages each service's whole testDirectory with
|
|
15
|
+
// `git add -- <testDirectory>`, so a testDirectory that encloses the executor's
|
|
16
|
+
// videos commits every .webm. This tool runs before any test is generated, so it
|
|
17
|
+
// is the only point where the choice can still be redirected.
|
|
18
|
+
/**
|
|
19
|
+
* First service whose `testDirectory` collides with the executor's working area,
|
|
20
|
+
* or undefined when none does. Two ways to collide, and both have to be rejected
|
|
21
|
+
* here: the directory sits inside `.skyramp` (which the generation tools refuse
|
|
22
|
+
* to write into, so accepting it would deadlock the agent between the two
|
|
23
|
+
* checks), or it encloses `.skyramp/videos` (`.`, "" or the repo root), which
|
|
24
|
+
* makes delivery commit every video. Relative paths resolve against
|
|
25
|
+
* `workspacePath`; an absolute one is matched on its own segments, so another
|
|
26
|
+
* repo's `.skyramp` is caught too.
|
|
27
|
+
*
|
|
28
|
+
* A directory that is itself a git repository root is rejected for the same
|
|
29
|
+
* reason, whichever repo it belongs to. The enclosure test above is anchored on
|
|
30
|
+
* the PRIMARY workspace's videos path, so a multi-repo entry naming another
|
|
31
|
+
* repo's root — `/work/repo-b` while workspacePath is `/work/repo-a` — slips
|
|
32
|
+
* past it: repo A's videos are not inside repo B. The executor writes
|
|
33
|
+
* `.skyramp/videos` at a repo root, so being one is the collision.
|
|
34
|
+
*/
|
|
35
|
+
function badTestDirectoryResult(bad) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: `Service "${bad.serviceName}" sets testDirectory to "${bad.testDirectory}", which collides with ${EXECUTOR_WORK_DIR}, the executor's working area: a directory inside it is never delivered, and one that encloses ${EXECUTOR_VIDEOS_DIR}/ makes delivery commit every execution video. Use a dedicated test path — the service's framework-configured test directory, or tests/skyramp. If this service came from the existing ${EXECUTOR_WORK_DIR}/workspace.yml, re-run with force: true and the full corrected services array.`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Whether `dir` is the top of a git checkout. `.git` is a directory in a normal
|
|
47
|
+
* clone and a file in a worktree, so existence is the test. */
|
|
48
|
+
function isGitRepositoryRoot(dir) {
|
|
49
|
+
return fsSync.existsSync(path.join(dir, ".git"));
|
|
50
|
+
}
|
|
51
|
+
function findBadTestDirectory(workspacePath, services) {
|
|
52
|
+
const workspaceVideos = path.resolve(workspacePath, EXECUTOR_VIDEOS_DIR);
|
|
53
|
+
for (const svc of services) {
|
|
54
|
+
const dir = svc.testDirectory;
|
|
55
|
+
if (dir === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
const resolved = path.resolve(workspacePath, dir);
|
|
58
|
+
if (isInsideExecutorWorkDir(resolved) ||
|
|
59
|
+
isInsideDir(workspaceVideos, resolved) ||
|
|
60
|
+
isGitRepositoryRoot(resolved)) {
|
|
61
|
+
return { serviceName: svc.serviceName, testDirectory: dir };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
10
66
|
function getExecutorVersion() {
|
|
11
67
|
return SKYRAMP_IMAGE_VERSION;
|
|
12
68
|
}
|
|
@@ -92,6 +148,40 @@ export function registerInitializeWorkspaceTool(server) {
|
|
|
92
148
|
isError: false,
|
|
93
149
|
};
|
|
94
150
|
}
|
|
151
|
+
if (!params.services || params.services.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: "No services provided. Follow the scanning instructions from skyramp_init_scan to discover services, then call this tool again.",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
isError: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Both checks above and below are input-only, so they run before
|
|
163
|
+
// validateAndConsumeScanToken: that
|
|
164
|
+
// token is single-use, and a rejection after it would make the corrected
|
|
165
|
+
// retry this error asks for fail with "Invalid or expired scanToken". It
|
|
166
|
+
// is also before initialize()/updateMetadata(), which both write
|
|
167
|
+
// workspace.yml (workspace.ts:431, :460) — a later rejection would leave
|
|
168
|
+
// an initialized file behind and the retry would then short-circuit as
|
|
169
|
+
// "already initialized".
|
|
170
|
+
const badIncoming = findBadTestDirectory(workspacePath, params.services);
|
|
171
|
+
if (badIncoming)
|
|
172
|
+
return badTestDirectoryResult(badIncoming);
|
|
173
|
+
// Merge mode writes the MERGED set, so a bad testDirectory already in the
|
|
174
|
+
// file would be re-persisted. Checked on the merge, not on the existing
|
|
175
|
+
// services alone, so replacing a bad entry with a corrected one of the
|
|
176
|
+
// same identity is still the way out. Reading the file is side-effect
|
|
177
|
+
// free, so this rejection belongs up here with the other two rather than
|
|
178
|
+
// after the token is spent.
|
|
179
|
+
const mergeInto = params.merge && alreadyExists ? await manager.read() : undefined;
|
|
180
|
+
if (mergeInto) {
|
|
181
|
+
const badMerged = findBadTestDirectory(workspacePath, upsertServicesByRepo([...(mergeInto.services ?? [])], params.services));
|
|
182
|
+
if (badMerged)
|
|
183
|
+
return badTestDirectoryResult(badMerged);
|
|
184
|
+
}
|
|
95
185
|
// scanToken is required for fresh init; for edits (force:true on an
|
|
96
186
|
// existing workspace.yml) the caller supplies the full services array
|
|
97
187
|
// directly and we skip the token check.
|
|
@@ -120,33 +210,15 @@ export function registerInitializeWorkspaceTool(server) {
|
|
|
120
210
|
};
|
|
121
211
|
}
|
|
122
212
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
// Initialize (fresh) or read (merge into existing). Merge mode preserves
|
|
135
|
-
// the existing primary repo identity + previously-registered services;
|
|
136
|
-
// initialize() would reset both, dropping other repos' services.
|
|
137
|
-
let config;
|
|
138
|
-
if (params.merge && alreadyExists) {
|
|
139
|
-
config = await manager.read();
|
|
140
|
-
config = await manager.updateMetadata({
|
|
141
|
-
executorVersion: getExecutorVersion(),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
config = await manager.initialize();
|
|
146
|
-
config = await manager.updateMetadata({
|
|
147
|
-
executorVersion: getExecutorVersion(),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
213
|
+
// Merge mode skips initialize(): it preserves the existing primary repo
|
|
214
|
+
// identity + previously-registered services, both of which initialize()
|
|
215
|
+
// would reset, dropping other repos' services. The file itself was
|
|
216
|
+
// already read above, for the merged-set check.
|
|
217
|
+
if (!mergeInto)
|
|
218
|
+
await manager.initialize();
|
|
219
|
+
const config = await manager.updateMetadata({
|
|
220
|
+
executorVersion: getExecutorVersion(),
|
|
221
|
+
});
|
|
150
222
|
// Batch all service upserts: update in memory, write once (direct YAML
|
|
151
223
|
// write avoids N+1 reads and a mid-loop validation error). Upsert is keyed
|
|
152
224
|
// on the composite (repo, serviceName) — see upsertServicesByRepo — so a
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The executor's own working area inside a repo — run videos, trace zips and
|
|
3
|
+
* other per-run artefacts. Delivered test files do not belong here: the run
|
|
4
|
+
* cleans and rewrites it, and `git add -- <testDirectory>` on a directory that
|
|
5
|
+
* encloses it commits every .webm.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EXECUTOR_WORK_DIR = ".skyramp";
|
|
8
|
+
export declare const EXECUTOR_VIDEOS_DIR = ".skyramp/videos";
|
|
9
|
+
/**
|
|
10
|
+
* Whether `p` is the executor's working area or sits inside it. Matches on whole
|
|
11
|
+
* path segments, so a sibling such as `.skyramp-tests` is not a hit, and on the
|
|
12
|
+
* canonical path, so a symlink pointing into `.skyramp` cannot spell its way past
|
|
13
|
+
* the check.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isInsideExecutorWorkDir(p: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Refusal text for a caller that asked for output inside the working area. Names
|
|
18
|
+
* the field and the replacement so the agent can correct itself without help.
|
|
19
|
+
*/
|
|
20
|
+
export declare function executorWorkDirRefusal(destination: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Every path a generation call could write to, given an outputDir and an optional
|
|
23
|
+
* caller-supplied file name. `output` can carry `../` segments, so outputDir alone
|
|
24
|
+
* is not enough to judge.
|
|
25
|
+
*
|
|
26
|
+
* Two compositions, because the writers disagree on an ABSOLUTE `output`:
|
|
27
|
+
* `generateEnrichedIntegrationTestTool` joins it (`path.join(outputDir, output)`,
|
|
28
|
+
* which keeps outputDir as a prefix), while `path.resolve` discards outputDir
|
|
29
|
+
* entirely. A guard cannot pick one and be right for both, so it judges both and
|
|
30
|
+
* refuses if either lands in the working area.
|
|
31
|
+
*
|
|
32
|
+
* outputDir is always judged on its own as well. An `output` of `../tests/x.py`
|
|
33
|
+
* resolves clear of the working area while outputDir stays `.skyramp`, and
|
|
34
|
+
* outputDir is what codegen receives as the directory to work in.
|
|
35
|
+
*/
|
|
36
|
+
export declare function generationTargets(outputDir: string, output?: string): string[];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* The executor's own working area inside a repo — run videos, trace zips and
|
|
5
|
+
* other per-run artefacts. Delivered test files do not belong here: the run
|
|
6
|
+
* cleans and rewrites it, and `git add -- <testDirectory>` on a directory that
|
|
7
|
+
* encloses it commits every .webm.
|
|
8
|
+
*/
|
|
9
|
+
export const EXECUTOR_WORK_DIR = ".skyramp";
|
|
10
|
+
export const EXECUTOR_VIDEOS_DIR = `${EXECUTOR_WORK_DIR}/videos`;
|
|
11
|
+
/**
|
|
12
|
+
* Canonical form of `p`, where `p` may not exist yet: the deepest ancestor that
|
|
13
|
+
* DOES exist is resolved through symlinks and the missing remainder is appended
|
|
14
|
+
* back.
|
|
15
|
+
*
|
|
16
|
+
* `utils-verify/locate.ts` (realpath) and `code-refactor/reuse-state.ts` (canon)
|
|
17
|
+
* both fall back to the lexical path when the target is missing. A generation
|
|
18
|
+
* outputDir usually IS missing, and the lexical path is what a symlinked parent
|
|
19
|
+
* hides behind — so neither is usable here.
|
|
20
|
+
*/
|
|
21
|
+
function realpathAllowingMissing(p) {
|
|
22
|
+
let current = path.resolve(p);
|
|
23
|
+
const missing = [];
|
|
24
|
+
for (;;) {
|
|
25
|
+
try {
|
|
26
|
+
return path.join(fs.realpathSync(current), ...[...missing].reverse());
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
const parent = path.dirname(current);
|
|
30
|
+
if (parent === current)
|
|
31
|
+
return path.resolve(p);
|
|
32
|
+
missing.push(path.basename(current));
|
|
33
|
+
current = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether `p` is the executor's working area or sits inside it. Matches on whole
|
|
39
|
+
* path segments, so a sibling such as `.skyramp-tests` is not a hit, and on the
|
|
40
|
+
* canonical path, so a symlink pointing into `.skyramp` cannot spell its way past
|
|
41
|
+
* the check.
|
|
42
|
+
*/
|
|
43
|
+
export function isInsideExecutorWorkDir(p) {
|
|
44
|
+
return realpathAllowingMissing(p).split(path.sep).includes(EXECUTOR_WORK_DIR);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Refusal text for a caller that asked for output inside the working area. Names
|
|
48
|
+
* the field and the replacement so the agent can correct itself without help.
|
|
49
|
+
*/
|
|
50
|
+
export function executorWorkDirRefusal(destination) {
|
|
51
|
+
return `Refusing to write into "${destination}": it is inside ${EXECUTOR_WORK_DIR}, the Skyramp executor's working area (run videos and trace zips). Files written there are not delivered as tests. Set outputDir — and any output file name — so the result lands in the testDirectory of the service under test, read from ${EXECUTOR_WORK_DIR}/workspace.yml, or tests/skyramp when that service declares none. If that testDirectory is itself inside ${EXECUTOR_WORK_DIR}, it is wrong: re-run skyramp_init_workspace with a corrected one.`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Every path a generation call could write to, given an outputDir and an optional
|
|
55
|
+
* caller-supplied file name. `output` can carry `../` segments, so outputDir alone
|
|
56
|
+
* is not enough to judge.
|
|
57
|
+
*
|
|
58
|
+
* Two compositions, because the writers disagree on an ABSOLUTE `output`:
|
|
59
|
+
* `generateEnrichedIntegrationTestTool` joins it (`path.join(outputDir, output)`,
|
|
60
|
+
* which keeps outputDir as a prefix), while `path.resolve` discards outputDir
|
|
61
|
+
* entirely. A guard cannot pick one and be right for both, so it judges both and
|
|
62
|
+
* refuses if either lands in the working area.
|
|
63
|
+
*
|
|
64
|
+
* outputDir is always judged on its own as well. An `output` of `../tests/x.py`
|
|
65
|
+
* resolves clear of the working area while outputDir stays `.skyramp`, and
|
|
66
|
+
* outputDir is what codegen receives as the directory to work in.
|
|
67
|
+
*/
|
|
68
|
+
export function generationTargets(outputDir, output) {
|
|
69
|
+
const dir = path.resolve(outputDir);
|
|
70
|
+
if (!output)
|
|
71
|
+
return [dir];
|
|
72
|
+
return [
|
|
73
|
+
dir,
|
|
74
|
+
path.resolve(outputDir, output),
|
|
75
|
+
path.resolve(path.join(outputDir, output)),
|
|
76
|
+
];
|
|
77
|
+
}
|