harness-mcp-v2 3.2.4 → 3.2.5

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.
Files changed (102) hide show
  1. package/README.md +62 -3
  2. package/build/config.d.ts +22 -3
  3. package/build/config.d.ts.map +1 -1
  4. package/build/config.js +10 -0
  5. package/build/config.js.map +1 -1
  6. package/build/data/examples/index.d.ts +2 -0
  7. package/build/data/examples/index.d.ts.map +1 -1
  8. package/build/data/examples/index.js +4 -0
  9. package/build/data/examples/index.js.map +1 -1
  10. package/build/data/schemas/v0/pipeline.d.ts.map +1 -1
  11. package/build/data/schemas/v0/pipeline.js +31 -10
  12. package/build/data/schemas/v0/pipeline.js.map +1 -1
  13. package/build/data/schemas/v0/template.d.ts.map +1 -1
  14. package/build/data/schemas/v0/template.js +28 -10
  15. package/build/data/schemas/v0/template.js.map +1 -1
  16. package/build/index.js +67 -11
  17. package/build/index.js.map +1 -1
  18. package/build/prompts/create-agent.d.ts.map +1 -1
  19. package/build/prompts/create-agent.js +21 -1
  20. package/build/prompts/create-agent.js.map +1 -1
  21. package/build/registry/extractors.d.ts +10 -3
  22. package/build/registry/extractors.d.ts.map +1 -1
  23. package/build/registry/extractors.js +27 -9
  24. package/build/registry/extractors.js.map +1 -1
  25. package/build/registry/toolsets/chaos-descriptions.d.ts +23 -19
  26. package/build/registry/toolsets/chaos-descriptions.d.ts.map +1 -1
  27. package/build/registry/toolsets/chaos-descriptions.js +134 -48
  28. package/build/registry/toolsets/chaos-descriptions.js.map +1 -1
  29. package/build/registry/toolsets/chaos.d.ts.map +1 -1
  30. package/build/registry/toolsets/chaos.js +323 -56
  31. package/build/registry/toolsets/chaos.js.map +1 -1
  32. package/build/registry/toolsets/dbops.js +2 -2
  33. package/build/registry/toolsets/dbops.js.map +1 -1
  34. package/build/registry/toolsets/idp.d.ts.map +1 -1
  35. package/build/registry/toolsets/idp.js +0 -6
  36. package/build/registry/toolsets/idp.js.map +1 -1
  37. package/build/registry/toolsets/pipelines.d.ts.map +1 -1
  38. package/build/registry/toolsets/pipelines.js +6 -4
  39. package/build/registry/toolsets/pipelines.js.map +1 -1
  40. package/build/registry/toolsets/pull-requests.d.ts.map +1 -1
  41. package/build/registry/toolsets/pull-requests.js +47 -2
  42. package/build/registry/toolsets/pull-requests.js.map +1 -1
  43. package/build/search/embedding-content.d.ts +5 -0
  44. package/build/search/embedding-content.d.ts.map +1 -0
  45. package/build/search/embedding-content.js +31 -0
  46. package/build/search/embedding-content.js.map +1 -0
  47. package/build/search/index.d.ts +8 -0
  48. package/build/search/index.d.ts.map +1 -0
  49. package/build/search/index.js +6 -0
  50. package/build/search/index.js.map +1 -0
  51. package/build/search/local-provider.d.ts +37 -0
  52. package/build/search/local-provider.d.ts.map +1 -0
  53. package/build/search/local-provider.js +269 -0
  54. package/build/search/local-provider.js.map +1 -0
  55. package/build/search/manager.d.ts +39 -0
  56. package/build/search/manager.d.ts.map +1 -0
  57. package/build/search/manager.js +283 -0
  58. package/build/search/manager.js.map +1 -0
  59. package/build/search/null-provider.d.ts +9 -0
  60. package/build/search/null-provider.d.ts.map +1 -0
  61. package/build/search/null-provider.js +8 -0
  62. package/build/search/null-provider.js.map +1 -0
  63. package/build/search/remote-provider.d.ts +28 -0
  64. package/build/search/remote-provider.d.ts.map +1 -0
  65. package/build/search/remote-provider.js +139 -0
  66. package/build/search/remote-provider.js.map +1 -0
  67. package/build/search/routing-eval.d.ts +49 -0
  68. package/build/search/routing-eval.d.ts.map +1 -0
  69. package/build/search/routing-eval.js +80 -0
  70. package/build/search/routing-eval.js.map +1 -0
  71. package/build/search/types.d.ts +58 -0
  72. package/build/search/types.d.ts.map +1 -0
  73. package/build/search/types.js +7 -0
  74. package/build/search/types.js.map +1 -0
  75. package/build/tools/harness-execute.d.ts.map +1 -1
  76. package/build/tools/harness-execute.js +19 -7
  77. package/build/tools/harness-execute.js.map +1 -1
  78. package/build/tools/harness-get.d.ts +2 -1
  79. package/build/tools/harness-get.d.ts.map +1 -1
  80. package/build/tools/harness-get.js +21 -1
  81. package/build/tools/harness-get.js.map +1 -1
  82. package/build/tools/harness-list.d.ts +2 -1
  83. package/build/tools/harness-list.d.ts.map +1 -1
  84. package/build/tools/harness-list.js +22 -1
  85. package/build/tools/harness-list.js.map +1 -1
  86. package/build/tools/harness-search.d.ts +20 -1
  87. package/build/tools/harness-search.d.ts.map +1 -1
  88. package/build/tools/harness-search.js +124 -14
  89. package/build/tools/harness-search.js.map +1 -1
  90. package/build/tools/index.d.ts +2 -1
  91. package/build/tools/index.d.ts.map +1 -1
  92. package/build/tools/index.js +4 -4
  93. package/build/tools/index.js.map +1 -1
  94. package/build/utils/http-health.d.ts +11 -0
  95. package/build/utils/http-health.d.ts.map +1 -0
  96. package/build/utils/http-health.js +11 -0
  97. package/build/utils/http-health.js.map +1 -0
  98. package/build/utils/http-sessions.d.ts +8 -0
  99. package/build/utils/http-sessions.d.ts.map +1 -0
  100. package/build/utils/http-sessions.js +12 -0
  101. package/build/utils/http-sessions.js.map +1 -0
  102. package/package.json +5 -1
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import YAML from "yaml";
2
3
  import { passthrough, ngExtract, chaosPageExtract, chaosExperimentListExtract, chaosInputSetListExtract, chaosAppMapPageExtract, chaosProbeListExtract, chaosInfraListExtract, chaosK8sInfraListExtract, chaosLoadTestListExtract, chaosLoadTestExtract, chaosHubListExtract, chaosDRTestListExtract, sdPageExtract, chaosRunTimeInputsExtract, chaosActionExtract, } from "../extractors.js";
