@xano/cli 1.0.6-beta.1 → 1.0.6-beta.10

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.
Files changed (83) hide show
  1. package/README.md +96 -4
  2. package/dist/commands/branch/list/index.js +5 -1
  3. package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
  4. package/dist/commands/ephemeral/impersonate/index.js +11 -3
  5. package/dist/commands/ephemeral/list/index.js +2 -1
  6. package/dist/commands/ephemeral/pull/index.d.ts +0 -6
  7. package/dist/commands/ephemeral/pull/index.js +10 -95
  8. package/dist/commands/ephemeral/push/index.d.ts +10 -0
  9. package/dist/commands/ephemeral/push/index.js +77 -0
  10. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
  11. package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
  12. package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
  13. package/dist/commands/ephemeral/static_host/list/index.js +20 -40
  14. package/dist/commands/flatten/index.d.ts +34 -0
  15. package/dist/commands/flatten/index.js +140 -0
  16. package/dist/commands/function/list/index.d.ts +4 -4
  17. package/dist/commands/function/list/index.js +30 -38
  18. package/dist/commands/knowledge/list/index.js +2 -1
  19. package/dist/commands/platform/list/index.js +13 -14
  20. package/dist/commands/release/list/index.js +13 -14
  21. package/dist/commands/release/pull/index.d.ts +0 -6
  22. package/dist/commands/release/pull/index.js +10 -95
  23. package/dist/commands/sandbox/env/list/index.js +4 -0
  24. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  25. package/dist/commands/sandbox/pull/index.js +10 -77
  26. package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
  27. package/dist/commands/sandbox/unit_test/list/index.js +9 -14
  28. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  29. package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
  30. package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
  31. package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
  32. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  33. package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
  34. package/dist/commands/static_host/build/list/index.d.ts +2 -3
  35. package/dist/commands/static_host/build/list/index.js +19 -40
  36. package/dist/commands/static_host/build/pull/index.js +12 -9
  37. package/dist/commands/static_host/list/index.d.ts +2 -3
  38. package/dist/commands/static_host/list/index.js +20 -40
  39. package/dist/commands/static_host/migrate/index.js +14 -10
  40. package/dist/commands/tenant/backup/list/index.js +13 -14
  41. package/dist/commands/tenant/cluster/list/index.js +9 -10
  42. package/dist/commands/tenant/env/list/index.js +4 -0
  43. package/dist/commands/tenant/list/index.js +19 -20
  44. package/dist/commands/tenant/pull/index.d.ts +0 -6
  45. package/dist/commands/tenant/pull/index.js +10 -95
  46. package/dist/commands/tenant/snapshot/list/index.js +5 -1
  47. package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
  48. package/dist/commands/tenant/unit_test/list/index.js +9 -14
  49. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  50. package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
  51. package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
  52. package/dist/commands/tenant/workflow_test/list/index.js +9 -14
  53. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  54. package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
  55. package/dist/commands/unit_test/list/index.d.ts +3 -1
  56. package/dist/commands/unit_test/list/index.js +9 -14
  57. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  58. package/dist/commands/unit_test/run_all/index.js +68 -40
  59. package/dist/commands/workflow_test/list/index.d.ts +3 -1
  60. package/dist/commands/workflow_test/list/index.js +10 -13
  61. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  62. package/dist/commands/workflow_test/run_all/index.js +67 -36
  63. package/dist/commands/workspace/git/pull/index.js +53 -3
  64. package/dist/commands/workspace/list/index.js +7 -13
  65. package/dist/commands/workspace/pull/index.d.ts +0 -6
  66. package/dist/commands/workspace/pull/index.js +10 -95
  67. package/dist/utils/concurrency.d.ts +29 -0
  68. package/dist/utils/concurrency.js +54 -0
  69. package/dist/utils/document-parser.d.ts +116 -0
  70. package/dist/utils/document-parser.js +274 -1
  71. package/dist/utils/flatten.d.ts +36 -0
  72. package/dist/utils/flatten.js +68 -0
  73. package/dist/utils/knowledge-sync.d.ts +1 -0
  74. package/dist/utils/knowledge-sync.js +11 -5
  75. package/dist/utils/microservice-wait.d.ts +53 -0
  76. package/dist/utils/microservice-wait.js +83 -0
  77. package/dist/utils/multidoc-push.d.ts +12 -0
  78. package/dist/utils/multidoc-push.js +84 -0
  79. package/dist/utils/paging.d.ts +145 -0
  80. package/dist/utils/paging.js +199 -0
  81. package/dist/utils/reference-checker.js +13 -0
  82. package/oclif.manifest.json +2762 -2550
  83. 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, 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 data = (await response.json());
