@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../utils/paging.js';
|
|
3
4
|
export default class TenantList extends BaseCommand {
|
|
4
5
|
static description = 'List all tenants in a workspace';
|
|
5
6
|
static examples = [
|
|
@@ -65,28 +66,26 @@ Tenants in workspace 5:
|
|
|
65
66
|
this.error('Unexpected API response format');
|
|
66
67
|
}
|
|
67
68
|
if (flags.output === 'json') {
|
|
68
|
-
this.log(JSON.stringify(tenants, null, 2));
|
|
69
|
+
this.log(JSON.stringify(buildPagingJson({ items: tenants }, { tier: 'none' }), null, 2));
|
|
70
|
+
}
|
|
71
|
+
else if (tenants.length === 0) {
|
|
72
|
+
this.log('No tenants found');
|
|
69
73
|
}
|
|
70
74
|
else {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (releaseName)
|
|
86
|
-
this.log(` Release: ${releaseName} (ID: ${releaseId})`);
|
|
87
|
-
if (tenant.platform?.name)
|
|
88
|
-
this.log(` Platform: ${tenant.platform.name}`);
|
|
89
|
-
}
|
|
75
|
+
this.log(`Tenants in workspace ${workspaceId}:`);
|
|
76
|
+
for (const tenant of tenants) {
|
|
77
|
+
const state = tenant.state ? ` [${tenant.state}]` : '';
|
|
78
|
+
const license = tenant.license ? ` - ${tenant.license}` : '';
|
|
79
|
+
const typeLabel = tenant.type && tenant.type !== 'standard' ? ` [${tenant.type}]` : '';
|
|
80
|
+
this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${typeLabel}`);
|
|
81
|
+
if (tenant.cluster?.name)
|
|
82
|
+
this.log(` Cluster: ${tenant.cluster.name}`);
|
|
83
|
+
const releaseName = typeof tenant.release === 'string' ? tenant.release : tenant.release?.name;
|
|
84
|
+
const releaseId = typeof tenant.release === 'object' ? tenant.release?.id : undefined;
|
|
85
|
+
if (releaseName)
|
|
86
|
+
this.log(` Release: ${releaseName} (ID: ${releaseId})`);
|
|
87
|
+
if (tenant.platform?.name)
|
|
88
|
+
this.log(` Platform: ${tenant.platform.name}`);
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -14,10 +14,4 @@ export default class Pull extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Sanitize a document name for use as a filename.
|
|
19
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
20
|
-
* characters that are unsafe in filenames.
|
|
21
|
-
*/
|
|
22
|
-
private sanitizeFilename;
|
|
23
17
|
}
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class Pull extends BaseCommand {
|
|
8
8
|
static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
|
|
9
9
|
static examples = [
|
|
@@ -129,96 +129,19 @@ Pulled 58 documents from tenant my-tenant
|
|
|
129
129
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
130
130
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
131
131
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
132
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
133
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
134
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
132
135
|
// Track filenames per type to handle duplicates
|
|
133
136
|
const filenameCounters = new Map();
|
|
134
137
|
let writtenCount = 0;
|
|
135
138
|
for (const doc of documents) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
else if (doc.type === 'workspace_trigger') {
|
|
144
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
145
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
146
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
147
|
-
}
|
|
148
|
-
else if (doc.type === 'error_trigger') {
|
|
149
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
150
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
151
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
152
|
-
}
|
|
153
|
-
else if (doc.type === 'agent') {
|
|
154
|
-
// agent → ai/agent/{name}.xs
|
|
155
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
156
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
157
|
-
}
|
|
158
|
-
else if (doc.type === 'mcp_server') {
|
|
159
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
160
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
161
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
162
|
-
}
|
|
163
|
-
else if (doc.type === 'tool') {
|
|
164
|
-
// tool → ai/tool/{name}.xs
|
|
165
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
166
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
167
|
-
}
|
|
168
|
-
else if (doc.type === 'agent_trigger') {
|
|
169
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
170
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
171
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
172
|
-
}
|
|
173
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
174
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
175
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
176
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
177
|
-
}
|
|
178
|
-
else if (doc.type === 'table_trigger') {
|
|
179
|
-
// table_trigger → table/trigger/{name}.xs
|
|
180
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
181
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
182
|
-
}
|
|
183
|
-
else if (doc.type === 'realtime_channel') {
|
|
184
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
185
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
186
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
187
|
-
}
|
|
188
|
-
else if (doc.type === 'realtime_trigger') {
|
|
189
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
190
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
191
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
-
}
|
|
193
|
-
else if (doc.type === 'api_group') {
|
|
194
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
195
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
196
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
197
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
198
|
-
}
|
|
199
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
200
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
201
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
202
|
-
const nameParts = doc.name.split('/');
|
|
203
|
-
const leafName = nameParts.pop();
|
|
204
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
205
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
206
|
-
baseName = this.sanitizeFilename(leafName);
|
|
207
|
-
if (doc.verb) {
|
|
208
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
// Default: split folder path from name
|
|
213
|
-
const nameParts = doc.name.split('/');
|
|
214
|
-
const leafName = nameParts.pop();
|
|
215
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
216
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
217
|
-
baseName = this.sanitizeFilename(leafName);
|
|
218
|
-
if (doc.verb) {
|
|
219
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
139
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
140
|
+
getApiGroupFolder,
|
|
141
|
+
getChannelServer,
|
|
142
|
+
join: path.join,
|
|
143
|
+
snakeCase,
|
|
144
|
+
});
|
|
222
145
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
223
146
|
// Track duplicates per directory
|
|
224
147
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -237,12 +160,4 @@ Pulled 58 documents from tenant my-tenant
|
|
|
237
160
|
}
|
|
238
161
|
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
239
162
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Sanitize a document name for use as a filename.
|
|
242
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
243
|
-
* characters that are unsafe in filenames.
|
|
244
|
-
*/
|
|
245
|
-
sanitizeFilename(name) {
|
|
246
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
247
|
-
}
|
|
248
163
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, formatPagingFooter } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantSnapshotList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
tenant_name: Args.string({
|
|
@@ -64,7 +65,7 @@ Snapshots for tenant t1234-abcd-xyz1:
|
|
|
64
65
|
this.error('Unexpected API response format');
|
|
65
66
|
}
|
|
66
67
|
if (flags.output === 'json') {
|
|
67
|
-
this.log(JSON.stringify(snapshots, null, 2));
|
|
68
|
+
this.log(JSON.stringify(buildPagingJson({ items: snapshots }, { tier: 'none' }), null, 2));
|
|
68
69
|
}
|
|
69
70
|
else if (snapshots.length === 0) {
|
|
70
71
|
this.log(`No snapshots found for tenant ${tenantName}`);
|
|
@@ -82,6 +83,9 @@ Snapshots for tenant t1234-abcd-xyz1:
|
|
|
82
83
|
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
83
84
|
this.log(` - ${snapshot.name} (${meta})${tagStr}`);
|
|
84
85
|
}
|
|
86
|
+
const footer = formatPagingFooter({ items: snapshots }, { noun: 'snapshot', tier: 'none' });
|
|
87
|
+
if (footer)
|
|
88
|
+
this.log(footer);
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
catch (error) {
|
|
@@ -3,11 +3,13 @@ export default class TenantUnitTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
10
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
11
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
12
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
15
|
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 TenantUnitTestList extends BaseCommand {
|
|
4
5
|
static description = 'List all unit tests for a tenant';
|
|
5
6
|
static examples = [
|
|
@@ -28,6 +29,7 @@ Unit tests for tenant my-tenant:
|
|
|
28
29
|
options: ['summary', 'json'],
|
|
29
30
|
required: false,
|
|
30
31
|
}),
|
|
32
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
31
33
|
tenant: Flags.string({
|
|
32
34
|
char: 't',
|
|
33
35
|
description: 'Tenant name',
|
|
@@ -47,8 +49,7 @@ Unit tests for tenant my-tenant:
|
|
|
47
49
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
48
50
|
}
|
|
49
51
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
50
|
-
const params = new URLSearchParams();
|
|
51
|
-
params.set('per_page', '10000');
|
|
52
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
52
53
|
if (flags.branch)
|
|
53
54
|
params.set('branch', flags.branch);
|
|
54
55
|
if (flags['obj-type'])
|
|
@@ -66,19 +67,10 @@ Unit tests for tenant my-tenant:
|
|
|
66
67
|
const errorText = await response.text();
|
|
67
68
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
68
69
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
if (Array.isArray(data)) {
|
|
72
|
-
tests = data;
|
|
73
|
-
}
|
|
74
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
75
|
-
tests = data.items;
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
this.error('Unexpected API response format');
|
|
79
|
-
}
|
|
70
|
+
const list = normalizeListResponse(await response.json());
|
|
71
|
+
const tests = list.items;
|
|
80
72
|
if (flags.output === 'json') {
|
|
81
|
-
this.log(JSON.stringify(
|
|
73
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
82
74
|
}
|
|
83
75
|
else {
|
|
84
76
|
if (tests.length === 0) {
|
|
@@ -90,6 +82,9 @@ Unit tests for tenant my-tenant:
|
|
|
90
82
|
this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
|
|
91
83
|
}
|
|
92
84
|
}
|
|
85
|
+
const footer = formatPagingFooter(list, { noun: 'unit test', page: flags.page, tier: 'envelope' });
|
|
86
|
+
if (footer)
|
|
87
|
+
this.log(footer);
|
|
93
88
|
}
|
|
94
89
|
}
|
|
95
90
|
catch (error) {
|
|
@@ -68,26 +68,34 @@ Result: PASS
|
|
|
68
68
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
69
69
|
}
|
|
70
70
|
const result = (await response.json());
|
|
71
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
72
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
73
|
+
const passed = result.status === 'ok';
|
|
71
74
|
if (flags.output === 'json') {
|
|
72
75
|
this.log(JSON.stringify(result, null, 2));
|
|
73
76
|
}
|
|
77
|
+
else if (passed) {
|
|
78
|
+
this.log('Result: PASS');
|
|
79
|
+
}
|
|
74
80
|
else {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
81
|
-
for (const expect of failedExpects) {
|
|
82
|
-
if (expect.message) {
|
|
83
|
-
this.log(` Error: ${expect.message}`);
|
|
84
|
-
}
|
|
81
|
+
this.log('Result: FAIL');
|
|
82
|
+
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
83
|
+
for (const expect of failedExpects) {
|
|
84
|
+
if (expect.message) {
|
|
85
|
+
this.log(` Error: ${expect.message}`);
|
|
85
86
|
}
|
|
86
|
-
this.exit(1);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
90
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
91
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
92
|
+
if (!passed) {
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
89
95
|
}
|
|
90
96
|
catch (error) {
|
|
97
|
+
if (error instanceof Error && 'oclif' in error)
|
|
98
|
+
throw error;
|
|
91
99
|
if (error instanceof Error) {
|
|
92
100
|
this.error(`Failed to run unit test: ${error.message}`);
|
|
93
101
|
}
|
|
@@ -4,6 +4,7 @@ export default class TenantUnitTestRunAll 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
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, 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 TenantUnitTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all unit tests for a tenant';
|
|
5
7
|
static examples = [
|
|
@@ -21,6 +23,11 @@ Results: 4 passed, 1 failed
|
|
|
21
23
|
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
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
|
'obj-type': Flags.string({
|
|
25
32
|
description: 'Filter by object type',
|
|
26
33
|
options: ['function', 'query', 'middleware'],
|
|
@@ -57,43 +64,61 @@ Results: 4 passed, 1 failed
|
|
|
57
64
|
// from the same branch.
|
|
58
65
|
const branch = flags.branch ?? profile.branch;
|
|
59
66
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
68
|
+
// request is plenty for typical suites and does not make the instance
|
|
69
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
70
|
+
// never "the page came back full".
|
|
71
|
+
const branchValue = branch;
|
|
72
|
+
const tests = await collectAllPages(async (page) => {
|
|
73
|
+
const listParams = new URLSearchParams();
|
|
74
|
+
listParams.set('page', String(page));
|
|
75
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
76
|
+
if (branchValue)
|
|
77
|
+
listParams.set('branch', branchValue);
|
|
78
|
+
if (flags['obj-type'])
|
|
79
|
+
listParams.set('obj_type', flags['obj-type']);
|
|
80
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
81
|
+
headers: {
|
|
82
|
+
accept: 'application/json',
|
|
83
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
84
|
+
},
|
|
85
|
+
method: 'GET',
|
|
86
|
+
}, flags.verbose, profile.access_token);
|
|
87
|
+
if (!listResponse.ok) {
|
|
88
|
+
const errorText = await listResponse.text();
|
|
89
|
+
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
90
|
+
}
|
|
91
|
+
return normalizeListResponse(await listResponse.json());
|
|
92
|
+
}, {
|
|
93
|
+
onTruncate: (collected) => {
|
|
94
|
+
this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
|
|
70
95
|
},
|
|
71
|
-
|
|
72
|
-
}, flags.verbose, profile.access_token);
|
|
73
|
-
if (!listResponse.ok) {
|
|
74
|
-
const errorText = await listResponse.text();
|
|
75
|
-
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
76
|
-
}
|
|
77
|
-
const data = (await listResponse.json());
|
|
78
|
-
let tests;
|
|
79
|
-
if (Array.isArray(data)) {
|
|
80
|
-
tests = data;
|
|
81
|
-
}
|
|
82
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
83
|
-
tests = data.items;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
this.error('Unexpected API response format');
|
|
87
|
-
}
|
|
96
|
+
});
|
|
88
97
|
if (tests.length === 0) {
|
|
89
|
-
|
|
98
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
99
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
100
|
+
// with the same keys the populated path emits below.
|
|
101
|
+
if (flags.output === 'json') {
|
|
102
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.log('No unit tests found');
|
|
106
|
+
}
|
|
90
107
|
return;
|
|
91
108
|
}
|
|
92
109
|
if (flags.output === 'summary') {
|
|
93
110
|
this.log(`Running ${tests.length} unit test${tests.length === 1 ? '' : 's'}...\n`);
|
|
94
111
|
}
|
|
95
112
|
const results = [];
|
|
96
|
-
|
|
113
|
+
const runOne = async (test) => {
|
|
114
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
115
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
116
|
+
// run can still emit in input order.
|
|
117
|
+
const results = [];
|
|
118
|
+
const lines = [];
|
|
119
|
+
const log = (message) => {
|
|
120
|
+
lines.push(message);
|
|
121
|
+
};
|
|
97
122
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
98
123
|
try {
|
|
99
124
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -115,10 +140,10 @@ Results: 4 passed, 1 failed
|
|
|
115
140
|
status: 'fail',
|
|
116
141
|
});
|
|
117
142
|
if (flags.output === 'summary') {
|
|
118
|
-
|
|
119
|
-
|
|
143
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
144
|
+
log(` Error: API error ${runResponse.status}`);
|
|
120
145
|
}
|
|
121
|
-
|
|
146
|
+
return { lines, results };
|
|
122
147
|
}
|
|
123
148
|
const runResult = (await runResponse.json());
|
|
124
149
|
const passed = runResult.status === 'ok';
|
|
@@ -132,19 +157,21 @@ Results: 4 passed, 1 failed
|
|
|
132
157
|
});
|
|
133
158
|
if (flags.output === 'summary') {
|
|
134
159
|
if (passed) {
|
|
135
|
-
|
|
160
|
+
log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
136
161
|
}
|
|
137
162
|
else {
|
|
138
|
-
|
|
163
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
139
164
|
for (const expect of failedExpects) {
|
|
140
165
|
if (expect.message) {
|
|
141
|
-
|
|
166
|
+
log(` Error: ${expect.message}`);
|
|
142
167
|
}
|
|
143
168
|
}
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
172
|
catch (error) {
|
|
173
|
+
if (error instanceof Error && 'oclif' in error)
|
|
174
|
+
throw error;
|
|
148
175
|
const message = error instanceof Error ? error.message : String(error);
|
|
149
176
|
results.push({
|
|
150
177
|
message,
|
|
@@ -154,11 +181,24 @@ Results: 4 passed, 1 failed
|
|
|
154
181
|
status: 'fail',
|
|
155
182
|
});
|
|
156
183
|
if (flags.output === 'summary') {
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
185
|
+
log(` Error: ${message}`);
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
|
-
|
|
188
|
+
return { lines, results };
|
|
189
|
+
};
|
|
190
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
191
|
+
// diffable against a sequential run.
|
|
192
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
193
|
+
for (const line of settled.lines)
|
|
194
|
+
this.log(line);
|
|
195
|
+
});
|
|
196
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
197
|
+
const settled = await runOne(test);
|
|
198
|
+
emitter.settle(index, settled);
|
|
199
|
+
return settled;
|
|
200
|
+
});
|
|
201
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
162
202
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
163
203
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
164
204
|
if (flags.output === 'json') {
|
|
@@ -172,6 +212,8 @@ Results: 4 passed, 1 failed
|
|
|
172
212
|
}
|
|
173
213
|
}
|
|
174
214
|
catch (error) {
|
|
215
|
+
if (error instanceof Error && 'oclif' in error)
|
|
216
|
+
throw error;
|
|
175
217
|
if (error instanceof Error) {
|
|
176
218
|
this.error(`Failed to run unit tests: ${error.message}`);
|
|
177
219
|
}
|
|
@@ -3,10 +3,12 @@ export default class TenantWorkflowTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
6
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
7
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, 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 TenantWorkflowTestList extends BaseCommand {
|
|
4
5
|
static description = 'List workflow tests for a tenant';
|
|
5
6
|
static examples = [
|
|
@@ -23,6 +24,7 @@ Workflow tests for tenant my-tenant:
|
|
|
23
24
|
options: ['summary', 'json'],
|
|
24
25
|
required: false,
|
|
25
26
|
}),
|
|
27
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
26
28
|
tenant: Flags.string({
|
|
27
29
|
char: 't',
|
|
28
30
|
description: 'Tenant name',
|
|
@@ -42,8 +44,7 @@ Workflow tests for tenant my-tenant:
|
|
|
42
44
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
43
45
|
}
|
|
44
46
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
45
|
-
const params = new URLSearchParams();
|
|
46
|
-
params.set('per_page', '10000');
|
|
47
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
47
48
|
if (flags.branch)
|
|
48
49
|
params.set('branch', flags.branch);
|
|
49
50
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test?${params}`;
|
|
@@ -59,19 +60,10 @@ Workflow tests for tenant my-tenant:
|
|
|
59
60
|
const errorText = await response.text();
|
|
60
61
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
61
62
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (Array.isArray(data)) {
|
|
65
|
-
tests = data;
|
|
66
|
-
}
|
|
67
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
68
|
-
tests = data.items;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
this.error('Unexpected API response format');
|
|
72
|
-
}
|
|
63
|
+
const list = normalizeListResponse(await response.json());
|
|
64
|
+
const tests = list.items;
|
|
73
65
|
if (flags.output === 'json') {
|
|
74
|
-
this.log(JSON.stringify(
|
|
66
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
75
67
|
}
|
|
76
68
|
else {
|
|
77
69
|
if (tests.length === 0) {
|
|
@@ -83,6 +75,9 @@ Workflow tests for tenant my-tenant:
|
|
|
83
75
|
this.log(` - ${test.name} (ID: ${test.id})`);
|
|
84
76
|
}
|
|
85
77
|
}
|
|
78
|
+
const footer = formatPagingFooter(list, { noun: 'workflow test', page: flags.page, tier: 'envelope' });
|
|
79
|
+
if (footer)
|
|
80
|
+
this.log(footer);
|
|
86
81
|
}
|
|
87
82
|
}
|
|
88
83
|
catch (error) {
|