3
4
  import { descToolsetChaos,
4
5
  // Resource descriptions
@@ -10,7 +11,7 @@ descRunExperiment, descStopExperiment, descDeleteExperiment, descEnableProbe, de
10
11
  // Body schema descriptions
11
12
  descBodyExperimentRun, descBodyNoBody, descBodyExperimentCreate, descBodyCreateFromTemplate, descBodyLoadtestDefinition, descBodyProbeEnable, descBodyProbeVerify, descBodyProbesInRun, descBodyProbeCreate,
12
13
  // Field descriptions
13
- descInputsetIdentity, descRuntimeInputs, descHubIdentity, descInfraType, descExperimentName, descExperimentIdentity, descInfraRef, descExperimentId, descInfraStatus, descLoadtestName, descLoadtestType, descLoadtestIdentity, descLoadtestDescription, descLoadtestTags, descLoadtestEnvId, descLoadtestInfraId, descLoadtestTargetType, descLoadtestTargetUrl, descLoadtestScript, descLoadtestUsers, descLoadtestDurationSec, descLoadtestRampUpSec, descLoadtestWorkerCount, descLoadtestScriptSource, descLoadtestScriptImage, descLoadtestScriptEntrypoint, descLoadtestLoadArgs, descHubIdentityExact, descHubName, descHubNameUpdate, descHubDescription, descHubDescriptionUpdate, descHubTags, descHubTagsReplace, descConnectorRef, descRepoName, descRepoBranch, descHubSearch, descIncludeAllScope, descTemplateSearch, descSortField, descSortAsc, descTags, descInfrastructure, descTemplateIdentity, descRevision, descRevision1, descRevision2, descRevisionToCompare, descFaultType, descFaultCategory, descFaultPermissions, descFaultIsEnterprise, descImportType, descExperimentDescription, descExperimentTags, descEntityTypeProbe, descEntityTypeAction, descEntityTypeFault, descPermissionsRequiredEnum, descOnlyTemplatisedFaults, descEnvironmentId, descK8sInfraStatus, descIncludeLegacyInfra, descSearchK8sInfra, descChaosEnabledInfraType, descInfraScope, descInfraAiEnabled, descSearchTermEnv, descSortEnv, descEnvironmentType, descGuardSearch, descGuardInfraType, descGuardTags, descGuardEnabled, descExperimentRunIdStop, descNotifyId, descForce, descIsEnabledFlag, descIsBulkUpdate, descVerifyFlag, descExperimentRunIds, descNotifyIds, descFaultIdentityParam, descIsEnterpriseFilter, descFaultSearch, descFaultListType, descFaultListInfraType, descFaultListInfrastructure, descFaultListTags, descFaultListCategory, descFaultListSortField, descFaultListSortAscending, descIsEnterpriseYaml, descIsEnterpriseVars, descIsEnterpriseRuns, descActionIdentityParam, descSearchActionsParam, descHubIdentityActions, descExperimentVariablesParam, descTasksParam, descEnvironmentIdCreate, descInfraIdCreate, descAccountIdBody, descOrgIdBody, descProjectIdBody, descSearchExperiments, descExperimentInfraId, descExperimentEnvironmentId, descExperimentIds, descExperimentStartDate, descExperimentEndDate, descExperimentTargetNetworkMapIds, descExperimentMyExperiments, descExperimentExcludeAutomation, descSearchProbes, descProbeIds, descProbeSortField, descProbeIdField, descProbeNameField, descProbePropertiesField, descRunPropertiesField, descDRTestSort, descCreateDRTest, descBodyDRTestCreate, descDRTestName, descDRTestIdentifier, descDRTestDescription, descDRTestObjective, descDRTestTags,
14
+ descInputsetIdentity, descRuntimeInputs, descHubIdentity, descInfraType, descExperimentName, descExperimentIdentity, descInfraRef, descExperimentId, descInfraStatus, descLoadtestName, descLoadtestType, descLoadtestIdentity, descLoadtestDescription, descLoadtestTags, descLoadtestEnvId, descLoadtestInfraId, descLoadtestTargetType, descLoadtestTargetUrl, descLoadtestScript, descLoadtestUsers, descLoadtestDurationSec, descLoadtestRampUpSec, descLoadtestWorkerCount, descLoadtestScriptSource, descLoadtestScriptImage, descLoadtestScriptEntrypoint, descLoadtestLoadArgs, descLoadtestHostUrl, descLoadtestRpsLimit, descLoadtestIterations, descLoadtestEnvVars, descHubIdentityExact, descHubName, descHubNameUpdate, descHubDescription, descHubDescriptionUpdate, descHubTags, descHubTagsReplace, descConnectorRef, descRepoName, descRepoBranch, descHubSearch, descIncludeAllScope, descTemplateSearch, descSortField, descSortAsc, descTags, descInfrastructure, descTemplateIdentity, descRevision, descRevision1, descRevision2, descRevisionToCompare, descFaultType, descFaultCategory, descFaultPermissions, descFaultIsEnterprise, descImportType, descExperimentDescription, descExperimentTags, descEntityTypeProbe, descEntityTypeAction, descEntityTypeFault, descPermissionsRequiredEnum, descOnlyTemplatisedFaults, descEnvironmentId, descK8sInfraStatus, descIncludeLegacyInfra, descSearchK8sInfra, descChaosEnabledInfraType, descInfraScope, descInfraAiEnabled, descSearchTermEnv, descSortEnv, descEnvironmentType, descGuardSearch, descGuardInfraType, descGuardTags, descGuardEnabled, descExperimentRunIdStop, descNotifyId, descForce, descIsEnabledFlag, descIsBulkUpdate, descVerifyFlag, descExperimentRunIds, descNotifyIds, descFaultIdentityParam, descIsEnterpriseFilter, descFaultSearch, descFaultListType, descFaultListInfraType, descFaultListInfrastructure, descFaultListTags, descFaultListCategory, descFaultListSortField, descFaultListSortAscending, descIsEnterpriseYaml, descIsEnterpriseVars, descIsEnterpriseRuns, descActionIdentityParam, descSearchActionsParam, descHubIdentityActions, descExperimentVariablesParam, descTasksParam, descEnvironmentIdCreate, descInfraIdCreate, descAccountIdBody, descOrgIdBody, descProjectIdBody, descSearchExperiments, descExperimentInfraId, descExperimentEnvironmentId, descExperimentIds, descExperimentStartDate, descExperimentEndDate, descExperimentTargetNetworkMapIds, descExperimentMyExperiments, descExperimentExcludeAutomation, descSearchProbes, descProbeIds, descProbeSortField, descProbeIdField, descProbeNameField, descProbePropertiesField, descRunPropertiesField, descDRTestSort, descCreateDRTest, descBodyDRTestCreate, descDRTestName, descDRTestIdentifier, descDRTestDescription, descDRTestObjective, descDRTestTags,
14
15
  // Input set descriptions
15
16
  descChaosInputSet, descListInputSets, descGetInputSet, descCreateInputSet, descUpdateInputSet, descDeleteInputSet, descInputSetIdentityField, descInputSetName, descInputSetDescription, descInputSetSpec, descIsIdentity,
16
17
  // Component variable descriptions
@@ -59,6 +60,230 @@ function coerceBody(input) {
59
60
  }
60
61
  return raw;
61
62
  }
