systemview 1.20.1 → 1.21.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/README.md +76 -40
- package/api/Connections.js +14 -0
- package/api/connections.json +1 -1
- package/api/index.js +40 -12
- package/build/asset-manifest.json +8 -6
- package/build/index.html +1 -1
- package/build/static/css/main.8e85f0c8.css +15 -0
- package/build/static/css/main.8e85f0c8.css.map +1 -0
- package/build/static/js/422.05c538a9.chunk.js +2 -0
- package/build/static/js/422.05c538a9.chunk.js.map +1 -0
- package/build/static/js/main.6240c0f1.js +3 -0
- package/build/static/js/{main.c3485d98.js.LICENSE.txt → main.6240c0f1.js.LICENSE.txt} +26 -48
- package/build/static/js/main.6240c0f1.js.map +1 -0
- package/cli/connectService.js +147 -55
- package/cli/cookieClient.js +1 -4
- package/cli/index.js +79 -25
- package/cli/launchApp.js +5 -4
- package/cli/listTests.js +15 -19
- package/cli/logs.js +251 -0
- package/cli/manifest.js +97 -0
- package/cli/probe.js +36 -19
- package/cli/runTests.js +35 -40
- package/cli/startLineReader.js +122 -13
- package/cli/utils/cli.js +59 -11
- package/package.json +3 -3
- package/testing-utilities/Test.class.js +2 -1
- package/testing-utilities/transformTests.js +7 -6
- package/build/static/css/main.3396e24d.css +0 -15
- package/build/static/css/main.3396e24d.css.map +0 -1
- package/build/static/js/main.c3485d98.js +0 -3
- package/build/static/js/main.c3485d98.js.map +0 -1
package/cli/logs.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const log = require("./logger");
|
|
3
|
+
const { createClient } = require("systemlynx");
|
|
4
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
5
|
+
const cookieHttpClient = createCookieHttpClient();
|
|
6
|
+
const Client = createClient(cookieHttpClient);
|
|
7
|
+
|
|
8
|
+
const LEVEL_COLOR = {
|
|
9
|
+
trace: (s) => chalk.dim(s),
|
|
10
|
+
info: (s) => chalk.white(s),
|
|
11
|
+
warn: (s) => chalk.yellow(s),
|
|
12
|
+
error: (s) => chalk.red(s),
|
|
13
|
+
debug: (s) => chalk.blue(s),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function colorLevel(level) {
|
|
17
|
+
const fn = LEVEL_COLOR[level] || ((s) => s);
|
|
18
|
+
return fn((level || "info").padEnd(5));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SKIP_KEYS = new Set([
|
|
22
|
+
"timestamp",
|
|
23
|
+
"projectCode",
|
|
24
|
+
"serviceId",
|
|
25
|
+
"moduleMethod",
|
|
26
|
+
"traceId",
|
|
27
|
+
"level",
|
|
28
|
+
"message",
|
|
29
|
+
"duration",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function formatRow(entry, verbose, extraFields) {
|
|
33
|
+
const d = new Date(entry.timestamp);
|
|
34
|
+
const time =
|
|
35
|
+
d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) +
|
|
36
|
+
" " +
|
|
37
|
+
d.toLocaleTimeString("en-US", { hour12: false });
|
|
38
|
+
const project = entry.projectCode || "—";
|
|
39
|
+
const service = entry.serviceId || "—";
|
|
40
|
+
const method = entry.moduleMethod || "—";
|
|
41
|
+
const level = colorLevel(entry.level);
|
|
42
|
+
const msg = entry.message || "";
|
|
43
|
+
const dur = entry.duration != null ? chalk.dim(` ${entry.duration}ms`) : "";
|
|
44
|
+
const traceId = chalk.dim(entry.traceId || "—");
|
|
45
|
+
let row = ` ${chalk.dim(time)} ${chalk.cyan(project)} › ${chalk.cyan(service)} ${method.padEnd(15)} ${level} ${traceId} ${msg}${dur}`;
|
|
46
|
+
if (extraFields && extraFields.length) {
|
|
47
|
+
const parts = extraFields.map((p) => `${chalk.dim(p + ":")} ${cellStr(getAtPath(entry, p))}`);
|
|
48
|
+
row += " " + parts.join(" ");
|
|
49
|
+
}
|
|
50
|
+
if (verbose) {
|
|
51
|
+
const extra = {};
|
|
52
|
+
Object.keys(entry).forEach((k) => {
|
|
53
|
+
if (!SKIP_KEYS.has(k)) extra[k] = entry[k];
|
|
54
|
+
});
|
|
55
|
+
if (Object.keys(extra).length) {
|
|
56
|
+
return (
|
|
57
|
+
row +
|
|
58
|
+
"\n" +
|
|
59
|
+
JSON.stringify(extra, null, 2)
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((l) => " " + chalk.yellow(l))
|
|
62
|
+
.join("\n")
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return row;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getAtPath(entry, field) {
|
|
70
|
+
return field.split(".").reduce((o, k) => (o != null ? o[k] : undefined), entry);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cellStr(v) {
|
|
74
|
+
if (v === null || v === undefined) return "—";
|
|
75
|
+
if (typeof v === "object") return Array.isArray(v) ? `[${v.length}]` : "{…}";
|
|
76
|
+
return String(v);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseFilters(filters) {
|
|
80
|
+
return (filters || [])
|
|
81
|
+
.map((f) => {
|
|
82
|
+
const eq = f.indexOf("=");
|
|
83
|
+
if (eq === -1) return null;
|
|
84
|
+
return { field: f.slice(0, eq), value: f.slice(eq + 1) };
|
|
85
|
+
})
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function matchesFilters(entry, andFilters, orFilters) {
|
|
90
|
+
const check = ({ field, value }) => {
|
|
91
|
+
if (field === "has") return getAtPath(entry, value) != null;
|
|
92
|
+
if (field === "missing") return getAtPath(entry, value) == null;
|
|
93
|
+
const v = getAtPath(entry, field);
|
|
94
|
+
return v != null && String(v).includes(value);
|
|
95
|
+
};
|
|
96
|
+
const andPass = andFilters.length === 0 || andFilters.every(check);
|
|
97
|
+
const orPass = orFilters.length === 0 || orFilters.some(check);
|
|
98
|
+
if (andFilters.length && orFilters.length) return andPass || orPass;
|
|
99
|
+
return andPass && orPass;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const DISPLAY_SKIP = new Set(["timestamp", "projectCode", "serviceId", "moduleMethod", "traceId", "level", "message", "duration"]);
|
|
103
|
+
|
|
104
|
+
function filterDisplayField(f) {
|
|
105
|
+
if (f.field === "has" || f.field === "missing") return f.value;
|
|
106
|
+
return f.field;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = async function logsCommand(
|
|
110
|
+
projectCode,
|
|
111
|
+
namespace,
|
|
112
|
+
{ uiUrl, level, limit = 50, clear, current, verbose, filter, or: orFilters, include },
|
|
113
|
+
) {
|
|
114
|
+
const andFilters = parseFilters(filter);
|
|
115
|
+
const orFiltersParsed = parseFilters(orFilters);
|
|
116
|
+
const extraFields = [
|
|
117
|
+
...andFilters.map(filterDisplayField),
|
|
118
|
+
...orFiltersParsed.map(filterDisplayField),
|
|
119
|
+
...(include || []),
|
|
120
|
+
].filter((f, i, arr) => !DISPLAY_SKIP.has(f) && arr.indexOf(f) === i);
|
|
121
|
+
|
|
122
|
+
let services = [];
|
|
123
|
+
let effectiveNamespace = namespace;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
|
|
127
|
+
|
|
128
|
+
if (!projectCode) {
|
|
129
|
+
const projects = await SystemView.getProjects();
|
|
130
|
+
services = Object.values(projects).flat();
|
|
131
|
+
} else {
|
|
132
|
+
const matches = await SystemView.getServices(projectCode);
|
|
133
|
+
if (matches && matches.length) {
|
|
134
|
+
services = matches;
|
|
135
|
+
} else {
|
|
136
|
+
// Fuzzy namespace: check if arg is a module/method name
|
|
137
|
+
const projects = await SystemView.getProjects();
|
|
138
|
+
const all = Object.values(projects).flat();
|
|
139
|
+
const isNamespace = all.some(({ system }) =>
|
|
140
|
+
(system.connectionData.modules || []).some(({ name, methods }) =>
|
|
141
|
+
name.toLowerCase().includes(projectCode.toLowerCase()) ||
|
|
142
|
+
(methods || []).some(({ fn }) => fn.toLowerCase().includes(projectCode.toLowerCase()))
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
if (isNamespace) {
|
|
146
|
+
services = all;
|
|
147
|
+
effectiveNamespace = projectCode;
|
|
148
|
+
} else {
|
|
149
|
+
log.warn(`No services found for: ${projectCode}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
log.error("Failed to connect to SystemView: " + err.message);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!services.length) {
|
|
160
|
+
log.warn("No services in store. Use: systemview connect <url>");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (clear) {
|
|
165
|
+
const confirmed = await promptConfirm("Clear all logs? (y/N) ");
|
|
166
|
+
if (!confirmed) { log.warn("Aborted."); return; }
|
|
167
|
+
for (const { system } of services) {
|
|
168
|
+
try {
|
|
169
|
+
const svc = await Client.loadService(system.connectionData.serviceUrl);
|
|
170
|
+
await svc.SystemView.clearLog();
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
log.success("Logs cleared.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log("");
|
|
178
|
+
console.log(chalk.bold(" Streaming logs"));
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(chalk.dim(` Services (${services.length}):`));
|
|
181
|
+
|
|
182
|
+
const connected = [];
|
|
183
|
+
for (const { serviceId, system } of services) {
|
|
184
|
+
const { serviceUrl } = system.connectionData;
|
|
185
|
+
try {
|
|
186
|
+
const svc = await Client.loadService(serviceUrl);
|
|
187
|
+
const unsub = svc.SystemView.on("log", (entry) => {
|
|
188
|
+
if (effectiveNamespace && !(entry.moduleMethod && entry.moduleMethod.includes(effectiveNamespace))) return;
|
|
189
|
+
if (andFilters.length || orFiltersParsed.length) {
|
|
190
|
+
if (!matchesFilters(entry, andFilters, orFiltersParsed)) return;
|
|
191
|
+
}
|
|
192
|
+
if (level && entry.level !== level) return;
|
|
193
|
+
console.log(formatRow(entry, verbose, extraFields));
|
|
194
|
+
});
|
|
195
|
+
console.log(` ${chalk.green("✓")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)}`);
|
|
196
|
+
connected.push({ serviceId, svc, unsub });
|
|
197
|
+
} catch {
|
|
198
|
+
console.log(` ${chalk.red("✗")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)} ${chalk.red("(failed to connect)")}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const activeFilters = [];
|
|
203
|
+
if (effectiveNamespace) activeFilters.push(`namespace: ${chalk.white(effectiveNamespace)}`);
|
|
204
|
+
if (level) activeFilters.push(`level: ${chalk.white(level)}`);
|
|
205
|
+
if (andFilters.length) activeFilters.push(`filter: ${chalk.white(andFilters.map((f) => `${f.field}=${f.value}`).join(", "))}`);
|
|
206
|
+
if (orFiltersParsed.length) activeFilters.push(`or: ${chalk.white(orFiltersParsed.map((f) => `${f.field}=${f.value}`).join(", "))}`);
|
|
207
|
+
if (extraFields.length) activeFilters.push(`include: ${chalk.white(extraFields.join(", "))}`);
|
|
208
|
+
if (verbose) activeFilters.push(`verbose: ${chalk.white("on")}`);
|
|
209
|
+
if (activeFilters.length) {
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log(chalk.dim(` Filters:`));
|
|
212
|
+
activeFilters.forEach((f) => console.log(` ${chalk.dim(f)}`));
|
|
213
|
+
}
|
|
214
|
+
console.log("");
|
|
215
|
+
|
|
216
|
+
if (!connected.length) {
|
|
217
|
+
log.warn("No services connected.");
|
|
218
|
+
return () => {};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (current) {
|
|
222
|
+
const allEntries = [];
|
|
223
|
+
for (const { svc } of connected) {
|
|
224
|
+
try {
|
|
225
|
+
let entries = await svc.SystemView.getLog({ limit });
|
|
226
|
+
entries = entries || [];
|
|
227
|
+
if (effectiveNamespace) entries = entries.filter((e) => e.moduleMethod && e.moduleMethod.includes(effectiveNamespace));
|
|
228
|
+
if (andFilters.length || orFiltersParsed.length) entries = entries.filter((e) => matchesFilters(e, andFilters, orFiltersParsed));
|
|
229
|
+
if (level) entries = entries.filter((e) => e.level === level);
|
|
230
|
+
allEntries.push(...entries);
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
if (allEntries.length) {
|
|
234
|
+
console.log(chalk.dim(` ── current (${allEntries.length}) ──`));
|
|
235
|
+
allEntries.forEach((e) => console.log(formatRow(e, verbose, extraFields)));
|
|
236
|
+
console.log(chalk.dim(` ── streaming ──`));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return () => { connected.forEach(({ unsub }) => unsub()); };
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
function promptConfirm(question) {
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
process.stdout.write(question);
|
|
246
|
+
process.stdin.setEncoding("utf8");
|
|
247
|
+
process.stdin.once("data", (data) => {
|
|
248
|
+
resolve(data.trim().toLowerCase() === "y");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
package/cli/manifest.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { HttpClient, createClient } = require("systemlynx");
|
|
4
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
5
|
+
const cookieHttpClient = createCookieHttpClient();
|
|
6
|
+
const Client = createClient(cookieHttpClient);
|
|
7
|
+
const log = require("./logger");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MANIFEST = path.join(process.cwd(), "systemview.manifest.json");
|
|
10
|
+
|
|
11
|
+
async function getUiSvc(uiUrl) {
|
|
12
|
+
try {
|
|
13
|
+
const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
|
|
14
|
+
return SystemView;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports.save = function saveManifest(manifestServices, manifestFile = DEFAULT_MANIFEST, probeHeaders = {}) {
|
|
21
|
+
if (!manifestServices || !manifestServices.length) {
|
|
22
|
+
log.warn("No services in session to save.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const byProject = {};
|
|
26
|
+
for (const { projectCode, serviceId, system, specList } of manifestServices) {
|
|
27
|
+
if (!byProject[projectCode]) byProject[projectCode] = [];
|
|
28
|
+
byProject[projectCode].push({ serviceId, system, specList });
|
|
29
|
+
}
|
|
30
|
+
const projectCodes = Object.keys(byProject);
|
|
31
|
+
if (projectCodes.length > 1) {
|
|
32
|
+
log.warn("Multiple project codes in session — saving first project only: " + projectCodes[0]);
|
|
33
|
+
}
|
|
34
|
+
const projectCode = projectCodes[0];
|
|
35
|
+
const manifest = {
|
|
36
|
+
projectCode,
|
|
37
|
+
services: byProject[projectCode],
|
|
38
|
+
...(Object.keys(probeHeaders).length && { probeHeaders }),
|
|
39
|
+
};
|
|
40
|
+
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
|
|
41
|
+
log.success(`Manifest saved to ${manifestFile} (${manifest.services.length} service(s))`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
module.exports.clean = async function cleanManifest(manifestFile = DEFAULT_MANIFEST) {
|
|
45
|
+
if (!fs.existsSync(manifestFile)) {
|
|
46
|
+
log.warn("No manifest file found at " + manifestFile);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let manifest;
|
|
50
|
+
try {
|
|
51
|
+
manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
|
|
52
|
+
} catch {
|
|
53
|
+
log.error("Failed to parse manifest.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const services = manifest.services || [manifest];
|
|
57
|
+
log.info(`Re-probing ${services.length} service(s)...`);
|
|
58
|
+
const alive = [];
|
|
59
|
+
for (const entry of services) {
|
|
60
|
+
const url = entry.system && entry.system.connectionData && entry.system.connectionData.serviceUrl;
|
|
61
|
+
if (!url) continue;
|
|
62
|
+
try {
|
|
63
|
+
await HttpClient.request({ url });
|
|
64
|
+
alive.push(entry);
|
|
65
|
+
log.success(`alive: ${entry.serviceId}`);
|
|
66
|
+
} catch {
|
|
67
|
+
log.warn(`stale: ${entry.serviceId} (removed)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
manifest.services = alive;
|
|
71
|
+
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
|
|
72
|
+
log.success(`Manifest cleaned. ${alive.length} service(s) remaining.`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports.disconnect = async function disconnect(projectCode, serviceId, { connectedUrls, uiUrl } = {}) {
|
|
76
|
+
if (!projectCode) {
|
|
77
|
+
log.warn("Usage: systemview disconnect <projectCode> [serviceId]");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const uiSvc = await getUiSvc(uiUrl);
|
|
81
|
+
if (serviceId) {
|
|
82
|
+
if (uiSvc) {
|
|
83
|
+
try { await uiSvc.deleteService(projectCode, serviceId); } catch {}
|
|
84
|
+
}
|
|
85
|
+
if (connectedUrls) {
|
|
86
|
+
for (const url of connectedUrls) {
|
|
87
|
+
if (url.includes(serviceId)) connectedUrls.delete(url);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
log.success(`Disconnected: ${projectCode} › ${serviceId}`);
|
|
91
|
+
} else {
|
|
92
|
+
if (uiSvc) {
|
|
93
|
+
try { await uiSvc.deleteProject(projectCode); } catch {}
|
|
94
|
+
}
|
|
95
|
+
log.success(`Disconnected project: ${projectCode}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
package/cli/probe.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const {
|
|
3
|
+
const { createClient } = require("systemlynx");
|
|
4
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
4
5
|
const log = require("./logger");
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const cookieHttpClient = createCookieHttpClient();
|
|
8
|
+
const Client = createClient(cookieHttpClient);
|
|
9
|
+
|
|
10
|
+
module.exports = async function probe(namespace, argsStr, { json = false, manifest: manifestPath, headers: cliHeaders = {}, uiUrl } = {}) {
|
|
7
11
|
if (!namespace) {
|
|
8
12
|
log.error("Usage: systemview probe <ServiceId.Module.method> [args]");
|
|
9
13
|
return 1;
|
|
@@ -27,32 +31,45 @@ module.exports = async function probe(namespace, argsStr, { json = false, manife
|
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
if (!fs.existsSync(manifestFile)) {
|
|
32
|
-
log.error(`No manifest found at ${manifestFile}. Run: systemview connect <serviceId> <url>`);
|
|
33
|
-
return 1;
|
|
34
|
-
}
|
|
34
|
+
let service = null;
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// Try UI server first
|
|
37
|
+
if (uiUrl) {
|
|
38
|
+
try {
|
|
39
|
+
const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
|
|
40
|
+
const projects = await SystemView.getProjects();
|
|
41
|
+
const all = Object.values(projects).flat();
|
|
42
|
+
service = all.find((s) => s.serviceId === serviceId);
|
|
43
|
+
} catch {}
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
const service = services.find((s) => s.serviceId === serviceId);
|
|
46
|
+
// Fall back to manifest file
|
|
46
47
|
if (!service) {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const manifestFile = manifestPath || path.join(process.cwd(), "systemview.manifest.json");
|
|
49
|
+
if (!fs.existsSync(manifestFile)) {
|
|
50
|
+
log.error(`Service "${serviceId}" not found. Connect it first with: systemview connect <url>`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
let manifest;
|
|
54
|
+
try {
|
|
55
|
+
manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
log.error(`Failed to read manifest: ${err.message}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const services = manifest.services || [manifest];
|
|
61
|
+
service = services.find((s) => s.serviceId === serviceId);
|
|
62
|
+
if (!service) {
|
|
63
|
+
log.error(`Service "${serviceId}" not found in manifest`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
const extraHeaders = { ...(manifest.probeHeaders || {}), ...cliHeaders };
|
|
52
68
|
if (!json) log.info(`${serviceId}.${moduleName}.${methodName}(${argsStr || ""})`);
|
|
53
69
|
|
|
54
70
|
try {
|
|
55
|
-
const client =
|
|
71
|
+
const client = Client.createService(service.system.connectionData);
|
|
72
|
+
if (Object.keys(cliHeaders).length) client.setHeaders(cliHeaders);
|
|
56
73
|
const result = await client[moduleName][methodName](...args);
|
|
57
74
|
if (json) {
|
|
58
75
|
process.stdout.write(JSON.stringify({ serviceId, moduleName, methodName, args, result }, null, 2) + "\n");
|
package/cli/runTests.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const { createClient } = require("systemlynx");
|
|
2
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
3
|
+
const cookieHttpClient = createCookieHttpClient();
|
|
4
|
+
const Client = createClient(cookieHttpClient);
|
|
4
5
|
const { initializeSavedTests } = require("../testing-utilities/transformTests");
|
|
5
6
|
const FullTestController = require("../testing-utilities/FullTestController");
|
|
6
7
|
const log = require("./logger");
|
|
@@ -45,7 +46,6 @@ module.exports = async function runTests(
|
|
|
45
46
|
{
|
|
46
47
|
json = false,
|
|
47
48
|
verbose = false,
|
|
48
|
-
manifest: manifestPath,
|
|
49
49
|
headers: cliHeaders = {},
|
|
50
50
|
bail = false,
|
|
51
51
|
dryRun = false,
|
|
@@ -61,23 +61,25 @@ module.exports = async function runTests(
|
|
|
61
61
|
return 1;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const { services: connectedServices,
|
|
65
|
-
|
|
66
|
-
project_code,
|
|
67
|
-
manifestPath
|
|
68
|
-
);
|
|
64
|
+
const { services: connectedServices, resolvedNamespace } = await resolveServices(api, project_code);
|
|
65
|
+
if (resolvedNamespace && !namespace) namespace = resolvedNamespace;
|
|
69
66
|
if (!connectedServices || !connectedServices.length) {
|
|
70
67
|
log.warn("No connected services found for project: " + project_code);
|
|
71
68
|
return 1;
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
const extraHeaders = { ...
|
|
75
|
-
const Client = createCookieClient(extraHeaders);
|
|
71
|
+
const extraHeaders = { ...cliHeaders };
|
|
76
72
|
|
|
77
73
|
if (!json) {
|
|
78
74
|
connectedServices.forEach(({ serviceId, system }) => {
|
|
79
75
|
log.success(`connected: ${serviceId} @ ${system.connectionData.serviceUrl}`);
|
|
80
76
|
});
|
|
77
|
+
if (namespace || skipPatterns.length) {
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log(chalk.dim(` Filters:`));
|
|
80
|
+
if (namespace) console.log(` ${chalk.dim(`namespace: ${chalk.white(namespace)}`)}`);
|
|
81
|
+
if (skipPatterns.length) console.log(` ${chalk.dim(`skip: ${chalk.white(skipPatterns.join(", "))}`)}`);
|
|
82
|
+
}
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
const allTestLists = await getTests(connectedServices, Client);
|
|
@@ -121,7 +123,7 @@ module.exports = async function runTests(
|
|
|
121
123
|
if (!json) log.info(`Initializing tests for ${serviceId}...`);
|
|
122
124
|
|
|
123
125
|
try {
|
|
124
|
-
const initialized = initializeSavedTests(testList, connectedServices, Client);
|
|
126
|
+
const initialized = initializeSavedTests(testList, connectedServices, Client, extraHeaders);
|
|
125
127
|
const { passed, failed, jsonTests, failures } = await runAllTests(
|
|
126
128
|
initialized,
|
|
127
129
|
url,
|
|
@@ -354,39 +356,32 @@ async function getTests(connectedServices, Client) {
|
|
|
354
356
|
return results;
|
|
355
357
|
}
|
|
356
358
|
|
|
357
|
-
async function resolveServices(api, project_code
|
|
358
|
-
|
|
359
|
+
async function resolveServices(api, project_code) {
|
|
360
|
+
try {
|
|
361
|
+
const { SystemView } = await Client.loadService(api);
|
|
359
362
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
? raw.services
|
|
365
|
-
: [{ serviceId: raw.serviceId, system: raw.system, specList: raw.specList }];
|
|
366
|
-
|
|
367
|
-
if (project_code && raw.projectCode && raw.projectCode !== project_code) {
|
|
368
|
-
log.warn(
|
|
369
|
-
`Manifest projectCode "${raw.projectCode}" doesn't match "${project_code}", falling back to API`
|
|
370
|
-
);
|
|
371
|
-
} else {
|
|
372
|
-
return { services, probeHeaders: raw.probeHeaders || {} };
|
|
373
|
-
}
|
|
374
|
-
} catch (err) {
|
|
375
|
-
log.warn(`Failed to read manifest: ${err.message}`);
|
|
363
|
+
// Try exact project code match first
|
|
364
|
+
const services = await SystemView.getServices(project_code);
|
|
365
|
+
if (services && services.length) {
|
|
366
|
+
return { services };
|
|
376
367
|
}
|
|
377
|
-
}
|
|
378
368
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
369
|
+
// Fuzzy namespace: check if project_code matches any module or method across all services
|
|
370
|
+
const projects = await SystemView.getProjects();
|
|
371
|
+
const all = Object.values(projects).flat();
|
|
372
|
+
const isNamespace = all.some(({ system }) =>
|
|
373
|
+
(system.connectionData.modules || []).some(({ name, methods }) =>
|
|
374
|
+
name.toLowerCase().includes(project_code.toLowerCase()) ||
|
|
375
|
+
(methods || []).some(({ fn }) => fn.toLowerCase().includes(project_code.toLowerCase()))
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
if (isNamespace) {
|
|
379
|
+
return { services: all, resolvedNamespace: project_code };
|
|
380
|
+
}
|
|
383
381
|
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
const { SystemView } = await Client.loadService(api);
|
|
387
|
-
return await SystemView.getServices(project_code);
|
|
382
|
+
return { services: [] };
|
|
388
383
|
} catch (error) {
|
|
389
384
|
log.error("Failed to connect to SystemView: " + error.message);
|
|
390
|
-
return [];
|
|
385
|
+
return { services: [] };
|
|
391
386
|
}
|
|
392
387
|
}
|