systemview 1.18.1 → 1.20.0

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/cli/runTests.js CHANGED
@@ -1,17 +1,59 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { createCookieClient } = require("./cookieClient");
4
- const Client = createCookieClient();
5
4
  const { initializeSavedTests } = require("../testing-utilities/transformTests");
6
5
  const FullTestController = require("../testing-utilities/FullTestController");
7
6
  const log = require("./logger");
8
7
  const validationMessages = require("../testing-utilities/validtionMessages");
8
+ const { truncate, TruncationMarker } = require("./utils/truncate");
9
+
10
+ function formatTruncated(value, indent = 0) {
11
+ const pad = " ".repeat(indent);
12
+ const inner = " ".repeat(indent + 1);
13
+ if (value instanceof TruncationMarker) return chalk.dim(value.message);
14
+ if (value === null) return "null";
15
+ if (typeof value !== "object") return JSON.stringify(value);
16
+ if (Array.isArray(value)) {
17
+ if (!value.length) return "[]";
18
+ const items = value.map((v) =>
19
+ v instanceof TruncationMarker
20
+ ? inner + chalk.dim(v.message)
21
+ : inner + formatTruncated(v, indent + 1)
22
+ );
23
+ return "[\n" + items.join(",\n") + "\n" + pad + "]";
24
+ }
25
+ const keys = Object.keys(value);
26
+ if (!keys.length) return "{}";
27
+ const entries = keys.map((k) => {
28
+ const v = value[k];
29
+ if (k === "__more__" && v instanceof TruncationMarker)
30
+ return inner + chalk.dim(v.message);
31
+ return inner + JSON.stringify(k) + ": " + formatTruncated(v, indent + 1);
32
+ });
33
+ return "{\n" + entries.join(",\n") + "\n" + pad + "}";
34
+ }
9
35
  const { runFullTest } = new FullTestController();
10
36
 
11
37
  const chalk = require("chalk");
12
38
  const DIVIDER = chalk.dim(" " + "─".repeat(60));
39
+ const SUITE_DIVIDER = chalk.cyan(" " + "─".repeat(60));
13
40
 
