@xano/cli 1.0.6-beta.1 → 1.0.6-beta.11
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 +138 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
- package/dist/commands/ephemeral/impersonate/index.js +11 -3
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +0 -6
- package/dist/commands/ephemeral/pull/index.js +10 -95
- package/dist/commands/ephemeral/push/index.d.ts +10 -0
- package/dist/commands/ephemeral/push/index.js +77 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
- package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/list/index.js +20 -40
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run/index.js +17 -11
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +76 -38
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +74 -36
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +10 -95
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run/index.js +19 -11
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +80 -38
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run/index.js +14 -9
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +78 -36
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run/index.js +19 -11
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +81 -41
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run/index.js +15 -10
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +82 -39
- package/dist/commands/workspace/git/pull/index.js +53 -3
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -95
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +274 -1
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -5
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +83 -0
- package/dist/utils/multidoc-push.d.ts +12 -0
- package/dist/utils/multidoc-push.js +84 -0
- package/dist/utils/paging.d.ts +145 -0
- package/dist/utils/paging.js +199 -0
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +3420 -3208
- package/package.json +1 -1
|
@@ -61,24 +61,29 @@ Result: PASS (0.25s)
|
|
|
61
61
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
62
62
|
}
|
|
63
63
|
const result = (await response.json());
|
|
64
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
65
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
66
|
+
const passed = result.status === 'ok';
|
|
64
67
|
if (flags.output === 'json') {
|
|
65
68
|
this.log(JSON.stringify(result, null, 2));
|
|
66
69
|
}
|
|
67
70
|
else {
|
|
68
71
|
const timing = result.timing ? ` (${result.timing}s)` : '';
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
else {
|
|
73
|
-
this.log(`Result: FAIL${timing}`);
|
|
74
|
-
if (result.message) {
|
|
75
|
-
this.log(` Error: ${result.message}`);
|
|
76
|
-
}
|
|
77
|
-
this.exit(1);
|
|
72
|
+
this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
|
|
73
|
+
if (!passed && result.message) {
|
|
74
|
+
this.log(` Error: ${result.message}`);
|
|
78
75
|
}
|
|
79
76
|
}
|
|
77
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
78
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
79
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
80
|
+
if (!passed) {
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
80
83
|
}
|
|
81
84
|
catch (error) {
|
|
85
|
+
if (error instanceof Error && 'oclif' in error)
|
|
86
|
+
throw error;
|
|
82
87
|
if (error instanceof Error) {
|
|
83
88
|
this.error(`Failed to run workflow test: ${error.message}`);
|
|
84
89
|
}
|
|
@@ -4,6 +4,7 @@ export default class TenantWorkflowTestRunAll extends BaseCommand {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../../utils/paging.js';
|
|
3
5
|
export default class TenantWorkflowTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all workflow tests for a tenant';
|
|
5
7
|
static examples = [
|
|
@@ -21,6 +23,11 @@ Results: 2 passed, 1 failed
|
|
|
21
23
|
description: 'Filter by branch name',
|
|
22
24
|
required: false,
|
|
23
25
|
}),
|
|
26
|
+
concurrency: Flags.integer({
|
|
27
|
+
default: 1,
|
|
28
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
24
31
|
output: Flags.string({
|
|
25
32
|
char: 'o',
|
|
26
33
|
default: 'summary',
|
|
@@ -49,41 +56,59 @@ Results: 2 passed, 1 failed
|
|
|
49
56
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
50
57
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test`;
|
|
51
58
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
60
|
+
// request is plenty for typical suites and does not make the instance
|
|
61
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
62
|
+
// never "the page came back full".
|
|
63
|
+
const branchValue = flags.branch;
|
|
64
|
+
const tests = await collectAllPages(async (page) => {
|
|
65
|
+
const listParams = new URLSearchParams();
|
|
66
|
+
listParams.set('page', String(page));
|
|
67
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
68
|
+
if (branchValue)
|
|
69
|
+
listParams.set('branch', branchValue);
|
|
70
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
71
|
+
headers: {
|
|
72
|
+
accept: 'application/json',
|
|
73
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
74
|
+
},
|
|
75
|
+
method: 'GET',
|
|
76
|
+
}, flags.verbose, profile.access_token);
|
|
77
|
+
if (!listResponse.ok) {
|
|
78
|
+
const errorText = await listResponse.text();
|
|
79
|
+
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
return normalizeListResponse(await listResponse.json());
|
|
82
|
+
}, {
|
|
83
|
+
onTruncate: (collected) => {
|
|
84
|
+
this.warn(`Stopped after ${collected} workflow tests — the list did not terminate. Some tests may not have run.`);
|
|
60
85
|
},
|
|
61
|
-
|
|
62
|
-
}, flags.verbose, profile.access_token);
|
|
63
|
-
if (!listResponse.ok) {
|
|
64
|
-
const errorText = await listResponse.text();
|
|
65
|
-
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
66
|
-
}
|
|
67
|
-
const data = (await listResponse.json());
|
|
68
|
-
let tests;
|
|
69
|
-
if (Array.isArray(data)) {
|
|
70
|
-
tests = data;
|
|
71
|
-
}
|
|
72
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
73
|
-
tests = data.items;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
this.error('Unexpected API response format');
|
|
77
|
-
}
|
|
86
|
+
});
|
|
78
87
|
if (tests.length === 0) {
|
|
79
|
-
|
|
88
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
89
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
90
|
+
// with the same keys the populated path emits below.
|
|
91
|
+
if (flags.output === 'json') {
|
|
92
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.log('No workflow tests found');
|
|
96
|
+
}
|
|
80
97
|
return;
|
|
81
98
|
}
|
|
82
99
|
if (flags.output === 'summary') {
|
|
83
100
|
this.log(`Running ${tests.length} workflow test${tests.length === 1 ? '' : 's'}...\n`);
|
|
84
101
|
}
|
|
85
102
|
const results = [];
|
|
86
|
-
|
|
103
|
+
const runOne = async (test) => {
|
|
104
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
105
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
106
|
+
// run can still emit in input order.
|
|
107
|
+
const results = [];
|
|
108
|
+
const lines = [];
|
|
109
|
+
const log = (message) => {
|
|
110
|
+
lines.push(message);
|
|
111
|
+
};
|
|
87
112
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
88
113
|
try {
|
|
89
114
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -102,10 +127,10 @@ Results: 2 passed, 1 failed
|
|
|
102
127
|
status: 'fail',
|
|
103
128
|
});
|
|
104
129
|
if (flags.output === 'summary') {
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
log(`FAIL ${test.name}`);
|
|
131
|
+
log(` Error: API error ${runResponse.status}`);
|
|
107
132
|
}
|
|
108
|
-
|
|
133
|
+
return { lines, results };
|
|
109
134
|
}
|
|
110
135
|
const runResult = (await runResponse.json());
|
|
111
136
|
const passed = runResult.status === 'ok';
|
|
@@ -118,17 +143,19 @@ Results: 2 passed, 1 failed
|
|
|
118
143
|
if (flags.output === 'summary') {
|
|
119
144
|
const timing = runResult.timing ? ` (${runResult.timing}s)` : '';
|
|
120
145
|
if (passed) {
|
|
121
|
-
|
|
146
|
+
log(`PASS ${test.name}${timing}`);
|
|
122
147
|
}
|
|
123
148
|
else {
|
|
124
|
-
|
|
149
|
+
log(`FAIL ${test.name}${timing}`);
|
|
125
150
|
if (runResult.message) {
|
|
126
|
-
|
|
151
|
+
log(` Error: ${runResult.message}`);
|
|
127
152
|
}
|
|
128
153
|
}
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
catch (error) {
|
|
157
|
+
if (error instanceof Error && 'oclif' in error)
|
|
158
|
+
throw error;
|
|
132
159
|
const message = error instanceof Error ? error.message : String(error);
|
|
133
160
|
results.push({
|
|
134
161
|
message,
|
|
@@ -136,11 +163,24 @@ Results: 2 passed, 1 failed
|
|
|
136
163
|
status: 'fail',
|
|
137
164
|
});
|
|
138
165
|
if (flags.output === 'summary') {
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
log(`FAIL ${test.name}`);
|
|
167
|
+
log(` Error: ${message}`);
|
|
141
168
|
}
|
|
142
169
|
}
|
|
143
|
-
|
|
170
|
+
return { lines, results };
|
|
171
|
+
};
|
|
172
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
173
|
+
// diffable against a sequential run.
|
|
174
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
175
|
+
for (const line of settled.lines)
|
|
176
|
+
this.log(line);
|
|
177
|
+
});
|
|
178
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
179
|
+
const settled = await runOne(test);
|
|
180
|
+
emitter.settle(index, settled);
|
|
181
|
+
return settled;
|
|
182
|
+
});
|
|
183
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
144
184
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
145
185
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
146
186
|
if (flags.output === 'json') {
|
|
@@ -154,6 +194,8 @@ Results: 2 passed, 1 failed
|
|
|
154
194
|
}
|
|
155
195
|
}
|
|
156
196
|
catch (error) {
|
|
197
|
+
if (error instanceof Error && 'oclif' in error)
|
|
198
|
+
throw error;
|
|
157
199
|
if (error instanceof Error) {
|
|
158
200
|
this.error(`Failed to run workflow tests: ${error.message}`);
|
|
159
201
|
}
|
|
@@ -3,10 +3,12 @@ export default class UnitTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
10
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
11
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../utils/paging.js';
|
|
3
4
|
export default class UnitTestList extends BaseCommand {
|
|
4
5
|
static description = 'List all unit tests in a workspace';
|
|
5
6
|
static examples = [
|
|
@@ -30,6 +31,7 @@ Unit tests in workspace 5:
|
|
|
30
31
|
options: ['summary', 'json'],
|
|
31
32
|
required: false,
|
|
32
33
|
}),
|
|
34
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
33
35
|
workspace: Flags.string({
|
|
34
36
|
char: 'w',
|
|
35
37
|
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
@@ -43,8 +45,7 @@ Unit tests in workspace 5:
|
|
|
43
45
|
if (!workspaceId) {
|
|
44
46
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
45
47
|
}
|
|
46
|
-
const params = new URLSearchParams();
|
|
47
|
-
params.set('per_page', '10000');
|
|
48
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
48
49
|
if (flags.branch) {
|
|
49
50
|
params.set('branch', flags.branch);
|
|
50
51
|
}
|
|
@@ -64,19 +65,10 @@ Unit tests in workspace 5:
|
|
|
64
65
|
const errorText = await response.text();
|
|
65
66
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
66
67
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (Array.isArray(data)) {
|
|
70
|
-
tests = data;
|
|
71
|
-
}
|
|
72
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
73
|
-
tests = data.items;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
this.error('Unexpected API response format');
|
|
77
|
-
}
|
|
68
|
+
const list = normalizeListResponse(await response.json());
|
|
69
|
+
const tests = list.items;
|
|
78
70
|
if (flags.output === 'json') {
|
|
79
|
-
this.log(JSON.stringify(
|
|
71
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
80
72
|
}
|
|
81
73
|
else {
|
|
82
74
|
if (tests.length === 0) {
|
|
@@ -88,6 +80,9 @@ Unit tests in workspace 5:
|
|
|
88
80
|
this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
|
|
89
81
|
}
|
|
90
82
|
}
|
|
83
|
+
const footer = formatPagingFooter(list, { noun: 'unit test', page: flags.page, tier: 'envelope' });
|
|
84
|
+
if (footer)
|
|
85
|
+
this.log(footer);
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
88
|
catch (error) {
|
|
@@ -63,26 +63,34 @@ Result: PASS
|
|
|
63
63
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
64
64
|
}
|
|
65
65
|
const result = await response.json();
|
|
66
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
67
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
68
|
+
const passed = result.status === 'ok';
|
|
66
69
|
if (flags.output === 'json') {
|
|
67
70
|
this.log(JSON.stringify(result, null, 2));
|
|
68
71
|
}
|
|
72
|
+
else if (passed) {
|
|
73
|
+
this.log('Result: PASS');
|
|
74
|
+
}
|
|
69
75
|
else {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const failedExpects = result.results?.filter(r => r.status === 'fail') ?? [];
|
|
76
|
-
for (const expect of failedExpects) {
|
|
77
|
-
if (expect.message) {
|
|
78
|
-
this.log(` Error: ${expect.message}`);
|
|
79
|
-
}
|
|
76
|
+
this.log('Result: FAIL');
|
|
77
|
+
const failedExpects = result.results?.filter(r => r.status === 'fail') ?? [];
|
|
78
|
+
for (const expect of failedExpects) {
|
|
79
|
+
if (expect.message) {
|
|
80
|
+
this.log(` Error: ${expect.message}`);
|
|
80
81
|
}
|
|
81
|
-
this.exit(1);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
85
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
86
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
87
|
+
if (!passed) {
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
84
90
|
}
|
|
85
91
|
catch (error) {
|
|
92
|
+
if (error instanceof Error && 'oclif' in error)
|
|
93
|
+
throw error;
|
|
86
94
|
if (error instanceof Error) {
|
|
87
95
|
this.error(`Failed to run unit test: ${error.message}`);
|
|
88
96
|
}
|
|
@@ -4,6 +4,7 @@ export default class UnitTestRunAll extends BaseCommand {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../utils/paging.js';
|
|
3
5
|
export default class UnitTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all unit tests in a workspace';
|
|
5
7
|
static examples = [
|
|
@@ -22,6 +24,11 @@ Results: 2 passed, 1 failed
|
|
|
22
24
|
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
23
25
|
required: false,
|
|
24
26
|
}),
|
|
27
|
+
concurrency: Flags.integer({
|
|
28
|
+
default: 1,
|
|
29
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
25
32
|
'obj-type': Flags.string({
|
|
26
33
|
description: 'Filter by object type',
|
|
27
34
|
options: ['function', 'query', 'middleware'],
|
|
@@ -53,38 +60,46 @@ Results: 2 passed, 1 failed
|
|
|
53
60
|
const branch = flags.branch ?? profile.branch;
|
|
54
61
|
try {
|
|
55
62
|
// Step 1: List all unit tests
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
listParams
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
64
|
+
// request is plenty for typical suites and does not make the instance
|
|
65
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
66
|
+
// never "the page came back full".
|
|
67
|
+
const branchValue = branch;
|
|
68
|
+
const tests = await collectAllPages(async (page) => {
|
|
69
|
+
const listParams = new URLSearchParams();
|
|
70
|
+
listParams.set('page', String(page));
|
|
71
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
72
|
+
if (branchValue)
|
|
73
|
+
listParams.set('branch', branchValue);
|
|
74
|
+
if (flags['obj-type'])
|
|
75
|
+
listParams.set('obj_type', flags['obj-type']);
|
|
76
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
77
|
+
headers: {
|
|
78
|
+
accept: 'application/json',
|
|
79
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
80
|
+
},
|
|
81
|
+
method: 'GET',
|
|
82
|
+
}, flags.verbose, profile.access_token);
|
|
83
|
+
if (!listResponse.ok) {
|
|
84
|
+
const errorText = await listResponse.text();
|
|
85
|
+
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
86
|
+
}
|
|
87
|
+
return normalizeListResponse(await listResponse.json());
|
|
88
|
+
}, {
|
|
89
|
+
onTruncate: (collected) => {
|
|
90
|
+
this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
|
|
68
91
|
},
|
|
69
|
-
|
|
70
|
-
}, flags.verbose, profile.access_token);
|
|
71
|
-
if (!listResponse.ok) {
|
|
72
|
-
const errorText = await listResponse.text();
|
|
73
|
-
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
74
|
-
}
|
|
75
|
-
const data = (await listResponse.json());
|
|
76
|
-
let tests;
|
|
77
|
-
if (Array.isArray(data)) {
|
|
78
|
-
tests = data;
|
|
79
|
-
}
|
|
80
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
81
|
-
tests = data.items;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
this.error('Unexpected API response format');
|
|
85
|
-
}
|
|
92
|
+
});
|
|
86
93
|
if (tests.length === 0) {
|
|
87
|
-
|
|
94
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
95
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
96
|
+
// with the same keys the populated path emits below.
|
|
97
|
+
if (flags.output === 'json') {
|
|
98
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.log('No unit tests found');
|
|
102
|
+
}
|
|
88
103
|
return;
|
|
89
104
|
}
|
|
90
105
|
if (flags.output === 'summary') {
|
|
@@ -92,7 +107,15 @@ Results: 2 passed, 1 failed
|
|
|
92
107
|
}
|
|
93
108
|
// Step 2: Run each test
|
|
94
109
|
const results = [];
|
|
95
|
-
|
|
110
|
+
const runOne = async (test) => {
|
|
111
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
112
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
113
|
+
// run can still emit in input order.
|
|
114
|
+
const results = [];
|
|
115
|
+
const lines = [];
|
|
116
|
+
const log = (message) => {
|
|
117
|
+
lines.push(message);
|
|
118
|
+
};
|
|
96
119
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
97
120
|
try {
|
|
98
121
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -115,10 +138,10 @@ Results: 2 passed, 1 failed
|
|
|
115
138
|
};
|
|
116
139
|
results.push(result);
|
|
117
140
|
if (flags.output === 'summary') {
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
142
|
+
log(` Error: API error ${runResponse.status}`);
|
|
120
143
|
}
|
|
121
|
-
|
|
144
|
+
return { lines, results };
|
|
122
145
|
}
|
|
123
146
|
const runResult = (await runResponse.json());
|
|
124
147
|
const passed = runResult.status === 'ok';
|
|
@@ -133,19 +156,21 @@ Results: 2 passed, 1 failed
|
|
|
133
156
|
results.push(result);
|
|
134
157
|
if (flags.output === 'summary') {
|
|
135
158
|
if (passed) {
|
|
136
|
-
|
|
159
|
+
log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
137
160
|
}
|
|
138
161
|
else {
|
|
139
|
-
|
|
162
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
140
163
|
for (const expect of failedExpects) {
|
|
141
164
|
if (expect.message) {
|
|
142
|
-
|
|
165
|
+
log(` Error: ${expect.message}`);
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
}
|
|
146
169
|
}
|
|
147
170
|
}
|
|
148
171
|
catch (error) {
|
|
172
|
+
if (error instanceof Error && 'oclif' in error)
|
|
173
|
+
throw error;
|
|
149
174
|
const message = error instanceof Error ? error.message : String(error);
|
|
150
175
|
results.push({
|
|
151
176
|
message,
|
|
@@ -155,16 +180,29 @@ Results: 2 passed, 1 failed
|
|
|
155
180
|
status: 'fail',
|
|
156
181
|
});
|
|
157
182
|
if (flags.output === 'summary') {
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
184
|
+
log(` Error: ${message}`);
|
|
160
185
|
}
|
|
161
186
|
}
|
|
162
|
-
|
|
187
|
+
return { lines, results };
|
|
188
|
+
};
|
|
189
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
190
|
+
// diffable against a sequential run.
|
|
191
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
192
|
+
for (const line of settled.lines)
|
|
193
|
+
this.log(line);
|
|
194
|
+
});
|
|
195
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
196
|
+
const settled = await runOne(test);
|
|
197
|
+
emitter.settle(index, settled);
|
|
198
|
+
return settled;
|
|
199
|
+
});
|
|
200
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
163
201
|
// Step 3: Summary
|
|
164
202
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
165
203
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
166
204
|
if (flags.output === 'json') {
|
|
167
|
-
this.log(JSON.stringify({
|
|
205
|
+
this.log(JSON.stringify({ failed, passed, results }, null, 2));
|
|
168
206
|
}
|
|
169
207
|
else {
|
|
170
208
|
this.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
@@ -174,6 +212,8 @@ Results: 2 passed, 1 failed
|
|
|
174
212
|
}
|
|
175
213
|
}
|
|
176
214
|
catch (error) {
|
|
215
|
+
if (error instanceof Error && 'oclif' in error)
|
|
216
|
+
throw error;
|
|
177
217
|
if (error instanceof Error) {
|
|
178
218
|
this.error(`Failed to run unit tests: ${error.message}`);
|
|
179
219
|
}
|
|
@@ -3,9 +3,11 @@ export default class WorkflowTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
11
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../utils/paging.js';
|
|
3
4
|
export default class WorkflowTestList extends BaseCommand {
|
|
4
5
|
static description = 'List all workflow tests in a workspace';
|
|
5
6
|
static examples = [
|
|
@@ -25,6 +26,7 @@ Workflow tests in workspace 5:
|
|
|
25
26
|
options: ['summary', 'json'],
|
|
26
27
|
required: false,
|
|
27
28
|
}),
|
|
29
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
28
30
|
workspace: Flags.string({
|
|
29
31
|
char: 'w',
|
|
30
32
|
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
@@ -38,7 +40,8 @@ Workflow tests in workspace 5:
|
|
|
38
40
|
if (!workspaceId) {
|
|
39
41
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
40
42
|
}
|
|
41
|
-
const params = new URLSearchParams(
|
|
43
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
44
|
+
params.set('include_xanoscript', 'false');
|
|
42
45
|
if (flags.branch) {
|
|
43
46
|
params.set('branch', flags.branch);
|
|
44
47
|
}
|
|
@@ -55,19 +58,10 @@ Workflow tests in workspace 5:
|
|
|
55
58
|
const errorText = await response.text();
|
|
56
59
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
57
60
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (Array.isArray(data)) {
|
|
61
|
-
tests = data;
|
|
62
|
-
}
|
|
63
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
64
|
-
tests = data.items;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
this.error('Unexpected API response format');
|
|
68
|
-
}
|
|
61
|
+
const list = normalizeListResponse(await response.json());
|
|
62
|
+
const tests = list.items;
|
|
69
63
|
if (flags.output === 'json') {
|
|
70
|
-
this.log(JSON.stringify(
|
|
64
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
71
65
|
}
|
|
72
66
|
else {
|
|
73
67
|
if (tests.length === 0) {
|
|
@@ -80,6 +74,9 @@ Workflow tests in workspace 5:
|
|
|
80
74
|
this.log(` - ${test.name} (ID: ${test.id})${desc}`);
|
|
81
75
|
}
|
|
82
76
|
}
|
|
77
|
+
const footer = formatPagingFooter(list, { noun: 'workflow test', page: flags.page, tier: 'envelope' });
|
|
78
|
+
if (footer)
|
|
79
|
+
this.log(footer);
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
catch (error) {
|