70
- let tests;
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(tests, null, 2));
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) {
@@ -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,34 +64,36 @@ Results: 4 passed, 1 failed
57
64
  // from the same branch.
58
65
  const branch = flags.branch ?? profile.branch;
59
66
  try {
60
- const listParams = new URLSearchParams();
61
- listParams.set('per_page', '10000');
62
- if (branch)
63
- listParams.set('branch', branch);
64
- if (flags['obj-type'])
65
- listParams.set('obj_type', flags['obj-type']);
66
- const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
67
- headers: {
68
- accept: 'application/json',
69
- Authorization: `Bearer ${profile.access_token}`,
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
- method: 'GET',
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
  this.log('No unit tests found');
90
99
  return;
@@ -93,7 +102,15 @@ Results: 4 passed, 1 failed
93
102
  this.log(`Running ${tests.length} unit test${tests.length === 1 ? '' : 's'}...\n`);
94
103
  }
95
104
  const results = [];
96
- for (const test of tests) {
105
+ const runOne = async (test) => {
106
+ // Shadowed accumulators: the body below is the original sequential
107
+ // logic, but it records output instead of printing it so a concurrent
108
+ // run can still emit in input order.
109
+ const results = [];
110
+ const lines = [];
111
+ const log = (message) => {
112
+ lines.push(message);
113
+ };
97
114
  const runUrl = `${baseUrl}/${test.id}/run`;
98
115
  try {
99
116
  const runResponse = await this.verboseFetch(runUrl, {
@@ -115,10 +132,10 @@ Results: 4 passed, 1 failed
115
132
  status: 'fail',
116
133
  });
117
134
  if (flags.output === 'summary') {
118
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
119
- this.log(` Error: API error ${runResponse.status}`);
135
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
136
+ log(` Error: API error ${runResponse.status}`);
120
137
  }
121
- continue;
138
+ return { lines, results };
122
139
  }
123
140
  const runResult = (await runResponse.json());
124
141
  const passed = runResult.status === 'ok';
@@ -132,13 +149,13 @@ Results: 4 passed, 1 failed
132
149
  });
133
150
  if (flags.output === 'summary') {
134
151
  if (passed) {
135
- this.log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
152
+ log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
136
153
  }
137
154
  else {
138
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
155
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
139
156
  for (const expect of failedExpects) {
140
157
  if (expect.message) {
141
- this.log(` Error: ${expect.message}`);
158
+ log(` Error: ${expect.message}`);
142
159
  }
143
160
  }
144
161
  }
@@ -154,11 +171,24 @@ Results: 4 passed, 1 failed
154
171
  status: 'fail',
155
172
  });
156
173
  if (flags.output === 'summary') {
157
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
158
- this.log(` Error: ${message}`);
174
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
175
+ log(` Error: ${message}`);
159
176
  }
160
177
  }
161
- }
178
+ return { lines, results };
179
+ };
180
+ // Print in input order as each test settles, so concurrent output stays
181
+ // diffable against a sequential run.
182
+ const emitter = createOrderedEmitter((settled) => {
183
+ for (const line of settled.lines)
184
+ this.log(line);
185
+ });
186
+ const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
187
+ const settled = await runOne(test);
188
+ emitter.settle(index, settled);
189
+ return settled;
190
+ });
191
+ results.push(...perTest.flatMap((r) => r.results));
162
192
  const passed = results.filter((r) => r.status === 'pass').length;
163
193
  const failed = results.filter((r) => r.status === 'fail').length;
