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