@xano/cli 1.0.6-beta.2 → 1.0.6-beta.20
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 +217 -6
- package/dist/base-command.d.ts +5 -0
- package/dist/base-command.js +61 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/create/index.js +3 -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/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +19 -5
- 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/create/index.d.ts +3 -0
- package/dist/commands/tenant/create/index.js +21 -0
- package/dist/commands/tenant/deploy_release/index.d.ts +21 -0
- package/dist/commands/tenant/deploy_release/index.js +94 -2
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +39 -4
- 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 -106
- 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/tenant_deploy_request/bypass/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/bypass/index.js +79 -0
- package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/create/index.js +108 -0
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
- package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
- package/dist/commands/tenant_deploy_request/get/index.d.ts +19 -0
- package/dist/commands/tenant_deploy_request/get/index.js +75 -0
- package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/list/index.js +122 -0
- package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/set_status/index.js +112 -0
- 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 +48 -7
- 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 -115
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +90 -0
- package/dist/utils/document-parser.js +273 -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 +88 -0
- package/dist/utils/multidoc-push.js +377 -8
- 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 +4999 -3973
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -55,24 +55,29 @@ Result: PASS (1.234s)
|
|
|
55
55
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
56
56
|
}
|
|
57
57
|
const result = await response.json();
|
|
58
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
59
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
60
|
+
const passed = result.status === 'ok';
|
|
58
61
|
if (flags.output === 'json') {
|
|
59
62
|
this.log(JSON.stringify(result, null, 2));
|
|
60
63
|
}
|
|
61
64
|
else {
|
|
62
|
-
const timing = `(${result.timing.toFixed(3)}s)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else {
|
|
67
|
-
this.log(`Result: FAIL ${timing}`);
|
|
68
|
-
if (result.message) {
|
|
69
|
-
this.log(` Error: ${result.message}`);
|
|
70
|
-
}
|
|
71
|
-
this.exit(1);
|
|
65
|
+
const timing = typeof result.timing === 'number' ? ` (${result.timing.toFixed(3)}s)` : '';
|
|
66
|
+
this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
|
|
67
|
+
if (!passed && result.message) {
|
|
68
|
+
this.log(` Error: ${result.message}`);
|
|
72
69
|
}
|
|
73
70
|
}
|
|
71
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
72
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
73
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
74
|
+
if (!passed) {
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
74
77
|
}
|
|
75
78
|
catch (error) {
|
|
79
|
+
if (error instanceof Error && 'oclif' in error)
|
|
80
|
+
throw error;
|
|
76
81
|
if (error instanceof Error) {
|
|
77
82
|
this.error(`Failed to run workflow test: ${error.message}`);
|
|
78
83
|
}
|
|
@@ -4,6 +4,7 @@ export default class WorkflowTestRunAll 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
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
config: 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 WorkflowTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all workflow tests in a workspace';
|
|
5
7
|
static examples = [
|
|
@@ -22,6 +24,11 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
22
24
|
description: 'Filter by branch name',
|
|
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
|
output: Flags.string({
|
|
26
33
|
char: 'o',
|
|
27
34
|
default: 'summary',
|
|
@@ -45,34 +52,45 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
45
52
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/workflow_test`;
|
|
46
53
|
try {
|
|
47
54
|
// Step 1: List all workflow tests
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
56
|
+
// request is plenty for typical suites and does not make the instance
|
|
57
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
58
|
+
// never "the page came back full".
|
|
59
|
+
const branchValue = flags.branch;
|
|
60
|
+
const tests = await collectAllPages(async (page) => {
|
|
61
|
+
const listParams = new URLSearchParams();
|
|
62
|
+
listParams.set('page', String(page));
|
|
63
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
64
|
+
if (branchValue)
|
|
65
|
+
listParams.set('branch', branchValue);
|
|
66
|
+
listParams.set('include_xanoscript', 'false');
|
|
67
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
68
|
+
headers: {
|
|
69
|
+
accept: 'application/json',
|
|
70
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
71
|
+
},
|
|
72
|
+
method: 'GET',
|
|
73
|
+
}, flags.verbose, profile.access_token);
|
|
74
|
+
if (!listResponse.ok) {
|
|
75
|
+
const errorText = await listResponse.text();
|
|
76
|
+
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
77
|
+
}
|
|
78
|
+
return normalizeListResponse(await listResponse.json());
|
|
79
|
+
}, {
|
|
80
|
+
onTruncate: (collected) => {
|
|
81
|
+
this.warn(`Stopped after ${collected} workflow tests — the list did not terminate. Some tests may not have run.`);
|
|
56
82
|
},
|
|
57
|
-
|
|
58
|
-
}, flags.verbose, profile.access_token);
|
|
59
|
-
if (!listResponse.ok) {
|
|
60
|
-
const errorText = await listResponse.text();
|
|
61
|
-
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
62
|
-
}
|
|
63
|
-
const data = (await listResponse.json());
|
|
64
|
-
let tests;
|
|
65
|
-
if (Array.isArray(data)) {
|
|
66
|
-
tests = data;
|
|
67
|
-
}
|
|
68
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
69
|
-
tests = data.items;
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
this.error('Unexpected API response format');
|
|
73
|
-
}
|
|
83
|
+
});
|
|
74
84
|
if (tests.length === 0) {
|
|
75
|
-
|
|
85
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
86
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
87
|
+
// with the same keys the populated path emits below.
|
|
88
|
+
if (flags.output === 'json') {
|
|
89
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [], total_timing: 0 }, null, 2));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.log('No workflow tests found');
|
|
93
|
+
}
|
|
76
94
|
return;
|
|
77
95
|
}
|
|
78
96
|
if (flags.output === 'summary') {
|
|
@@ -81,7 +99,15 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
81
99
|
// Step 2: Run each test
|
|
82
100
|
const results = [];
|
|
83
101
|
let totalTiming = 0;
|
|
84
|
-
|
|
102
|
+
const runOne = async (test) => {
|
|
103
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
104
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
105
|
+
// run can still emit in input order.
|
|
106
|
+
const results = [];
|
|
107
|
+
const lines = [];
|
|
108
|
+
const log = (message) => {
|
|
109
|
+
lines.push(message);
|
|
110
|
+
};
|
|
85
111
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
86
112
|
try {
|
|
87
113
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -102,10 +128,10 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
102
128
|
};
|
|
103
129
|
results.push(result);
|
|
104
130
|
if (flags.output === 'summary') {
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
log(`FAIL ${test.name} (0.000s)`);
|
|
132
|
+
log(` Error: API error ${runResponse.status}`);
|
|
107
133
|
}
|
|
108
|
-
|
|
134
|
+
return { lines, results };
|
|
109
135
|
}
|
|
110
136
|
const runResult = (await runResponse.json());
|
|
111
137
|
const passed = runResult.status === 'ok';
|
|
@@ -116,21 +142,23 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
116
142
|
timing: runResult.timing,
|
|
117
143
|
};
|
|
118
144
|
results.push(result);
|
|
119
|
-
totalTiming += runResult.timing;
|
|
145
|
+
totalTiming += runResult.timing ?? 0;
|
|
120
146
|
if (flags.output === 'summary') {
|
|
121
|
-
const timing = `(${runResult.timing.toFixed(3)}s)`;
|
|
147
|
+
const timing = `(${(runResult.timing ?? 0).toFixed(3)}s)`;
|
|
122
148
|
if (passed) {
|
|
123
|
-
|
|
149
|
+
log(`PASS ${test.name} ${timing}`);
|
|
124
150
|
}
|
|
125
151
|
else {
|
|
126
|
-
|
|
152
|
+
log(`FAIL ${test.name} ${timing}`);
|
|
127
153
|
if (runResult.message) {
|
|
128
|
-
|
|
154
|
+
log(` Error: ${runResult.message}`);
|
|
129
155
|
}
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
158
|
}
|
|
133
159
|
catch (error) {
|
|
160
|
+
if (error instanceof Error && 'oclif' in error)
|
|
161
|
+
throw error;
|
|
134
162
|
const message = error instanceof Error ? error.message : String(error);
|
|
135
163
|
results.push({
|
|
136
164
|
message,
|
|
@@ -139,16 +167,29 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
139
167
|
timing: 0,
|
|
140
168
|
});
|
|
141
169
|
if (flags.output === 'summary') {
|
|
142
|
-
|
|
143
|
-
|
|
170
|
+
log(`FAIL ${test.name} (0.000s)`);
|
|
171
|
+
log(` Error: ${message}`);
|
|
144
172
|
}
|
|
145
173
|
}
|
|
146
|
-
|
|
174
|
+
return { lines, results };
|
|
175
|
+
};
|
|
176
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
177
|
+
// diffable against a sequential run.
|
|
178
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
179
|
+
for (const line of settled.lines)
|
|
180
|
+
this.log(line);
|
|
181
|
+
});
|
|
182
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
183
|
+
const settled = await runOne(test);
|
|
184
|
+
emitter.settle(index, settled);
|
|
185
|
+
return settled;
|
|
186
|
+
});
|
|
187
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
147
188
|
// Step 3: Summary
|
|
148
189
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
149
190
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
150
191
|
if (flags.output === 'json') {
|
|
151
|
-
this.log(JSON.stringify({ passed,
|
|
192
|
+
this.log(JSON.stringify({ failed, passed, results, total_timing: totalTiming }, null, 2));
|
|
152
193
|
}
|
|
153
194
|
else {
|
|
154
195
|
this.log(`\nResults: ${passed} passed, ${failed} failed (${totalTiming.toFixed(3)}s total)`);
|
|
@@ -158,6 +199,8 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
158
199
|
}
|
|
159
200
|
}
|
|
160
201
|
catch (error) {
|
|
202
|
+
if (error instanceof Error && 'oclif' in error)
|
|
203
|
+
throw error;
|
|
161
204
|
if (error instanceof Error) {
|
|
162
205
|
this.error(`Failed to run workflow tests: ${error.message}`);
|
|
163
206
|
}
|
|
@@ -5,7 +5,7 @@ import * as os from 'node:os';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import snakeCase from 'lodash.snakecase';
|
|
7
7
|
import BaseCommand, { buildUserAgent } from '../../../../base-command.js';
|
|
8
|
-
import { buildApiGroupFolderResolver, channelPathSegments, parseDocument, } from '../../../../utils/document-parser.js';
|
|
8
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, channelPathSegments, parseDocument, } from '../../../../utils/document-parser.js';
|
|
9
9
|
export default class GitPull extends BaseCommand {
|
|
10
10
|
static description = 'Pull XanoScript files from a git repository into a local directory';
|
|
11
11
|
static examples = [
|
|
@@ -97,10 +97,13 @@ export default class GitPull extends BaseCommand {
|
|
|
97
97
|
// Write documents to output directory using the same file tree logic as workspace pull
|
|
98
98
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
99
99
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
100
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
101
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
102
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
100
103
|
const filenameCounters = new Map();
|
|
101
104
|
let writtenCount = 0;
|
|
102
105
|
for (const doc of documents) {
|
|
103
|
-
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder);
|
|
106
|
+
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer);
|
|
104
107
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
105
108
|
// Track duplicates per directory
|
|
106
109
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -325,7 +328,7 @@ export default class GitPull extends BaseCommand {
|
|
|
325
328
|
* Resolve the output directory and base filename for a parsed document.
|
|
326
329
|
* Uses the same type-to-directory mapping as workspace pull.
|
|
327
330
|
*/
|
|
328
|
-
resolveOutputPath(outputDir, doc, getApiGroupFolder) {
|
|
331
|
+
resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer) {
|
|
329
332
|
let typeDir;
|
|
330
333
|
let baseName;
|
|
331
334
|
if (doc.type === 'workspace') {
|
|
@@ -373,13 +376,51 @@ export default class GitPull extends BaseCommand {
|
|
|
373
376
|
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
374
377
|
baseName = this.sanitizeFilename(doc.name);
|
|
375
378
|
}
|
|
379
|
+
else if (doc.type === 'realtime_server') {
|
|
380
|
+
// Realtime v2 — see workspace/pull for the full rationale. Its own
|
|
381
|
+
// document is named after itself, mirroring api_group.
|
|
382
|
+
// realtime_server "chat" → realtime/server/chat/chat.xs
|
|
383
|
+
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.name));
|
|
384
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
385
|
+
}
|
|
376
386
|
else if (doc.type === 'channel') {
|
|
377
|
-
// Realtime v2
|
|
378
|
-
|
|
379
|
-
|
|
387
|
+
// Realtime v2. The channel owns a directory named after itself (the full
|
|
388
|
+
// channel name, snake_cased into a single flat segment — like every other
|
|
389
|
+
// object), and its messages live in a message/ subfolder inside it. It
|
|
390
|
+
// nests under its owning realtime_server (from `server = "..."`):
|
|
391
|
+
// channel "rooms/{room_id}" (server "chat")
|
|
392
|
+
// → realtime/server/chat/channel/rooms_room_id/rooms_room_id.xs
|
|
393
|
+
//
|
|
394
|
+
// The channel name is snake_cased as a whole (rooms/{room_id} →
|
|
395
|
+
// rooms_room_id), so there is no path nesting and no bracket escaping —
|
|
396
|
+
// the on-disk name is a display form; the .xs content keeps the real
|
|
397
|
+
// name. A channel with no resolvable server (e.g. a pre-server export)
|
|
398
|
+
// falls back to the legacy flat channel/<path>/_channel.xs layout.
|
|
399
|
+
if (doc.server) {
|
|
400
|
+
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.server), 'channel', snakeCase(doc.name));
|
|
401
|
+
baseName = snakeCase(doc.name);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
405
|
+
baseName = '_channel';
|
|
406
|
+
}
|
|
380
407
|
}
|
|
381
408
|
else if (doc.type === 'message' && doc.channel) {
|
|
382
|
-
|
|
409
|
+
// Realtime v2 message → nests in a message/ subfolder under its channel,
|
|
410
|
+
// under that channel's server. The message names only its channel; the
|
|
411
|
+
// server is resolved by looking that channel up in the same multidoc
|
|
412
|
+
// (two-pass resolve).
|
|
413
|
+
// message "post" on channel "rooms/{room_id}" (server "chat")
|
|
414
|
+
// → realtime/server/chat/channel/rooms_room_id/message/post.xs
|
|
415
|
+
//
|
|
416
|
+
// Nesting matters beyond tidiness: message names are unique only WITHIN
|
|
417
|
+
// a channel, so a flat message/ directory would collide when two
|
|
418
|
+
// channels both define e.g. "say". A channel whose server can't be
|
|
419
|
+
// resolved falls back to the legacy flat channel/ layout.
|
|
420
|
+
const messageServer = getChannelServer(doc.channel);
|
|
421
|
+
typeDir = messageServer
|
|
422
|
+
? path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(messageServer), 'channel', snakeCase(doc.channel), 'message')
|
|
423
|
+
: path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
383
424
|
baseName = this.sanitizeFilename(doc.name);
|
|
384
425
|
}
|
|
385
426
|
else if (doc.type === 'api_group') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, formatPagingFooter, normalizeListResponse } from '../../../utils/paging.js';
|
|
3
4
|
export default class WorkspaceList extends BaseCommand {
|
|
4
5
|
static description = 'List all workspaces from the Xano Metadata API';
|
|
5
6
|
static examples = [
|
|
@@ -71,24 +72,14 @@ Available workspaces:
|
|
|
71
72
|
const errorText = await response.text();
|
|
72
73
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
73
74
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
let workspaces;
|
|
77
|
-
if (Array.isArray(data)) {
|
|
78
|
-
workspaces = data;
|
|
79
|
-
}
|
|
80
|
-
else if (data && typeof data === 'object' && 'workspaces' in data && Array.isArray(data.workspaces)) {
|
|
81
|
-
workspaces = data.workspaces;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
this.error('Unexpected API response format');
|
|
85
|
-
}
|
|
75
|
+
const list = normalizeListResponse(await response.json(), ['workspaces']);
|
|
76
|
+
const workspaces = list.items;
|
|
86
77
|
if (flags.latest) {
|
|
87
78
|
workspaces.sort((a, b) => b.id - a.id);
|
|
88
79
|
}
|
|
89
80
|
// Output results
|
|
90
81
|
if (flags.output === 'json') {
|
|
91
|
-
this.log(JSON.stringify(
|
|
82
|
+
this.log(JSON.stringify(buildPagingJson(list, { tier: 'none' }), null, 2));
|
|
92
83
|
}
|
|
93
84
|
else {
|
|
94
85
|
// summary format
|
|
@@ -107,6 +98,9 @@ Available workspaces:
|
|
|
107
98
|
}
|
|
108
99
|
}
|
|
109
100
|
}
|
|
101
|
+
const footer = formatPagingFooter({ items: workspaces }, { noun: 'workspace', tier: 'none' });
|
|
102
|
+
if (footer)
|
|
103
|
+
this.log(footer);
|
|
110
104
|
}
|
|
111
105
|
}
|
|
112
106
|
catch (error) {
|