@icyouo/evt-cli 0.1.0 → 0.1.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.
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: evt-profile-generator
3
+ description: Use when an agent needs to create or update evt-cli profile JSON files from project source, environment config, API YAML schemas, documentation, or user-provided credentials so evt-cli can run real APIs and flows.
4
+ ---
5
+
6
+ # evt-profile-generator
7
+
8
+ Use this skill to produce usable evt-cli profiles under `data/profiles/*.json`
9
+ or legacy `profiles/*.json`.
10
+
11
+ ## Workflow
12
+
13
+ 1. Locate the evt config root.
14
+ - Prefer the user's explicit `--config-root`.
15
+ - Otherwise use `EVT_CLI_ROOT`.
16
+ - Otherwise use `./cli` from the project root.
17
+ 2. Read existing profile examples:
18
+
19
+ ```bash
20
+ evt profile list --config-root ./cli
21
+ ```
22
+
23
+ 3. Inspect project source, docs, and environment files for:
24
+ - base URLs by environment
25
+ - static headers, app version, device/platform headers, tenant headers
26
+ - auth header name and token scheme
27
+ - login inputs and test fixtures required by endpoint schemas
28
+ 4. Inspect the project's HTTP runtime before writing headers:
29
+ - environment config and app config
30
+ - DI modules or client factories
31
+ - header builders and interceptors
32
+ - auth/session/cache code
33
+ - platform/device providers
34
+ 5. Write real profiles such as `data/profiles/dev.json`,
35
+ `data/profiles/android.dev.json`, or `data/profiles/ios.dev.json`.
36
+ 6. Validate:
37
+
38
+ ```bash
39
+ evt validate --config-root ./cli
40
+ ```
41
+
42
+ ## Profile Format
43
+
44
+ Use valid JSON only. Do not add comments.
45
+
46
+ ```json
47
+ {
48
+ "name": "dev",
49
+ "env": "dev",
50
+ "platform": "ANDROID",
51
+ "baseUrls": {
52
+ "default": "https://api.dev.example.com"
53
+ },
54
+ "auth": {
55
+ "header": "Authorization",
56
+ "scheme": "bearer"
57
+ },
58
+ "headers": {
59
+ "Accept": "application/json",
60
+ "Content-Type": "application/json",
61
+ "Platform": "{{profile.platform}}"
62
+ },
63
+ "device": {
64
+ "fingerprintId": "dev-fingerprint"
65
+ },
66
+ "inputs": {
67
+ "email": "user@example.com",
68
+ "password": "Password123."
69
+ },
70
+ "fixtures": {
71
+ "defaults": {
72
+ "page": 1,
73
+ "pageSize": 20
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Rules
80
+
81
+ - Keep bundled `*.example.json` generic; write project-specific data to
82
+ non-example profile files.
83
+ - Do not invent production secrets. Use user-provided test credentials,
84
+ documented test values, or safe placeholders.
85
+ - Prefer separate profiles when platforms have different headers, device
86
+ fields, or base URLs.
87
+ - Use exact enum casing and header names from source. If source uses `ANDROID`,
88
+ do not write `android`.
89
+ - Include backing fields for every templated header value, such as
90
+ `{{profile.device.fingerprintId}}`.
91
+ - Only include headers sent by the same runtime client. Do not merge unrelated
92
+ web, server, or third-party headers just because they appear elsewhere in the
93
+ repository.
94
+ - Keep static app headers and device headers in the profile; keep dynamic
95
+ per-request headers in endpoint YAML or flow steps.
96
+ - Put values needed by interactive flows in `inputs`.
97
+ - Put reusable endpoint test values in `fixtures.defaults` or grouped fixture
98
+ objects named after the endpoint domain.
99
+ - Configure token injection with `auth.header` and `auth.scheme`; token values
100
+ themselves belong in evt cache after login, not in the profile.
101
+ - If multiple API hosts exist, add named entries in `baseUrls` and set matching
102
+ endpoint metadata only when the API YAML supports it.
103
+ - Make `baseUrls` names match endpoint `service` values exactly.
104
+ - If auth uses a raw token header, set `"scheme": "raw"`. If it uses an
105
+ Authorization bearer token, set `"scheme": "bearer"`.
106
+ - Prefer values from source or docs over guesses. If a required runtime value is
107
+ unknown, leave a realistic placeholder and make the related flow input
108
+ interactive.
109
+
110
+ ## Completion Check
111
+
112
+ After writing the profile, run:
113
+
114
+ ```bash
115
+ evt validate --config-root ./cli
116
+ evt profile list --config-root ./cli
117
+ ```
118
+
119
+ If endpoint YAML and flows already exist, also dry-run login with the generated
120
+ profile and compare the produced request headers with the source header builder.
121
+ Report placeholder credentials, uncertain headers, and base URLs that were not
122
+ found in source.
@@ -97,6 +97,7 @@ function normalizeConfig(config, baseDir = repoRoot) {
97
97
  return {
98
98
  ...config,
99
99
  root,
100
+ apiDir: config.apiDir ? path.resolve(baseDir, config.apiDir) : undefined,
100
101
  targets: (config.targets || []).map((target) => ({ ...target }))
101
102
  };
102
103
  }
@@ -375,37 +376,85 @@ function normalizeSkillEndpoint(rawEndpoint, target, config) {
375
376
  }
376
377
 
377
378
  function scanSkillTarget(config, target) {
378
- if (!target.command) {
379
+ const resolvedTarget = resolveSkillTarget(target);
380
+ if (!resolvedTarget.command) {
379
381
  throw new Error("Skill scanner target must define command");
380
382
  }
381
- const cwd = target.cwd ? path.resolve(config.root, target.cwd) : config.root;
382
- const result = spawnSync(target.command, (target.args || []).map(String), {
383
+ const cwd = resolvedTarget.cwd ? path.resolve(config.root, resolvedTarget.cwd) : config.root;
384
+ const result = spawnSync(resolvedTarget.command, (resolvedTarget.args || []).map(String), {
383
385
  cwd,
384
386
  encoding: "utf8",
385
387
  shell: false,
386
388
  env: {
387
389
  ...process.env,
388
- ...(target.env || {})
390
+ EVT_SCAN_ROOT: config.root,
391
+ ...(resolvedTarget.env || {})
389
392
  },
390
- maxBuffer: target.maxBuffer || 10 * 1024 * 1024
393
+ maxBuffer: resolvedTarget.maxBuffer || 10 * 1024 * 1024
391
394
  });
392
395
 
393
396
  if (result.error) {
394
- throw new Error(`Skill scanner ${target.name || target.command} failed: ${result.error.message}`);
397
+ throw new Error(`Skill scanner ${resolvedTarget.name || resolvedTarget.command} failed: ${result.error.message}`);
395
398
  }
396
399
  if (result.status !== 0) {
397
400
  const stderr = String(result.stderr || "").trim();
398
- throw new Error(`Skill scanner ${target.name || target.command} exited with ${result.status}${stderr ? `: ${stderr}` : ""}`);
401
+ throw new Error(`Skill scanner ${resolvedTarget.name || resolvedTarget.command} exited with ${result.status}${stderr ? `: ${stderr}` : ""}`);
399
402
  }
400
403
 
401
- return parseSkillOutput(result.stdout, target)
402
- .map((endpoint) => normalizeSkillEndpoint(endpoint, target, config));
404
+ return parseSkillOutput(result.stdout, resolvedTarget)
405
+ .map((endpoint) => normalizeSkillEndpoint(endpoint, resolvedTarget, config));
406
+ }
407
+
408
+ function resolveSkillTarget(target) {
409
+ if (!target.skill) return target;
410
+ if (target.skill !== "evt-api-scanner") return target;
411
+ return {
412
+ ...target,
413
+ name: target.name || "evt-api-scanner",
414
+ command: process.execPath,
415
+ args: target.args || [
416
+ path.join(cliRoot, "skills", "evt-api-scanner", "scripts", "scan.js"),
417
+ "--root",
418
+ "."
419
+ ]
420
+ };
421
+ }
422
+
423
+ function splitTargets(targets) {
424
+ const skillTargets = [];
425
+ const fallbackTargets = [];
426
+ for (const target of targets || []) {
427
+ if (isSkillTarget(target)) {
428
+ skillTargets.push(target);
429
+ } else {
430
+ fallbackTargets.push(target);
431
+ }
432
+ }
433
+ return { skillTargets, fallbackTargets };
434
+ }
435
+
436
+ function scanTargets(config, targets) {
437
+ return targets.flatMap((target) => scanTarget(config, target));
403
438
  }
404
439
 
405
440
  function scanServices(options = {}) {
406
441
  const config = loadScanConfig(options);
407
442
  if (!config.targets || config.targets.length === 0) return [];
408
- return config.targets.flatMap((target) => scanTarget(config, target));
443
+ if (options.preferFallback) {
444
+ return scanTargets(config, config.targets.filter((target) => !isSkillTarget(target)));
445
+ }
446
+
447
+ const { skillTargets, fallbackTargets } = splitTargets(config.targets);
448
+ if (skillTargets.length === 0) {
449
+ return scanTargets(config, fallbackTargets);
450
+ }
451
+ try {
452
+ const skillEndpoints = scanTargets(config, skillTargets);
453
+ if (skillEndpoints.length > 0 || fallbackTargets.length === 0) return skillEndpoints;
454
+ } catch (error) {
455
+ if (fallbackTargets.length === 0 || options.strictSkill) throw error;
456
+ }
457
+ return scanTargets(config, fallbackTargets);
409
458
  }
410
459
 
411
460
  function compareScanToRegistry(scanned, registry) {
@@ -422,5 +471,6 @@ module.exports = {
422
471
  loadScanConfig,
423
472
  defaultScanConfig,
424
473
  parseCliScanOptions,
474
+ resolveSkillTarget,
425
475
  repoRoot
426
476
  };
package/src/index.js CHANGED
@@ -1,7 +1,9 @@
1
+ const path = require("node:path");
2
+ const { spawnSync } = require("node:child_process");
1
3
  const { parseArgs } = require("./util/args");
2
4
  const { parseJsonObject, stableJson } = require("./util/json");
3
5
  const { redact } = require("./util/redact");
4
- const { setConfigRoot } = require("./util/paths");
6
+ const { cliRoot, setConfigRoot } = require("./util/paths");
5
7
  const {
6
8
  listApiFiles,
7
9
  listProfiles,
@@ -16,6 +18,9 @@ const { runFlow } = require("./flow/runner");
16
18
  const { toCurl } = require("./http/requestBuilder");
17
19
  const { compareScanToRegistry, scanServices } = require("./config/serviceScanner");
18
20
  const { runLiveApiTest } = require("./api/liveTester");
21
+ const { runSyncApiCoverage } = require("../scripts/sync-api-coverage");
22
+ const { runApiCoverage } = require("../scripts/check-api-coverage");
23
+ const { runApiAudit } = require("../scripts/check-api-audit");
19
24
 
20
25
  function print(value, json = false) {
21
26
  if (json || typeof value !== "string") {
@@ -32,7 +37,11 @@ function usage() {
32
37
  " evt profile show <name>",
33
38
  " evt api list",
34
39
  " evt api show <id>",
40
+ " evt api discover [--config-root ./cli]",
35
41
  " evt api scan [--missing] [--scan-config path/to/scanner.json]",
42
+ " evt api sync [--config-root ./cli]",
43
+ " evt api coverage [--config-root ./cli]",
44
+ " evt api audit [--config-root ./cli] [--strict]",
36
45
  " evt api call <id> [--profile local] [--set k=v] [--body '{...}'] [--dry-run]",
37
46
  " evt api test-all [--profile local] [--include-dangerous] [--only namespace]",
38
47
  " evt validate",
@@ -42,6 +51,17 @@ function usage() {
42
51
  ].join("\n");
43
52
  }
44
53
 
54
+ function runNodeScript(relativeScript, args) {
55
+ const result = spawnSync(process.execPath, [path.join(cliRoot, relativeScript), ...args], {
56
+ cwd: process.cwd(),
57
+ stdio: "inherit",
58
+ shell: false
59
+ });
60
+ if (result.status !== 0) {
61
+ throw new Error(`${relativeScript} failed with exit code ${result.status || 1}`);
62
+ }
63
+ }
64
+
45
65
  function commonRuntime(options) {
46
66
  const profile = loadProfile(options.profile || "local");
47
67
  const cachePath = resolveCachePath(options.cache);
@@ -73,6 +93,30 @@ async function handleApi(tokens) {
73
93
  const sub = tokens[0];
74
94
  const options = parseArgs(tokens.slice(1));
75
95
  setConfigRoot(options.configRoot);
96
+
97
+ if (sub === "discover") {
98
+ const args = tokens.slice(1);
99
+ if (!options.configRoot && !args.includes("--config-root")) {
100
+ args.push("--config-root", "./cli");
101
+ }
102
+ runNodeScript("skills/evt-api-scanner/scripts/discover.js", args);
103
+ return;
104
+ }
105
+ if (sub === "sync") {
106
+ runSyncApiCoverage(tokens.slice(1));
107
+ return;
108
+ }
109
+ if (sub === "coverage") {
110
+ const result = runApiCoverage(tokens.slice(1));
111
+ if (result.failed) throw new Error("API coverage failed");
112
+ return;
113
+ }
114
+ if (sub === "audit") {
115
+ const result = runApiAudit(tokens.slice(1));
116
+ if (result.failed) throw new Error("API audit failed");
117
+ return;
118
+ }
119
+
76
120
  const registry = loadApiRegistry();
77
121
 
78
122
  if (sub === "list") {
package/src/util/args.js CHANGED
@@ -38,6 +38,9 @@ function parseArgs(tokens) {
38
38
  "includeDangerous",
39
39
  "noLogin",
40
40
  "missing",
41
+ "strict",
42
+ "preferFallback",
43
+ "strictSkill",
41
44
  "help"
42
45
  ]);
43
46
 
@@ -0,0 +1,137 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+ const { runApiAudit } = require("../scripts/check-api-audit");
8
+
9
+ function writeFile(root, relative, content) {
10
+ const file = path.join(root, relative);
11
+ fs.mkdirSync(path.dirname(file), { recursive: true });
12
+ fs.writeFileSync(file, content);
13
+ return file;
14
+ }
15
+
16
+ function fixtureRoot() {
17
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "evt-api-audit-"));
18
+ writeFile(root, "src/AuthService.kt", `
19
+ class AuthService(val http: Http) {
20
+ suspend fun login(): Resp<LoginData> {
21
+ return http.post("/api/login")
22
+ }
23
+ }
24
+ `);
25
+ writeFile(root, "data/scanner.json", JSON.stringify({
26
+ root,
27
+ targets: [
28
+ {
29
+ language: "kotlin",
30
+ paths: ["src"],
31
+ namespaceStripSuffixes: ["Service"]
32
+ }
33
+ ]
34
+ }, null, 2));
35
+ return root;
36
+ }
37
+
38
+ test("api audit flags scanner skeletons in strict mode", () => {
39
+ const root = fixtureRoot();
40
+ writeFile(root, "data/apis/auth.yaml", `
41
+ namespace: auth
42
+
43
+ endpoints:
44
+ login:
45
+ method: "POST"
46
+ path: "/api/login"
47
+ auth: false
48
+ schema: {}
49
+ response:
50
+ data:
51
+ type: "object"
52
+ `);
53
+
54
+ const result = runApiAudit(["--config-root", root, "--strict"], { silent: true });
55
+
56
+ assert.equal(result.failed, true);
57
+ assert.equal(result.payload.counts.emptySchema, 1);
58
+ assert.equal(result.payload.counts.genericResponse, 1);
59
+ assert.match(result.payload.failures.join("\\n"), /empty schema ratio/);
60
+ assert.match(result.payload.failures.join("\\n"), /generic response ratio/);
61
+ });
62
+
63
+ test("api audit passes enriched endpoint definitions", () => {
64
+ const root = fixtureRoot();
65
+ writeFile(root, "data/apis/auth.yaml", `
66
+ namespace: auth
67
+
68
+ endpoints:
69
+ login:
70
+ method: "POST"
71
+ path: "/api/login"
72
+ auth: false
73
+ schema:
74
+ body:
75
+ email:
76
+ type: "string"
77
+ required: true
78
+ password:
79
+ type: "string"
80
+ required: true
81
+ response:
82
+ envelope: "Resp"
83
+ data:
84
+ type: "object"
85
+ model: "LoginData"
86
+ fields:
87
+ token: "string"
88
+ `);
89
+
90
+ const result = runApiAudit(["--config-root", root, "--strict"], { silent: true });
91
+
92
+ assert.equal(result.failed, false);
93
+ assert.equal(result.payload.counts.emptySchema, 0);
94
+ assert.equal(result.payload.counts.genericResponse, 0);
95
+ assert.deepEqual(result.payload.failures, []);
96
+ });
97
+
98
+ test("cli api audit accepts strict as a boolean flag", () => {
99
+ const root = fixtureRoot();
100
+ writeFile(root, "data/apis/auth.yaml", `
101
+ namespace: auth
102
+
103
+ endpoints:
104
+ login:
105
+ method: "POST"
106
+ path: "/api/login"
107
+ auth: false
108
+ schema:
109
+ body:
110
+ email:
111
+ type: "string"
112
+ required: true
113
+ response:
114
+ envelope: "Resp"
115
+ data:
116
+ type: "object"
117
+ model: "LoginData"
118
+ fields:
119
+ token: "string"
120
+ `);
121
+
122
+ const result = spawnSync(process.execPath, [
123
+ path.join(__dirname, "..", "bin", "evt.js"),
124
+ "api",
125
+ "audit",
126
+ "--config-root",
127
+ root,
128
+ "--strict"
129
+ ], {
130
+ encoding: "utf8"
131
+ });
132
+
133
+ assert.equal(result.status, 0, result.stderr || result.stdout);
134
+ const payload = JSON.parse(result.stdout);
135
+ assert.equal(payload.ok, true);
136
+ assert.equal(payload.strict, true);
137
+ });
@@ -0,0 +1,11 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { parseDuration } = require("../src/util/duration");
4
+
5
+ test("parses duration values", () => {
6
+ assert.equal(parseDuration(1000), 1000);
7
+ assert.equal(parseDuration("1000"), 1000);
8
+ assert.equal(parseDuration("500ms"), 500);
9
+ assert.equal(parseDuration("2s"), 2000);
10
+ assert.equal(parseDuration("1m"), 60000);
11
+ });
@@ -0,0 +1,215 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { runFlow } = require("../src/flow/runner");
4
+ const { loadApiRegistry, loadProfile } = require("../src/config/loaders");
5
+
6
+ test("runs login flow in dry-run mode without prompt", async () => {
7
+ const result = await runFlow({
8
+ name: "login",
9
+ inputs: {
10
+ email: { required: true },
11
+ password: { required: true },
12
+ code: { default: "" },
13
+ googleCode: { default: "" }
14
+ },
15
+ steps: [
16
+ {
17
+ id: "login",
18
+ call: "auth.login",
19
+ body: {
20
+ email: "{{inputs.email}}",
21
+ password: "{{inputs.password}}"
22
+ }
23
+ }
24
+ ]
25
+ }, {
26
+ registry: loadApiRegistry(),
27
+ profile: loadProfile("local"),
28
+ cache: {},
29
+ cachePath: "/tmp/evt-cli-test-cache.json",
30
+ set: {
31
+ email: "user@example.com",
32
+ password: "secret"
33
+ },
34
+ dryRun: true,
35
+ noInteractive: true
36
+ });
37
+
38
+ assert.equal(result.flow, "login");
39
+ assert.equal(result.timeline.filter((item) => item.call).length, 1);
40
+ assert.equal(result.steps.login.request.method, "POST");
41
+ });
42
+
43
+ test("fails when required cache save value is missing", async () => {
44
+ const cachePath = "/tmp/evt-cli-required-cache-test.json";
45
+ const originalFetch = global.fetch;
46
+ global.fetch = async () => ({
47
+ ok: true,
48
+ status: 200,
49
+ text: async () => JSON.stringify({ code: 0, data: { info: { email: "user@example.com" } } })
50
+ });
51
+
52
+ try {
53
+ await assert.rejects(
54
+ runFlow({
55
+ name: "save-required",
56
+ steps: [
57
+ {
58
+ id: "login",
59
+ call: "auth.login",
60
+ body: {
61
+ email: "user@example.com",
62
+ password: "secret"
63
+ }
64
+ }
65
+ ],
66
+ save: {
67
+ cache: {
68
+ token: "{{steps.login.response.data.token}}"
69
+ },
70
+ required: ["cache.token"]
71
+ }
72
+ }, {
73
+ registry: loadApiRegistry(),
74
+ profile: loadProfile("local"),
75
+ cache: {},
76
+ cachePath,
77
+ set: {},
78
+ dryRun: false,
79
+ noInteractive: true
80
+ }),
81
+ /required save value: cache\.token/
82
+ );
83
+ } finally {
84
+ global.fetch = originalFetch;
85
+ }
86
+ });
87
+
88
+ test("supports step expect and extract variables", async () => {
89
+ const originalFetch = global.fetch;
90
+ global.fetch = async () => ({
91
+ ok: true,
92
+ status: 200,
93
+ text: async () => JSON.stringify({ code: 0, data: { token: "token-value" } })
94
+ });
95
+
96
+ try {
97
+ const result = await runFlow({
98
+ name: "expect-extract",
99
+ steps: [
100
+ {
101
+ id: "login",
102
+ call: "auth.login",
103
+ body: {
104
+ email: "user@example.com",
105
+ password: "secret"
106
+ },
107
+ expect: [
108
+ { path: "response.code", equals: 0 },
109
+ { path: "response.data.token", exists: true }
110
+ ],
111
+ extract: {
112
+ token: "response.data.token"
113
+ }
114
+ }
115
+ ]
116
+ }, {
117
+ registry: loadApiRegistry(),
118
+ profile: loadProfile("local"),
119
+ cache: {},
120
+ cachePath: "/tmp/evt-cli-extract-cache-test.json",
121
+ set: {},
122
+ dryRun: false,
123
+ noInteractive: true
124
+ });
125
+
126
+ assert.deepEqual(result.vars, ["token"]);
127
+ } finally {
128
+ global.fetch = originalFetch;
129
+ }
130
+ });
131
+
132
+ test("updates flow cache for later authenticated steps", async () => {
133
+ const originalFetch = global.fetch;
134
+ const seen = [];
135
+ global.fetch = async (url, options) => {
136
+ seen.push({ url, options });
137
+ if (seen.length === 1) {
138
+ return {
139
+ ok: true,
140
+ status: 200,
141
+ text: async () => JSON.stringify({ code: 0, data: { token: "token-value" } })
142
+ };
143
+ }
144
+ return {
145
+ ok: true,
146
+ status: 200,
147
+ text: async () => JSON.stringify({ code: 0, data: { email: "user@example.com" } })
148
+ };
149
+ };
150
+
151
+ try {
152
+ await runFlow({
153
+ name: "flow-cache",
154
+ steps: [
155
+ {
156
+ id: "login",
157
+ call: "auth.login",
158
+ body: {
159
+ email: "user@example.com",
160
+ password: "secret"
161
+ },
162
+ extract: {
163
+ token: "response.data.token"
164
+ },
165
+ cache: {
166
+ token: "{{vars.token}}"
167
+ }
168
+ },
169
+ {
170
+ id: "user",
171
+ call: "todo.list"
172
+ }
173
+ ]
174
+ }, {
175
+ registry: loadApiRegistry(),
176
+ profile: loadProfile("local"),
177
+ cache: {},
178
+ cachePath: "/tmp/evt-cli-flow-cache-test.json",
179
+ set: {},
180
+ dryRun: false,
181
+ noInteractive: true
182
+ });
183
+
184
+ assert.equal(seen[1].options.headers.Authorization, "Bearer token-value");
185
+ } finally {
186
+ global.fetch = originalFetch;
187
+ }
188
+ });
189
+
190
+ test("skips response expectations during dry-run", async () => {
191
+ const result = await runFlow({
192
+ name: "dry-run-expect",
193
+ steps: [
194
+ {
195
+ id: "oauth",
196
+ call: "auth.login",
197
+ expect: { path: "response.code", equals: 0 },
198
+ extract: {
199
+ token: "response.data.token"
200
+ }
201
+ }
202
+ ]
203
+ }, {
204
+ registry: loadApiRegistry(),
205
+ profile: loadProfile("local"),
206
+ cache: {},
207
+ cachePath: "/tmp/evt-cli-dry-run-expect-cache-test.json",
208
+ set: {},
209
+ dryRun: true,
210
+ noInteractive: true
211
+ });
212
+
213
+ assert.equal(result.flow, "dry-run-expect");
214
+ assert.deepEqual(result.vars, []);
215
+ });