164
194
  if (flags.output === 'json') {
@@ -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 data = (await response.json());
63
- let tests;
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(tests, null, 2));
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) {
@@ -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,32 +56,34 @@ 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
- const listParams = new URLSearchParams();
53
- listParams.set('per_page', '10000');
54
- if (flags.branch)
55
- listParams.set('branch', flags.branch);
56
- const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
57
- headers: {
58
- accept: 'application/json',
59
- Authorization: `Bearer ${profile.access_token}`,
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
- method: 'GET',
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
  this.log('No workflow tests found');
80
89
  return;
@@ -83,7 +92,15 @@ Results: 2 passed, 1 failed
83
92
  this.log(`Running ${tests.length} workflow test${tests.length === 1 ? '' : 's'}...\n`);
84
93
  }
85
94
  const results = [];
86
- for (const test of tests) {
95
+ const runOne = async (test) => {
96
+ // Shadowed accumulators: the body below is the original sequential
97
+ // logic, but it records output instead of printing it so a concurrent
98
+ // run can still emit in input order.
99
+ const results = [];
100
+ const lines = [];
101
+ const log = (message) => {
102
+ lines.push(message);
103
+ };
87
104
  const runUrl = `${baseUrl}/${test.id}/run`;
88
105
  try {
89
106
  const runResponse = await this.verboseFetch(runUrl, {
@@ -102,10 +119,10 @@ Results: 2 passed, 1 failed
102
119
  status: 'fail',
103
120
  });
104
121
  if (flags.output === 'summary') {
105
- this.log(`FAIL ${test.name}`);
106
- this.log(` Error: API error ${runResponse.status}`);
122
+ log(`FAIL ${test.name}`);
123
+ log(` Error: API error ${runResponse.status}`);
107
124
  }
108
- continue;
125
+ return { lines, results };
109
126
  }
110
127
  const runResult = (await runResponse.json());
111
128
  const passed = runResult.status === 'ok';
@@ -118,12 +135,12 @@ Results: 2 passed, 1 failed
118
135
  if (flags.output === 'summary') {
119
136
  const timing = runResult.timing ? ` (${runResult.timing}s)` : '';
120
137
  if (passed) {
121
- this.log(`PASS ${test.name}${timing}`);
138
+ log(`PASS ${test.name}${timing}`);
122
139
  }
123
140
  else {
124
- this.log(`FAIL ${test.name}${timing}`);
141
+ log(`FAIL ${test.name}${timing}`);
125
142
  if (runResult.message) {
126
- this.log(` Error: ${runResult.message}`);
143
+ log(` Error: ${runResult.message}`);
127
144
  }
128
145
  }
129
146
  }
@@ -136,11 +153,24 @@ Results: 2 passed, 1 failed
136
153
  status: 'fail',
137
154
  });
138
155
  if (flags.output === 'summary') {
139
- this.log(`FAIL ${test.name}`);
140
- this.log(` Error: ${message}`);
156
+ log(`FAIL ${test.name}`);
157
+ log(` Error: ${message}`);
141
158
  }
142
159
  }
143
- }
160
+ return { lines, results };
161
+ };
162
+ // Print in input order as each test settles, so concurrent output stays
163
+ // diffable against a sequential run.
164
+ const emitter = createOrderedEmitter((settled) => {
165
+ for (const line of settled.lines)
166
+ this.log(line);
167
+ });
168
+ const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
169
+ const settled = await runOne(test);
170
+ emitter.settle(index, settled);
171
+ return settled;
172
+ });
173
+ results.push(...perTest.flatMap((r) => r.results));
144
174
  const passed = results.filter((r) => r.status === 'pass').length;
145
175
  const failed = results.filter((r) => r.status === 'fail').length;
146
176
  if (flags.output === 'json') {
@@ -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 data = await response.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
- }
68
+ const list = normalizeListResponse(await response.json());
69
+ const tests = list.items;
78
70
  if (flags.output === 'json') {
79
- this.log(JSON.stringify(tests, null, 2));
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) {
@@ -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>;