63
+ /**
64
+ * Map ergonomic snake_case scalars to the canonical TemplateInput[] array the
65
+ * load-test backend expects. Only emits entries for values the caller provided
66
+ * (plus the three always-present run params and the K8s-only workerCount).
67
+ */
68
+ function buildLoadtestInputs(b, opts) {
69
+ const inputs = [];
70
+ // Run params: always emitted (backend treats targetUsers as required).
71
+ const users = b.users != null ? b.users : 100;
72
+ const duration = b.duration_sec != null ? b.duration_sec : 600;
73
+ const rampUp = b.ramp_up_sec != null ? b.ramp_up_sec : 120;
74
+ inputs.push({ name: "targetUsers", value: users, type: "Integer", required: true });
75
+ inputs.push({ name: "durationSeconds", value: duration, type: "Integer" });
76
+ inputs.push({ name: "rampUpTimeSec", value: rampUp, type: "Integer" });
77
+ // workerCount is Kubernetes-only (0 = standalone, N > 0 = distributed).
78
+ if (opts.targetType === "kubernetes") {
79
+ const workerCount = b.worker_count != null ? b.worker_count : 0;
80
+ inputs.push({ name: "workerCount", value: workerCount, type: "Integer" });
81
+ }
82
+ // Target URL: present on every load test (backend treats it as the host under test).
83
+ const targetUrl = (b.target_url ?? b.targetUrl);
84
+ if (targetUrl != null) {
85
+ inputs.push({ name: "targetUrl", value: targetUrl, type: "String" });
86
+ }
87
+ // Image-mode tunables (Custom Image): scriptImage/scriptEntrypoint are
88
+ // mandatory in image mode (marked required), loadArgs is optional.
89
+ if (opts.scriptSource === "image") {
90
+ const scriptImage = (b.script_image ?? b.scriptImage);
91
+ if (scriptImage != null) {
92
+ inputs.push({ name: "scriptImage", value: scriptImage, type: "String", required: true });
93
+ }
94
+ const entrypoint = (b.script_entrypoint ?? b.scriptEntrypoint);
95
+ if (entrypoint != null) {
96
+ inputs.push({ name: "scriptEntrypoint", value: entrypoint, type: "String", required: true });
97
+ }
98
+ const loadArgs = (b.load_args ?? b.loadArgs);
99
+ if (loadArgs != null) {
100
+ inputs.push({ name: "loadArgs", value: loadArgs, type: "String" });
101
+ }
102
+ }
103
+ return inputs;
104
+ }
105
+ // ── K6-specific helpers ──────────────────────────────────────────────
106
+ // K6 is a Kubernetes-only load test runner. Script mode wraps the K6 JS source
107
+ // in a `toolConfig` object (rather than top-level `scriptContent` like Locust).
108
+ // Env vars and secret references live exclusively in `toolConfig.envVars`.
109
+ // Reserved K6 env-var names (case-insensitive). Mirrors
110
+ // loadTestManager/internal/domain/ReservedEnvVarNames; the frontend list is at
111
+ // hce-saas/web/src/services/loadTest/loadTestVariables.ts:337-372.
112
+ const RESERVED_K6_ENV_VAR_NAMES = new Set([
113
+ "RUN_ID", "LOAD_TEST_ID", "TARGET_USERS", "SPAWN_RATE", "SCRIPT_CONTENT_BASE64",
114
+ "TARGET_URL", "ACCOUNT_ID", "ORG_ID", "PROJECT_ID", "ENV_ID", "DURATION_SECONDS",
115
+ "CONTROL_PLANE_URL", "CONTROL_PLANE_TOKEN", "HARNESS_CUSTOM_VAR_NAMES",
116
+ "METRICS_PUSH_INTERVAL", "INFRA_ID", "ACCESS_KEY", "TENANT_ID",
117
+ "PYTHONPATH", "PATH", "HOME", "USER", "SHELL", "LANG", "TERM", "HOSTNAME",
118
+ "PWD", "LD_LIBRARY_PATH", "LD_PRELOAD", "TMPDIR", "TMP", "TEMP",
119
+ ]);
120
+ const K6_ENV_VAR_KEY_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
121
+ // Build the HSM secret reference: account → "account.<id>", org → "org.<id>", project → "<id>".
122
+ // Matches the Harness UI convention (scope prefix encodes which secret manager the value lives in).
123
+ function buildSecretReference(secretId, scope) {
124
+ const prefix = scope === "account" ? "account." : scope === "org" ? "org." : "";
125
+ return `secrets.getValue("${prefix}${secretId}")`;
126
+ }
127
+ // Validate + project the structured env_vars input into the wire shape K6 expects.
128
+ // Each entry sets exactly one of:
129
+ // - { key, value } → literal env var
130
+ // - { key, secret_id, secret_scope?: "..." } → MCP builds the secrets.getValue(...) string
131
+ function buildK6EnvVars(raw) {
132
+ if (raw == null)
133
+ return [];
134
+ if (!Array.isArray(raw)) {
135
+ throw new Error("env_vars must be an array of {key, value | secret_id, secret_scope?} entries.");
136
+ }
137
+ const seen = new Set();
138
+ return raw.map((entry, idx) => {
139
+ const ev = entry;
140
+ const key = ev.key;
141
+ if (!key || typeof key !== "string") {
142
+ throw new Error(`env_vars[${idx}].key is required.`);
143
+ }
144
+ if (!K6_ENV_VAR_KEY_REGEX.test(key)) {
145
+ throw new Error(`env_vars[${idx}].key '${key}' is invalid: must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
146
+ }
147
+ if (RESERVED_K6_ENV_VAR_NAMES.has(key.toUpperCase())) {
148
+ throw new Error(`env_vars[${idx}].key '${key}' is a reserved name and cannot be used as a custom env var.`);
149
+ }
150
+ if (seen.has(key)) {
151
+ throw new Error(`env_vars[${idx}].key '${key}' is duplicated.`);
152
+ }
153
+ seen.add(key);
154
+ const hasValue = ev.value != null;
155
+ const hasSecretId = ev.secret_id != null;
156
+ if (hasValue === hasSecretId) {
157
+ throw new Error(`env_vars[${idx}] must set exactly one of 'value' (literal) or 'secret_id' (with optional secret_scope).`);
158
+ }
159
+ if (hasSecretId) {
160
+ const scopeRaw = ev.secret_scope ?? "project";
161
+ if (scopeRaw !== "account" && scopeRaw !== "org" && scopeRaw !== "project") {
162
+ throw new Error(`env_vars[${idx}].secret_scope '${scopeRaw}' must be 'account', 'org', or 'project'.`);
163
+ }
164
+ return { key, value: buildSecretReference(ev.secret_id, scopeRaw), secret: true };
165
+ }
166
+ return { key, value: String(ev.value) };
167
+ });
168
+ }
169
+ // Strip path/query/fragment, keep <protocol>//<host>. Mirrors K6LoadTestService's
170
+ // resolveBaseHost (web/src/services/loadTest/K6LoadTestService.ts:117-129).
171
+ function parseHostOrigin(rawUrl) {
172
+ if (!rawUrl)
173
+ return undefined;
174
+ try {
175
+ const u = new URL(rawUrl);
176
+ return `${u.protocol}//${u.host}`;
177
+ }
178
+ catch {
179
+ return rawUrl;
180
+ }
181
+ }
182
+ // Build the K6 `toolConfig` object for script or image mode (UI mode is deferred).
183
+ // Script mode: enforces the mandatory `export default function` rule client-side so the
184
+ // agent gets a clear error before the API trip. Image mode: wires the prebuilt container
185
+ // image (+ optional entrypoint) into customImage. hostUrl / options / envVars are shared.
186
+ function buildK6ToolConfig(b, args) {
187
+ const hostUrl = b.host_url ??
188
+ b.hostUrl ??
189
+ parseHostOrigin(args.targetUrl);
190
+ const rpsLimit = b.rps_limit != null ? b.rps_limit : undefined;
191
+ const envVars = buildK6EnvVars(b.env_vars);
192
+ const options = {};
193
+ if (rpsLimit != null && rpsLimit > 0)
194
+ options.rpsLimit = rpsLimit;
195
+ // Build the mode-specific payload first; shared keys (hostUrl, options, envVars,
196
+ // iterations) are appended after so we get a stable key order matching the studio.
197
+ let toolConfig;
198
+ if (args.scriptSource === "image") {
199
+ // Custom Image: image + optional entrypoint live in toolConfig.customImage.
200
+ // Note: load_args is NOT part of customImage for K6 — it rides only in inputs[]
201
+ // (matches K6LoadTestService.ts:487-495 where customImage only has image/entrypoint).
202
+ const scriptImage = (b.script_image ?? b.scriptImage);
203
+ if (scriptImage == null) {
204
+ // Defensive — the early image-mode gate in bodyBuilder already throws this.
205
+ throw new Error("K6 image mode requires 'script_image'.");
206
+ }
207
+ const customImage = { image: scriptImage };
208
+ const entrypoint = (b.script_entrypoint ?? b.scriptEntrypoint);
209
+ if (entrypoint != null)
210
+ customImage.entrypoint = entrypoint;
211
+ toolConfig = { mode: "image", customImage };
212
+ }
213
+ else {
214
+ // Script mode: base64 JS source. Mandatory client-side rule (matches Harness UI:
215
+ // validateScriptContent at K6LoadTestService.ts:767-774). Substring check (not
216
+ // full AST) matches the UI exactly.
217
+ if (args.script == null) {
218
+ throw new Error("K6 script mode requires 'script' (the raw JavaScript K6 source).");
219
+ }
220
+ if (!args.script.includes("export default")) {
221
+ throw new Error("K6 script must export a default function (export default function ...).");
222
+ }
223
+ toolConfig = {
224
+ mode: "script",
225
+ scriptContent: Buffer.from(args.script, "utf8").toString("base64"),
226
+ };
227
+ // iterations is K6 script-mode-only (matches K6LoadTestService.ts:483-484).
228
+ const iterations = b.iterations != null ? b.iterations : undefined;
229
+ if (iterations != null && iterations > 0)
230
+ toolConfig.iterations = iterations;
231
+ }
232
+ if (hostUrl)
233
+ toolConfig.hostUrl = hostUrl;
234
+ if (Object.keys(options).length > 0)
235
+ toolConfig.options = options;
236
+ if (envVars.length > 0)
237
+ toolConfig.envVars = envVars;
238
+ return toolConfig;
239
+ }
240
+ /**
241
+ * Build the canonical LoadTest YAML manifest. Mirrors the studio's
242
+ * formDataToManifest shape (kind: LoadTest, apiVersion: v1alpha1, spec.{...}).
243
+ *
244
+ * scriptContent inside the YAML is PLAIN TEXT (so the manifest is human-
245
+ * readable); only the JSON request body's top-level scriptContent is base64.
246
+ * Caller base64-encodes the returned string into the `yaml` request field.
247
+ */
248
+ function buildLoadtestYamlManifest(args) {
249
+ const infraType = args.targetType === "kubernetes" ? "kubernetes" : "linux";
250
+ const manifest = {
251
+ kind: "LoadTest",
252
+ apiVersion: "v1alpha1",
253
+ name: args.name,
254
+ };
255
+ if (args.description)
256
+ manifest.description = args.description;
257
+ if (args.tags && args.tags.length)
258
+ manifest.tags = args.tags;
259
+ const spec = {
260
+ identity: args.identity,
261
+ toolType: args.toolType,
262
+ infraType,
263
+ targetType: args.targetType,
264
+ scriptSource: args.scriptSource,
265
+ };
266
+ // Locust inline only: readable scriptContent at the manifest root (plain text).
267
+ // K6 keeps the script inside spec.toolConfig instead.
268
+ if (args.toolType === "Locust" && args.scriptSource === "inline" && args.script) {
269
+ spec.scriptContent = args.script;
270
+ }
271
+ spec.infraId = args.infraIdentifier;
272
+ spec.envId = args.environmentIdentifier;
273
+ spec.inputs = args.inputs;
274
+ // K6: emit spec.toolConfig with PLAIN TEXT scriptContent (decode the base64 from
275
+ // the wire-shape toolConfig so the YAML view is human-readable). Other
276
+ // toolConfig keys (mode, hostUrl, options, envVars, iterations) pass through.
277
+ if (args.toolType === "K6" && args.k6ToolConfig) {
278
+ const tc = { ...args.k6ToolConfig };
279
+ if (typeof tc.scriptContent === "string" && args.script) {
280
+ tc.scriptContent = args.script; // plain text for YAML readability
281
+ }
282
+ spec.toolConfig = tc;
283
+ }
284
+ manifest.spec = spec;
285
+ return YAML.stringify(manifest);
286
+ }
62
287
  export const chaosToolset = {
63
288
  name: "chaos",
64
289
  displayName: "Chaos Engineering",
@@ -1322,79 +1547,114 @@ export const chaosToolset = {
1322
1547
  bodyBuilder: (input) => {
1323
1548
  const b = coerceBody(input);
1324
1549
  const name = b.name ?? "";
1325
- // Backend constraint: name allows only lowercase letters, numbers and dashes.
1326
- if (name && !/^[a-z0-9-]+$/.test(name)) {
1327
- throw new Error(`Invalid load test name '${name}': only lowercase letters, numbers and dashes are allowed.`);
1550
+ if (!name) {
1551
+ throw new Error("name is required.");
1328
1552
  }
1553
+ // identity is the slug-constrained key; auto-derive from name when omitted.
1554
+ // The display name is permissive (any non-empty string) — only the identity
1555
+ // must be a slug. We strip non-alphanumerics from name as a sensible default;
1556
+ // a fully-blank slug falls back to a UUID so we never write an empty id.
1329
1557
  const slug = (s) => s.replace(/[^a-zA-Z0-9]/g, "");
1558
+ const identity = b.identity || slug(name) || randomUUID();
1559
+ if (!/^[a-zA-Z0-9_]+$/.test(identity)) {
1560
+ throw new Error(`Invalid identity '${identity}': only letters, numbers and underscores are allowed (derived from name when omitted).`);
1561
+ }
1330
1562
  const targetType = b.target_type ?? "machine-chaos-linux";
1331
- const tags = b.tags;
1332
- const environmentIdentifier = b.environment_id ?? b.environmentIdentifier;
1333
- const infraIdentifier = b.infra_id ?? b.infraIdentifier;
1334
- const targetUrl = b.target_url ?? b.targetUrl;
1563
+ const environmentIdentifier = (b.environment_id ?? b.environmentIdentifier);
1564
+ const infraIdentifier = (b.infra_id ?? b.infraIdentifier);
1565
+ const targetUrl = (b.target_url ?? b.targetUrl);
1335
1566
  const script = b.script;
1336
1567
  const scriptImage = (b.script_image ?? b.scriptImage);
1337
- // Resolve test-definition source: explicit script_source wins, else infer
1338
- // "image" when an image is supplied, otherwise "inline" (Python script).
1568
+ // Explicit script_source wins, else infer "image" when an image is supplied.
1339
1569
  const scriptSource = b.script_source ?? (scriptImage != null ? "image" : "inline");
1570
+ // Mode-specific required fields (fail loudly before we touch the wire).
1571
+ // Tool-agnostic message: Locust = Python locust source, K6 = JavaScript with 'export default'.
1572
+ // The K6-specific 'export default' rule is enforced later in buildK6ToolConfig.
1573
+ if (scriptSource === "inline" && script == null) {
1574
+ throw new Error("script is required when script_source='inline'.");
1575
+ }
1576
+ if (scriptSource === "image" && scriptImage == null) {
1577
+ throw new Error("script_image is required when script_source='image'.");
1578
+ }
1579
+ // Tool type: Locust (default) or K6. K6 is Kubernetes-only and supports
1580
+ // script and image modes (UI mode is deferred).
1581
+ const toolType = (b.tool_type ?? "Locust");
1582
+ if (toolType !== "Locust" && toolType !== "K6") {
1583
+ throw new Error(`tool_type '${toolType}' must be 'Locust' or 'K6'.`);
1584
+ }
1585
+ if (toolType === "K6" && targetType !== "kubernetes") {
1586
+ throw new Error("K6 load tests require target_type='kubernetes' (LinuxVM is not supported).");
1587
+ }
1588
+ // Normalise tags (accept array or comma-separated string; default []).
1589
+ const rawTags = b.tags;
1590
+ const tags = Array.isArray(rawTags)
1591
+ ? rawTags
1592
+ : typeof rawTags === "string"
1593
+ ? rawTags.split(",").map((t) => t.trim()).filter(Boolean)
1594
+ : [];
1595
+ // Build the canonical inputs[] array (well-known tunables → TemplateInput[]).
1596
+ // inputs[] is identical between Locust and K6 script/image modes.
1597
+ const inputs = buildLoadtestInputs(b, { targetType, scriptSource });
1598
+ // K6 wraps the script in a toolConfig object (no top-level scriptContent).
1599
+ // Built up-front so the same value drives both the JSON body and the YAML view.
1600
+ let k6ToolConfig;
1601
+ if (toolType === "K6") {
1602
+ k6ToolConfig = buildK6ToolConfig(b, { scriptSource, script, targetUrl });
1603
+ }
1604
+ // Build the canonical LoadTest YAML manifest, then base64-encode for the wire.
1605
+ // The manifest carries plain-text scriptContent (Locust inline) OR plain-text
1606
+ // toolConfig.scriptContent (K6), so the YAML view is human-readable; the JSON
1607
+ // body's base64 lives only on the wire.
1608
+ const yamlManifest = buildLoadtestYamlManifest({
1609
+ name,
1610
+ description: b.description,
1611
+ tags,
1612
+ identity,
1613
+ toolType,
1614
+ targetType,
1615
+ scriptSource,
1616
+ script,
1617
+ k6ToolConfig,
1618
+ environmentIdentifier: environmentIdentifier,
1619
+ infraIdentifier: infraIdentifier,
1620
+ inputs,
1621
+ });
1340
1622
  const body = {
1341
- identity: b.identity || slug(name) || randomUUID(),
1623
+ identity,
1342
1624
  name,
1625
+ description: b.description ?? "",
1626
+ tags,
1343
1627
  environmentIdentifier,
1344
1628
  infraIdentifier,
1345
- targetType,
1346
- toolType: b.tool_type ?? "Locust",
1347
- targetUrl,
1348
1629
  scriptSource,
1349
- defaultUsers: b.users != null ? b.users : 100,
1350
- defaultDurationSec: b.duration_sec != null ? b.duration_sec : 600,
1351
- defaultRampUpTimeSec: b.ramp_up_sec != null ? b.ramp_up_sec : 120,
1630
+ targetType,
1631
+ toolType,
1632
+ inputs,
1633
+ yaml: Buffer.from(yamlManifest, "utf8").toString("base64"),
1352
1634
  };
1353
- // Emit snake_case aliases for the required fields whose API key is a
1354
- // camelCase rename, so the registry's required-field validator (which
1355
- // checks the built body against the snake_case bodySchema field names)
1356
- // sees them. The load-test backend reads the camelCase keys and ignores
1357
- // the extra ones (same pattern as chaos_action create).
1635
+ // Top-level scriptContent rules:
1636
+ // - Locust inline: base64 of the raw Python script.
1637
+ // - Locust image: omitted (script_image rides in inputs[]).
1638
+ // - K6 script: OMITTED at top level the K6 script lives in
1639
+ // toolConfig.scriptContent only (top-level would be
1640
+ // treated as a Locust-style script and double-encoded).
1641
+ // - K6 image: not yet supported (rejected earlier).
1642
+ if (toolType === "Locust" && scriptSource === "inline") {
1643
+ body.scriptContent = Buffer.from(script, "utf8").toString("base64");
1644
+ }
1645
+ if (toolType === "K6" && k6ToolConfig) {
1646
+ body.toolConfig = k6ToolConfig;
1647
+ }
1648
+ // Emit snake_case aliases for the registry's required-field validator (which
1649
+ // checks the built body against the snake_case bodySchema field names). The
1650
+ // load-test backend reads the camelCase / inputs[] keys and ignores extras
1651
+ // (same pattern as chaos_action create).
1358
1652
  if (environmentIdentifier != null)
1359
1653
  body.environment_id = environmentIdentifier;
1360
1654
  if (infraIdentifier != null)
1361
1655
  body.infra_id = infraIdentifier;
1362
1656
  if (targetUrl != null)
1363
1657
  body.target_url = targetUrl;
1364
- if (b.description != null)
1365
- body.description = b.description;
1366
- if (tags != null) {
1367
- body.tags = Array.isArray(tags)
1368
- ? tags
1369
- : tags.split(",").map((t) => t.trim()).filter(Boolean);
1370
- }
1371
- if (scriptSource === "image") {
1372
- // Custom Image mode (Kubernetes): prebuilt container image as the source.
1373
- if (scriptImage == null) {
1374
- throw new Error("script_image is required when script_source='image'.");
1375
- }
1376
- body.scriptImage = scriptImage;
1377
- const entrypoint = b.script_entrypoint ?? b.scriptEntrypoint;
1378
- if (entrypoint != null)
1379
- body.scriptEntrypoint = entrypoint;
1380
- const loadArgs = b.load_args ?? b.loadArgs;
1381
- if (loadArgs != null)
1382
- body.loadArgs = loadArgs;
1383
- }
1384
- else {
1385
- // Inline (Python script) mode: base64-encode the raw locust script.
1386
- if (script == null) {
1387
- throw new Error("script is required when script_source='inline' (the raw Python locust script).");
1388
- }
1389
- body.scriptContent = Buffer.from(script, "utf8").toString("base64");
1390
- }
1391
- // Kubernetes always carries a worker count (0 = standalone, N = distributed).
1392
- if (targetType === "kubernetes") {
1393
- const workerCount = b.worker_count != null ? b.worker_count : 0;
1394
- body.variables = {
1395
- workerCount: { type: "fixed", valueType: "int", value: workerCount },
1396
- };
1397
- }
1398
1658
  return body;
1399
1659
  },
1400
1660
  responseExtractor: chaosLoadTestExtract,
@@ -1420,6 +1680,10 @@ export const chaosToolset = {
1420
1680
  { name: "duration_sec", type: "number", required: false, description: descLoadtestDurationSec },
1421
1681
  { name: "ramp_up_sec", type: "number", required: false, description: descLoadtestRampUpSec },
1422
1682
  { name: "worker_count", type: "number", required: false, description: descLoadtestWorkerCount },
1683
+ { name: "host_url", type: "string", required: false, description: descLoadtestHostUrl },
1684
+ { name: "rps_limit", type: "number", required: false, description: descLoadtestRpsLimit },
1685
+ { name: "iterations", type: "number", required: false, description: descLoadtestIterations },
1686
+ { name: "env_vars", type: "array", required: false, description: descLoadtestEnvVars },
1423
1687
  ],
1424
1688
  },
1425
1689
  },
@@ -1438,6 +1702,9 @@ export const chaosToolset = {
1438
1702
  path: `${CHAOS_LOADTEST}/v1/load-tests/{loadtestId}/runs`,
1439
1703
  operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
1440
1704
  pathParams: { loadtest_id: "loadtestId" },
1705
+ // TODO(runtime-inputs): accept { values: TemplateInputMinimum[] } when runtime
1706
+ // inputs ('<+input>' sentinel) are added (see deferred follow-up plan). The empty
1707
+ // body is correct while runtime inputs are unsupported on the create side.
1441
1708
  bodyBuilder: () => ({}),
1442
1709
  responseExtractor: passthrough,
1443
1710
  actionDescription: descRunLoadtest,