14
- module.exports = async function runTests(url, project_code, namespace, { json = false, verbose = false, manifest: manifestPath } = {}) {
41
+ module.exports = async function runTests(
42
+ url,
43
+ project_code,
44
+ namespace,
45
+ {
46
+ json = false,
47
+ verbose = false,
48
+ manifest: manifestPath,
49
+ headers: cliHeaders = {},
50
+ bail = false,
51
+ dryRun = false,
52
+ phase: phaseFilter = null,
53
+ index: indexFilter = undefined,
54
+ skip: skipPatterns = [],
55
+ } = {}
56
+ ) {
15
57
  const api = `${url}/systemview/api`;
16
58
 
17
59
  if (!project_code) {
@@ -19,26 +61,34 @@ module.exports = async function runTests(url, project_code, namespace, { json =
19
61
  return 1;
20
62
  }
21
63
 
22
- const connectedServices = await resolveServices(api, project_code, manifestPath);
64
+ const { services: connectedServices, probeHeaders: manifestHeaders } = await resolveServices(
65
+ api,
66
+ project_code,
67
+ manifestPath
68
+ );
23
69
  if (!connectedServices || !connectedServices.length) {
24
70
  log.warn("No connected services found for project: " + project_code);
25
71
  return 1;
26
72
  }
27
73
 
74
+ const extraHeaders = { ...manifestHeaders, ...cliHeaders };
75
+ const Client = createCookieClient(extraHeaders);
76
+
28
77
  if (!json) {
29
78
  connectedServices.forEach(({ serviceId, system }) => {
30
79
  log.success(`connected: ${serviceId} @ ${system.connectionData.serviceUrl}`);
31
80
  });
32
81
  }
33
82
 
34
- const allTestLists = await getTests(connectedServices);
83
+ const allTestLists = await getTests(connectedServices, Client);
35
84
  const testToRun = allTestLists
36
85
  .map((list) =>
37
- namespace
38
- ? list.filter(({ namespace: n }) =>
39
- `${n.serviceId}.${n.moduleName}.${n.methodName}`.includes(namespace)
40
- )
41
- : list
86
+ list.filter(({ namespace: n }) => {
87
+ const full = `${n.serviceId}.${n.moduleName}.${n.methodName}`;
88
+ const matchesInclude = !namespace || full.includes(namespace);
89
+ const matchesSkip = skipPatterns.length && skipPatterns.some((p) => full.includes(p));
90
+ return matchesInclude && !matchesSkip;
91
+ })
42
92
  )
43
93
  .filter((list) => list.length);
44
94
 
@@ -47,8 +97,24 @@ module.exports = async function runTests(url, project_code, namespace, { json =
47
97
  return 0;
48
98
  }
49
99
 
100
+ if (dryRun) {
101
+ const total = testToRun.reduce((n, list) => n + list.length, 0);
102
+ console.log("");
103
+ console.log(` Would run ${total} ${total === 1 ? "test" : "tests"}:`);
104
+ console.log("");
105
+ testToRun.forEach((list) => {
106
+ list.forEach(({ namespace: n, title }) => {
107
+ console.log(` ${chalk.cyan(`${n.serviceId}.${n.moduleName}.${n.methodName}`)} — "${title}"`);
108
+ });
109
+ });
110
+ console.log("");
111
+ return 0;
112
+ }
113
+
50
114
  const jsonOutput = json ? { projectCode: project_code, passed: 0, failed: 0, tests: [] } : null;
51
115
  let totalFailed = 0;
116
+ const serviceSummaries = [];
117
+ const allFailures = [];
52
118
 
53
119
  for (const testList of testToRun) {
54
120
  const { serviceId } = testList[0].namespace;
@@ -56,7 +122,14 @@ module.exports = async function runTests(url, project_code, namespace, { json =
56
122
 
57
123
  try {
58
124
  const initialized = initializeSavedTests(testList, connectedServices, Client);
59
- const { passed, failed, jsonTests } = await runAllTests(initialized, url, project_code, json, verbose);
125
+ const { passed, failed, jsonTests, failures } = await runAllTests(
126
+ initialized,
127
+ url,
128
+ project_code,
129
+ json,
130
+ verbose,
131
+ { bail, phaseFilter, indexFilter }
132
+ );
60
133
  totalFailed += failed;
61
134
 
62
135
  if (json) {
@@ -64,10 +137,11 @@ module.exports = async function runTests(url, project_code, namespace, { json =
64
137
  jsonOutput.failed += failed;
65
138
  jsonOutput.tests.push(...jsonTests);
66
139
  } else {
67
- log[failed ? "error" : "success"](
68
- `${serviceId}: ${passed + failed} tests, ${passed} passed, ${failed} failed`
69
- );
140
+ serviceSummaries.push({ serviceId, passed, failed });
141
+ allFailures.push(...failures);
70
142
  }
143
+
144
+ if (bail && failed > 0) break;
71
145
  } catch (error) {
72
146
  log.error(`Unexpected error for ${serviceId}: ${error.message}`);
73
147
  }
@@ -79,22 +153,67 @@ module.exports = async function runTests(url, project_code, namespace, { json =
79
153
  console.log("");
80
154
  log.info("TEST COMPLETE");
81
155
  console.log("");
156
+
157
+ serviceSummaries.forEach(({ serviceId, passed, failed }) => {
158
+ const total = passed + failed;
159
+ log[failed ? "error" : "success"](
160
+ `${serviceId}: ${total} tests, ${passed} passed, ${failed} failed`
161
+ );
162
+ });
163
+
164
+ if (allFailures.length) {
165
+ console.log("");
166
+ log.warn("FAILED:");
167
+ allFailures.forEach(({ serviceId, moduleName, methodName, title, failedPhases }) => {
168
+ console.log("");
169
+ log.error(` ${serviceId}.${moduleName}.${methodName} — "${title}"`);
170
+ failedPhases.forEach(({ phase, actions }) => {
171
+ actions.forEach(({ actionTitle, errors }) => {
172
+ console.log(` ${phase}: "${actionTitle}"`);
173
+ errors.forEach((err) => console.log(` → ${validationMessages(err)}`));
174
+ });
175
+ });
176
+ });
177
+ console.log("");
178
+ }
82
179
  }
83
180
 
84
181
  return totalFailed > 0 ? 1 : 0;
85
182
  };
86
183
 
87
- const runAllTests = async (savedTests, url, project_code, json, verbose) => {
88
- const sum = { passed: 0, failed: 0, jsonTests: [] };
184
+ const runAllTests = async (savedTests, url, project_code, json, verbose, opts = {}) => {
185
+ const { bail = false, phaseFilter = null, indexFilter = undefined } = opts;
186
+ const sum = { passed: 0, failed: 0, jsonTests: [], failures: [] };
187
+
188
+ for (const { Before: B, Main: M, Events: E, After: A, title, namespace } of savedTests) {
189
+ let Before = [...B];
190
+ let Main = [...M];
191
+ let Events = [...E];
192
+ let After = [...A];
193
+
194
+ if (phaseFilter) {
195
+ const allowed = phaseFilter.split(",").map((p) => p.trim().toLowerCase());
196
+ if (!allowed.includes("before")) Before = [];
197
+ if (!allowed.includes("main")) Main = [];
198
+ if (!allowed.includes("events")) Events = [];
199
+ if (!allowed.includes("after")) After = [];
200
+ }
201
+
202
+ if (indexFilter !== undefined) {
203
+ Before = Before.slice(indexFilter, indexFilter + 1);
204
+ Main = Main.slice(indexFilter, indexFilter + 1);
205
+ Events = Events.slice(indexFilter, indexFilter + 1);
206
+ After = After.slice(indexFilter, indexFilter + 1);
207
+ }
89
208
 
90
- for (const { Before, Main, Events, After, title, namespace } of savedTests) {
91
- const { moduleName, methodName, serviceId } = namespace;
92
209
  const fullTest = [Before, Main, Events, After];
210
+ const { moduleName, methodName, serviceId } = namespace;
93
211
 
94
212
  if (!json) {
95
- console.log("");
96
- console.log(" " + chalk.bold.cyan(`${serviceId}.${moduleName}.${methodName}(...)`));
97
- console.log(chalk.dim(` UI → ${url}/${project_code}/${serviceId}/${moduleName}/${methodName}`));
213
+ console.log(SUITE_DIVIDER);
214
+ console.log(" " + chalk.bold("Test: ") + chalk.bold.cyan(title));
215
+ console.log(SUITE_DIVIDER);
216
+ console.log(chalk.dim(` ${serviceId}.${moduleName}.${methodName} → ${url}/${project_code}/${serviceId}/${moduleName}/${methodName}`));
98
217
  console.log("");
99
218
  }
100
219
 
@@ -103,8 +222,24 @@ const runAllTests = async (savedTests, url, project_code, json, verbose) => {
103
222
  const hasFailed =
104
223
  countErrors(Before) + countErrors(Main) + countErrors(Events) + countErrors(After) > 0;
105
224
 
106
- if (hasFailed) sum.failed++;
107
- else sum.passed++;
225
+ if (hasFailed) {
226
+ sum.failed++;
227
+ const failedPhases = [];
228
+ [
229
+ { phase: "Before", tests: Before },
230
+ { phase: "Main", tests: Main },
231
+ { phase: "Events", tests: Events },
232
+ { phase: "After", tests: After },
233
+ ].forEach(({ phase, tests }) => {
234
+ const actions = tests
235
+ .filter((t) => t.errors && t.errors.length)
236
+ .map((t) => ({ actionTitle: t.title, errors: t.errors }));
237
+ if (actions.length) failedPhases.push({ phase, actions });
238
+ });
239
+ sum.failures.push({ serviceId, moduleName, methodName, title, failedPhases });
240
+ } else {
241
+ sum.passed++;
242
+ }
108
243
 
109
244
  if (json) {
110
245
  sum.jsonTests.push({
@@ -119,47 +254,65 @@ const runAllTests = async (savedTests, url, project_code, json, verbose) => {
119
254
  After: serializePhase(After),
120
255
  });
121
256
  } else {
122
- logPhase(Before, "Before", verbose, verbose);
123
- logPhase(Main, "Main", true, verbose);
124
- logPhase(Events, "Events", verbose, verbose);
125
- logPhase(After, "After", verbose, verbose);
257
+ const compact = !verbose;
258
+ logPhase(Before, "Before", verbose, compact, false);
259
+ logPhase(Main, "Main", verbose, false, true);
260
+ logPhase(Events, "Events", verbose, compact, false);
261
+ logPhase(After, "After", verbose, compact, false);
126
262
  }
263
+
264
+ if (bail && hasFailed) break;
127
265
  }
128
266
 
129
267
  return sum;
130
268
  };
131
269
 
132
- function countErrors(tests) {
133
- return tests.reduce((n, t) => n + (t.errors ? t.errors.length : 0), 0);
134
- }
135
-
136
- function logPhase(tests, section, logSuccess = false, verbose = false) {
270
+ function logPhase(tests, section, verbose = false, compact = false, isMain = false) {
137
271
  tests.forEach(({ errors, results, namespace, title, args }) => {
138
272
  const { serviceId, moduleName, methodName } = namespace;
139
273
  const failed = errors && errors.length;
140
- if (!failed && !logSuccess) return;
141
- console.log(chalk.dim(` ${section}: ${title}`));
142
- failed
143
- ? log.error(` ${serviceId}.${moduleName}.${methodName}(...)`)
144
- : log.success(` ${serviceId}.${moduleName}.${methodName}(...)`);
274
+
275
+ const label = isMain ? chalk.bold.green(section) : chalk.blue(section);
276
+ console.log(` ${label}: ${chalk.dim(title)}`);
277
+ if (failed) {
278
+ console.log(" " + chalk.bold.red(`${serviceId}.${moduleName}.${methodName}(...)`));
279
+ } else if (isMain) {
280
+ console.log(" " + chalk.bold.green(`${serviceId}.${moduleName}.${methodName}(...)`));
281
+ } else {
282
+ log.success(` ${serviceId}.${moduleName}.${methodName}(...)`);
283
+ }
284
+
145
285
  if (verbose && args && args.length) {
146
286
  console.log("");
147
287
  args.forEach(({ name, input }) => {
148
288
  console.log(` ${chalk.dim(name)}`, JSON.stringify(input));
149
289
  });
150
290
  }
291
+
151
292
  console.log("");
152
- console.log(" results:", JSON.stringify(results, null, 2).replace(/\n/g, "\n "));
293
+ if (compact) {
294
+ console.log(" results:", JSON.stringify(truncate(results)));
295
+ } else if (verbose) {
296
+ console.log(" results:", JSON.stringify(results, null, 2).replace(/\n/g, "\n "));
297
+ } else {
298
+ console.log(" results:", formatTruncated(truncate(results)).replace(/\n/g, "\n "));
299
+ }
153
300
  console.log("");
301
+
154
302
  if (failed) {
155
303
  log.warn(` ${errors.length} validation error(s)`);
156
304
  errors.forEach((err) => console.log(` → ${validationMessages(err)}`));
157
305
  console.log("");
158
306
  }
307
+
159
308
  console.log(DIVIDER);
160
309
  });
161
310
  }
162
311
 
312
+ function countErrors(tests) {
313
+ return tests.reduce((n, t) => n + (t.errors ? t.errors.length : 0), 0);
314
+ }
315
+
163
316
  function serializePhase(tests) {
164
317
  return tests.map((test) => {
165
318
  const { title, namespace, args, results, errors, evaluations } = test;
@@ -188,7 +341,7 @@ function serializePhase(tests) {
188
341
  });
189
342
  }
190
343
 
191
- async function getTests(connectedServices) {
344
+ async function getTests(connectedServices, Client) {
192
345
  const results = [];
193
346
  for (const { system } of connectedServices) {
194
347
  const { connectionData } = system;
@@ -207,25 +360,28 @@ async function resolveServices(api, project_code, manifestPath) {
207
360
  if (fs.existsSync(manifestFile)) {
208
361
  try {
209
362
  const raw = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
210
- // Support both { projectCode, services: [] } and single-service { projectCode, serviceId, system, specList }
211
363
  const services = raw.services
212
364
  ? raw.services
213
365
  : [{ serviceId: raw.serviceId, system: raw.system, specList: raw.specList }];
214
366
 
215
367
  if (project_code && raw.projectCode && raw.projectCode !== project_code) {
216
- log.warn(`Manifest projectCode "${raw.projectCode}" doesn't match "${project_code}", falling back to API`);
368
+ log.warn(
369
+ `Manifest projectCode "${raw.projectCode}" doesn't match "${project_code}", falling back to API`
370
+ );
217
371
  } else {
218
- return services;
372
+ return { services, probeHeaders: raw.probeHeaders || {} };
219
373
  }
220
374
  } catch (err) {
221
375
  log.warn(`Failed to read manifest: ${err.message}`);
222
376
  }
223
377
  }
224
378
 
225
- return await getConnectedServices(api, project_code);
379
+ const BaseClient = createCookieClient();
380
+ const services = await getConnectedServices(api, project_code, BaseClient);
381
+ return { services, probeHeaders: {} };
226
382
  }
227
383
 
228
- async function getConnectedServices(api, project_code) {
384
+ async function getConnectedServices(api, project_code, Client) {
229
385
  try {
230
386
  const { SystemView } = await Client.loadService(api);
231
387
  return await SystemView.getServices(project_code);
package/cli/utils/cli.js CHANGED
@@ -7,6 +7,7 @@ const HELP_TEXT = `
7
7
  Commands:
8
8
  start [port] Launch SystemView UI (default port 3000)
9
9
  test <projectCode> [namespace] Run saved tests for a project
10
+ list [projectCode] [namespace] List projects, services, or tests
10
11
  connect <serviceId> <url> Register a service and write manifest
11
12
  connect Re-probe all services in existing manifest
12
13
  probe <ServiceId.Module.method> [args] Call a method ad-hoc
@@ -14,33 +15,82 @@ const HELP_TEXT = `
14
15
  shutdown [port] Stop a running SystemView instance
15
16
 
16
17
  Flags:
18
+ --version Print version and exit
17
19
  --json Output results as JSON (for agents/CI)
18
- --verbose Print all phases including Before/After on pass
20
+ --verbose test: full results and args for all phases; list: expand to show methods and test titles
19
21
  --manifest <path> Path to manifest file (default: ./systemview.manifest.json)
22
+ --header "Name: Value" Extra request header (repeatable; overrides manifest.probeHeaders)
23
+ --skip <pattern> Exclude tests matching pattern (repeatable)
24
+ --bail Stop after first failure
25
+ --dry-run Print which tests would run without executing
26
+ --phase <before|main|events|after> Run only specified phase(s), comma-separated
27
+ --index <n> Run only action at index n within each phase (0-based)
20
28
 
21
29
  Examples:
22
30
  systemview start
23
31
  systemview test buAPI
24
32
  systemview test buAPI Users.signUp --json
33
+ systemview test buAPI --skip deleteUser --bail
34
+ systemview test buAPI Users.signIn --phase main --index 0
35
+ systemview list
36
+ systemview list buAPI
37
+ systemview list buAPI --verbose
38
+ systemview list buAPI signUp
25
39
  systemview connect ProfilesService http://localhost:4100/bu/api/profiles
26
40
  systemview probe ProfilesService.Users.getUser '{"userId":"123"}'
41
+ systemview open buAPI signUp
42
+ systemview test buAPI --header "X-Api-Key: secret"
27
43
  `;
28
44
 
29
45
  const rawArgs = process.argv.slice(2);
30
46
 
47
+ const flagValueArgs = ["--manifest", "--header", "--skip", "--phase", "--index"];
48
+
31
49
  const flags = {
32
50
  json: rawArgs.includes("--json"),
33
51
  verbose: rawArgs.includes("--verbose") || rawArgs.includes("-v"),
34
52
  debug: rawArgs.includes("--debug") || rawArgs.includes("-d"),
53
+ bail: rawArgs.includes("--bail"),
54
+ dryRun: rawArgs.includes("--dry-run"),
35
55
  manifest: (() => {
36
56
  const i = rawArgs.indexOf("--manifest");
37
57
  return i !== -1 ? rawArgs[i + 1] : null;
38
58
  })(),
59
+ phase: (() => {
60
+ const i = rawArgs.indexOf("--phase");
61
+ return i !== -1 ? rawArgs[i + 1] : null;
62
+ })(),
63
+ index: (() => {
64
+ const i = rawArgs.indexOf("--index");
65
+ if (i === -1) return undefined;
66
+ const val = parseInt(rawArgs[i + 1], 10);
67
+ return isNaN(val) ? 0 : val;
68
+ })(),
69
+ skip: (() => {
70
+ const result = [];
71
+ rawArgs.forEach((a, i) => {
72
+ if (a === "--skip" && rawArgs[i + 1]) result.push(rawArgs[i + 1]);
73
+ });
74
+ return result;
75
+ })(),
76
+ headers: (() => {
77
+ const result = {};
78
+ rawArgs.forEach((a, i) => {
79
+ if (a === "--header" && rawArgs[i + 1]) {
80
+ const val = rawArgs[i + 1];
81
+ const colonIdx = val.indexOf(":");
82
+ if (colonIdx !== -1) {
83
+ result[val.slice(0, colonIdx).trim()] = val.slice(colonIdx + 1).trim();
84
+ }
85
+ }
86
+ });
87
+ return result;
88
+ })(),
39
89
  };
40
90
 
41
91
  const input = rawArgs.filter((a, i) => {
42
92
  if (a.startsWith("-")) return false;
43
- if (i > 0 && rawArgs[i - 1] === "--manifest") return false;
93
+ if (i > 0 && flagValueArgs.includes(rawArgs[i - 1])) return false;
44
94
  return true;
45
95
  });
46
96
 
@@ -0,0 +1,16 @@
1
+ module.exports = function resolveNamespace(partial, connectedServices) {
2
+ if (!partial || !connectedServices || !connectedServices.length) return [];
3
+ const matches = [];
4
+ for (const { serviceId, system } of connectedServices) {
5
+ const modules = (system && system.connectionData && system.connectionData.modules) || [];
6
+ for (const { name: moduleName, methods = [] } of modules) {
7
+ for (const { fn: methodName } of methods) {
8
+ const full = `${serviceId}.${moduleName}.${methodName}`;
9
+ if (full.includes(partial)) {
10
+ matches.push({ serviceId, moduleName, methodName });
11
+ }
12
+ }
13
+ }
14
+ }
15
+ return matches;
16
+ };
@@ -0,0 +1,31 @@
1
+ class TruncationMarker {
2
+ constructor(message) {
3
+ this.message = message;
4
+ }
5
+ toJSON() {
6
+ return this.message;
7
+ }
8
+ }
9
+
10
+ function truncate(value, maxItems = 3, maxStrLen = 100) {
11
+ if (Array.isArray(value)) {
12
+ const slice = value.slice(0, maxItems).map((item) => truncate(item, maxItems, maxStrLen));
13
+ if (value.length > maxItems) slice.push(new TruncationMarker(`...${value.length - maxItems} more`));
14
+ return slice;
15
+ }
16
+ if (value !== null && typeof value === "object") {
17
+ const keys = Object.keys(value);
18
+ const result = {};
19
+ keys.slice(0, maxItems).forEach((k) => {
20
+ result[k] = truncate(value[k], maxItems, maxStrLen);
21
+ });
22
+ if (keys.length > maxItems) result.__more__ = new TruncationMarker(`...${keys.length - maxItems} more`);
23
+ return result;
24
+ }
25
+ if (typeof value === "string" && value.length > maxStrLen) {
26
+ return value.slice(0, maxStrLen) + `...[${value.length - maxStrLen} more chars]`;
27
+ }
28
+ return value;
29
+ }
30
+
31
+ module.exports = { truncate, TruncationMarker };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "systemview",
3
3
  "description": "A documentation and testing suite for SystemLynx",
4
- "version": "1.18.1",
4
+ "version": "1.20.0",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
7
7
  "systemview": "cli/index.js"