@astrale-os/cli 1.0.0-beta.17 → 1.0.0-beta.19

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.
@@ -8242,14 +8242,14 @@ function isPidAlive(pid) {
8242
8242
  function sleep(milliseconds) {
8243
8243
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
8244
8244
  }
8245
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.10/node_modules/@astrale-os/kernel-dsl/dist/value/limits.js
8245
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.12/node_modules/@astrale-os/kernel-dsl/dist/value/limits.js
8246
8246
  var defaultLimits = Object.freeze({
8247
8247
  maxBytes: 16 * 1024 * 1024,
8248
8248
  maxDepth: 64,
8249
8249
  maxMembers: 1e5,
8250
8250
  maxStringBytes: 1024 * 1024
8251
8251
  });
8252
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.10/node_modules/@astrale-os/kernel-dsl/dist/value/canonical.js
8252
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.12/node_modules/@astrale-os/kernel-dsl/dist/value/canonical.js
8253
8253
  var encoder2 = new TextEncoder;
8254
8254
  // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
8255
8255
  var CharacterCodes;
@@ -8429,7 +8429,7 @@ var ParseErrorCode;
8429
8429
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
8430
8430
  })(ParseErrorCode || (ParseErrorCode = {}));
8431
8431
 
8432
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.10/node_modules/@astrale-os/kernel-dsl/dist/value/decode.js
8432
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.12/node_modules/@astrale-os/kernel-dsl/dist/value/decode.js
8433
8433
  var decoder2 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
8434
8434
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
8435
8435
  var exports_external = {};
@@ -23659,7 +23659,7 @@ var log = {
23659
23659
  dim: (msg) => console.log(source_default.dim(msg))
23660
23660
  };
23661
23661
  var IS_CI = !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.NO_SPINNER);
23662
- // node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.13/node_modules/@astrale-os/kernel-core/dist/dns-label.js
23662
+ // node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.15/node_modules/@astrale-os/kernel-core/dist/dns-label.js
23663
23663
  var DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
23664
23664
  function isDnsLabel(value) {
23665
23665
  return DNS_LABEL_RE.test(value);
@@ -11,7 +11,7 @@ export type MachineOpts = RawOutputOpts & {
11
11
  };
12
12
  export declare const RAW_OUTPUT_OPTIONS: readonly [{
13
13
  readonly flags: '--json';
14
- readonly description: 'Always-valid JSON (for jq)';
14
+ readonly description: 'JSON; streaming commands emit one JSON value per line';
15
15
  }, {
16
16
  readonly flags: '--raw';
17
17
  readonly description: 'Unwrapped: bare scalar / raw bytes / JSON for objects';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.17",
3
+ "version": "1.0.0-beta.19",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@astrale-os/ox": ">=0.1.3 <1.0.0",
65
- "@astrale-os/sdk": "0.5.0-beta.38",
65
+ "@astrale-os/sdk": "0.5.0-beta.49",
66
66
  "@astrale-os/shell": "0.4.2-beta.3",
67
67
  "@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
68
68
  "@commitlint/cli": "21.2.2",
@@ -38,9 +38,7 @@ describe('direct domain install operation identity', () => {
38
38
  recovery = input.recovery
39
39
  requests.push(
40
40
  await input.fn({
41
- session: {
42
- call: async (call: { input: unknown }) => call.input,
43
- },
41
+ session: { schema: { install: async (input: unknown) => input } },
44
42
  } as never),
45
43
  )
46
44
  },
@@ -51,7 +49,7 @@ describe('direct domain install operation identity', () => {
51
49
  expect(requests).toEqual([
52
50
  {
53
51
  operation: GENERATED,
54
- domains: [{ source: { kind: 'remote', url: 'https://crm.test' } }],
52
+ domains: [{ publication: { url: 'https://crm.test' } }],
55
53
  },
56
54
  ])
57
55
  expect(recovery).toEqual({
@@ -78,9 +76,7 @@ describe('direct domain install operation identity', () => {
78
76
  runKernelCommand: async (input) => {
79
77
  requests.push(
80
78
  await input.fn({
81
- session: {
82
- call: async (call: { input: unknown }) => call.input,
83
- },
79
+ session: { schema: { install: async (input: unknown) => input } },
84
80
  } as never),
85
81
  )
86
82
  },
@@ -92,7 +88,7 @@ describe('direct domain install operation identity', () => {
92
88
  expect(requests).toEqual([
93
89
  {
94
90
  operation: RETRY,
95
- domains: [{ source: { kind: 'remote', url: 'https://crm.test' } }],
91
+ domains: [{ publication: { url: 'https://crm.test' } }],
96
92
  },
97
93
  ])
98
94
  })
@@ -1,21 +1,128 @@
1
+ import type { InstallResult } from '@astrale-os/sdk/client/schema'
2
+
3
+ import { defineSchema, schema } from '@astrale-os/sdk/schema'
1
4
  import { describe, expect, test } from 'bun:test'
2
5
 
3
- import { directInstallCallInput } from '../domain/install'
6
+ import { directInstallCallInput, directInstallPresentation } from '../domain/install'
7
+
8
+ const currentRevision = schema.revision(defineSchema('tasks.astrale.ai', {}))
4
9
 
5
10
  describe('directInstallCallInput', () => {
6
11
  test('sends the current remote install syscall, not a legacy url list', () => {
7
12
  const operation = '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8'
8
- expect(directInstallCallInput('https://tasks.example.test', operation, 'secret')).toEqual({
9
- operation,
13
+ const input = directInstallCallInput('https://tasks.example.test', operation, 'secret')
14
+
15
+ expect(String(input.operation)).toBe(operation)
16
+ expect(input.domains).toEqual([
17
+ {
18
+ publication: {
19
+ url: 'https://tasks.example.test',
20
+ token: 'secret',
21
+ },
22
+ },
23
+ ])
24
+ })
25
+
26
+ test('presents a committed install from its receipt', () => {
27
+ const operation = directInstallCallInput(
28
+ 'https://tasks.example.test',
29
+ '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
30
+ ).operation
31
+ const result = {
32
+ changed: true,
33
+ receipt: {
34
+ operation,
35
+ transitions: [
36
+ {
37
+ intent: {
38
+ transition: 'transition-1',
39
+ operation,
40
+ origin: 'tasks.astrale.ai',
41
+ previous: null,
42
+ generation: {
43
+ origin: 'tasks.astrale.ai',
44
+ revision: currentRevision,
45
+ generation: 'sha256:generation',
46
+ },
47
+ },
48
+ phase: 'cutover',
49
+ state: 'committed',
50
+ },
51
+ ],
52
+ },
53
+ } satisfies InstallResult
54
+
55
+ expect(directInstallPresentation(result, 'ignored')).toEqual({
56
+ operation: '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
57
+ origin: 'tasks.astrale.ai',
58
+ revision: currentRevision,
59
+ status: 'installed',
60
+ })
61
+ })
62
+
63
+ test('presents an idempotent install from the current Domain observation', () => {
64
+ const operation = '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8'
65
+ const result = {
66
+ changed: false,
10
67
  domains: [
11
68
  {
12
- source: {
13
- kind: 'remote',
14
- url: 'https://tasks.example.test',
15
- token: 'secret',
16
- },
69
+ origin: 'tasks.astrale.ai',
70
+ revision: currentRevision,
71
+ generation: 'sha256:generation',
72
+ publication: null,
73
+ readiness: 'sha256:readiness',
74
+ capabilities: { requested: {}, materialized: {} },
75
+ bindings: { callables: [], views: [] },
17
76
  },
18
77
  ],
78
+ } satisfies InstallResult
79
+
80
+ expect(directInstallPresentation(result, operation)).toEqual({
81
+ operation,
82
+ origin: 'tasks.astrale.ai',
83
+ revision: currentRevision,
84
+ status: 'already current',
19
85
  })
20
86
  })
87
+
88
+ test('rejects a changed result without a committed transition', () => {
89
+ const result = {
90
+ changed: true,
91
+ receipt: { operation: '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8', transitions: [] },
92
+ } as unknown as InstallResult
93
+
94
+ expect(() => directInstallPresentation(result, 'ignored')).toThrow(
95
+ 'Kernel install returned no committed Domain transition.',
96
+ )
97
+ })
98
+
99
+ test('rejects a committed transition without an installed generation', () => {
100
+ const operation = directInstallCallInput(
101
+ 'https://tasks.example.test',
102
+ '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
103
+ ).operation
104
+ const result = {
105
+ changed: true,
106
+ receipt: {
107
+ operation,
108
+ transitions: [
109
+ {
110
+ intent: {
111
+ transition: 'transition-1',
112
+ operation,
113
+ origin: 'tasks.astrale.ai',
114
+ previous: null,
115
+ generation: null,
116
+ },
117
+ phase: 'cutover',
118
+ state: 'committed',
119
+ },
120
+ ],
121
+ },
122
+ } satisfies InstallResult
123
+
124
+ expect(() => directInstallPresentation(result, 'ignored')).toThrow(
125
+ 'Kernel install returned a committed transition without a Domain generation.',
126
+ )
127
+ })
21
128
  })
@@ -1,6 +1,44 @@
1
+ import type { DomainBundle, DomainInfo } from '@astrale-os/sdk/client/schema'
2
+
3
+ import { bundle, defineSchema, func, method, nodeClass, schema } from '@astrale-os/sdk/schema'
1
4
  import { describe, expect, test } from 'bun:test'
2
5
 
3
- import { parseIntrospectTarget } from '../introspect'
6
+ import { introspectCommand, parseIntrospectTarget } from '../introspect'
7
+
8
+ const source = defineSchema('host.astrale.ai', {
9
+ classes: {
10
+ Manager: nodeClass({
11
+ methods: {
12
+ createInstance: method({
13
+ auth: 'authenticated',
14
+ input: { type: 'object', properties: {}, additionalProperties: false },
15
+ output: { type: 'object', additionalProperties: true },
16
+ static: true,
17
+ }),
18
+ },
19
+ }),
20
+ },
21
+ functions: {
22
+ journal: func({
23
+ auth: 'authenticated',
24
+ input: { type: 'object', properties: {}, additionalProperties: false },
25
+ output: { type: 'object', additionalProperties: true },
26
+ }),
27
+ },
28
+ })
29
+
30
+ const info = {
31
+ origin: source.origin,
32
+ revision: schema.revision(source),
33
+ generation: 'sha256:generation',
34
+ publication: null,
35
+ readiness: 'sha256:readiness',
36
+ capabilities: { requested: {}, materialized: {} },
37
+ bindings: { callables: [], views: [] },
38
+ } satisfies DomainInfo
39
+
40
+ const installed = bundle.create(source)
41
+ const bundled = { domain: info, bundle: installed } satisfies DomainBundle
4
42
 
5
43
  describe('parseIntrospectTarget', () => {
6
44
  test('accepts a bare origin', () => {
@@ -18,4 +56,45 @@ describe('parseIntrospectTarget', () => {
18
56
  test('rejects an @id', () => {
19
57
  expect(() => parseIntrospectTarget('@abc')).toThrow('not an @id')
20
58
  })
59
+
60
+ test('routes bare, bundle, Method, and Function commands through the current Schema API', async () => {
61
+ const calls: string[] = []
62
+ const schema = {
63
+ inspect: async (origin: string) => {
64
+ calls.push(`inspect:${origin}`)
65
+ return info
66
+ },
67
+ bundle: async (origin: string) => {
68
+ calls.push(`bundle:${origin}`)
69
+ return bundled
70
+ },
71
+ }
72
+ const results: unknown[] = []
73
+ const runKernelCommand = async (input: {
74
+ fn: (context: unknown) => Promise<unknown>
75
+ }): Promise<void> => {
76
+ results.push(await input.fn({ session: { schema } }))
77
+ }
78
+ const dependencies = { runKernelCommand: runKernelCommand as never }
79
+
80
+ await introspectCommand('host.astrale.ai', { json: true }, dependencies)
81
+ await introspectCommand('host.astrale.ai', { bundle: true, json: true }, dependencies)
82
+ await introspectCommand(
83
+ '/:host.astrale.ai:class.Manager:createInstance',
84
+ { json: true },
85
+ dependencies,
86
+ )
87
+ await introspectCommand('/:host.astrale.ai:function.journal', { json: true }, dependencies)
88
+
89
+ expect(results[0]).toBe(info)
90
+ expect(results[1]).toBe(bundled)
91
+ expect(results[2]).toMatchObject({ class: 'Manager', method: 'createInstance' })
92
+ expect(results[3]).toMatchObject({ function: 'journal' })
93
+ expect(calls).toEqual([
94
+ 'inspect:host.astrale.ai',
95
+ 'bundle:host.astrale.ai',
96
+ 'bundle:host.astrale.ai',
97
+ 'bundle:host.astrale.ai',
98
+ ])
99
+ })
21
100
  })
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
 
3
- import { acceptJournalPage, buildJournalInput } from '../logs'
3
+ import logsCommand, {
4
+ acceptJournalPage,
5
+ buildJournalInput,
6
+ followLogs,
7
+ formatFollowRecord,
8
+ } from '../logs'
4
9
 
5
10
  describe('buildJournalInput', () => {
6
11
  /** @evidence TEST-CLI-LOGS-MAPS-EXACT-JOURNAL-INPUT */
@@ -81,7 +86,15 @@ describe('acceptJournalPage', () => {
81
86
  occurredAt: '2026-08-19T16:51:10.049Z',
82
87
  committedAt: '2026-08-19T16:51:10.070Z',
83
88
  payload: { outcome: 'rejected' },
84
- correlation: { invocationId: 'cf862a64-3aa1-4343-ba86-f9b516c4ff95' },
89
+ correlation: {
90
+ operationId: 'operation-child',
91
+ parentOperationId: 'operation-parent',
92
+ invocationId: 'cf862a64-3aa1-4343-ba86-f9b516c4ff95',
93
+ rootInvocationId: 'invocation-root',
94
+ parentInvocationId: 'invocation-parent',
95
+ traceId: 'trace-1',
96
+ spanId: 'span-1',
97
+ },
85
98
  },
86
99
  ],
87
100
  })
@@ -91,7 +104,242 @@ describe('acceptJournalPage', () => {
91
104
  timestamp: '2026-08-19T16:51:10.049Z',
92
105
  occurredAt: '2026-08-19T16:51:10.049Z',
93
106
  committedAt: '2026-08-19T16:51:10.070Z',
107
+ correlation: {
108
+ operationId: 'operation-child',
109
+ parentOperationId: 'operation-parent',
110
+ invocationId: 'cf862a64-3aa1-4343-ba86-f9b516c4ff95',
111
+ rootInvocationId: 'invocation-root',
112
+ parentInvocationId: 'invocation-parent',
113
+ traceId: 'trace-1',
114
+ spanId: 'span-1',
115
+ },
94
116
  correlationId: 'cf862a64-3aa1-4343-ba86-f9b516c4ff95',
95
117
  })
96
118
  })
119
+
120
+ test('rejects malformed or invented structured correlation fields', () => {
121
+ const record = {
122
+ sequence: 1,
123
+ topic: 'function.invoke',
124
+ occurredAt: '2026-08-19T16:51:10.049Z',
125
+ payload: {},
126
+ }
127
+ expect(() => acceptJournalPage({ records: [{ ...record, correlation: 'opaque' }] })).toThrow(
128
+ 'correlation must be an object',
129
+ )
130
+ expect(() =>
131
+ acceptJournalPage({ records: [{ ...record, correlation: { authority: 'forged' } }] }),
132
+ ).toThrow('correlation.authority is unsupported')
133
+ for (const field of [
134
+ 'operationId',
135
+ 'parentOperationId',
136
+ 'invocationId',
137
+ 'rootInvocationId',
138
+ 'parentInvocationId',
139
+ 'traceId',
140
+ 'spanId',
141
+ ]) {
142
+ expect(() =>
143
+ acceptJournalPage({ records: [{ ...record, correlation: { [field]: 7 } }] }),
144
+ ).toThrow(`correlation.${field}`)
145
+ }
146
+ expect(() =>
147
+ acceptJournalPage({ records: [{ ...record, correlation: { invocationId: ' ' } }] }),
148
+ ).toThrow('must be non-empty')
149
+ expect(() =>
150
+ acceptJournalPage({
151
+ records: [{ ...record, correlation: { invocationId: 'x'.repeat(257) } }],
152
+ }),
153
+ ).toThrow('at most 256 UTF-8 bytes')
154
+ expect(
155
+ acceptJournalPage({
156
+ records: [{ ...record, correlation: { invocationId: 'x'.repeat(256) } }],
157
+ }).records[0].correlation?.invocationId,
158
+ ).toHaveLength(256)
159
+ expect(
160
+ acceptJournalPage({
161
+ records: [{ ...record, correlation: { invocationId: 'é'.repeat(128) } }],
162
+ }).records[0].correlation?.invocationId,
163
+ ).toHaveLength(128)
164
+ expect(() =>
165
+ acceptJournalPage({
166
+ records: [{ ...record, correlation: { invocationId: `${'é'.repeat(127)}€` } }],
167
+ }),
168
+ ).toThrow('at most 256 UTF-8 bytes')
169
+ })
170
+
171
+ test('keeps legacy identity compatibility coherent with structured correlation', () => {
172
+ const record = {
173
+ sequence: 1,
174
+ topic: 'function.invoke',
175
+ occurredAt: '2026-08-19T16:51:10.049Z',
176
+ payload: {},
177
+ }
178
+ expect(
179
+ acceptJournalPage({
180
+ records: [{ ...record, correlationId: 'legacy-only', causationId: 'legacy-cause' }],
181
+ }).records[0],
182
+ ).toMatchObject({ correlationId: 'legacy-only', causationId: 'legacy-cause' })
183
+ expect(
184
+ acceptJournalPage({
185
+ records: [
186
+ {
187
+ ...record,
188
+ correlationId: 'same',
189
+ correlation: { invocationId: 'same' },
190
+ },
191
+ ],
192
+ }).records[0],
193
+ ).toMatchObject({ correlationId: 'same', correlation: { invocationId: 'same' } })
194
+ expect(() =>
195
+ acceptJournalPage({
196
+ records: [
197
+ {
198
+ ...record,
199
+ correlationId: 'legacy',
200
+ correlation: { invocationId: 'structured' },
201
+ },
202
+ ],
203
+ }),
204
+ ).toThrow('conflicting correlation identifiers')
205
+ })
206
+
207
+ test('serializes one complete structured record per machine-follow line', () => {
208
+ const record = acceptJournalPage({
209
+ records: [
210
+ {
211
+ sequence: 2,
212
+ topic: 'function.invoke',
213
+ occurredAt: '2026-08-19T16:51:10.049Z',
214
+ payload: { outcome: 'completed' },
215
+ correlation: {
216
+ operationId: 'operation-child',
217
+ parentOperationId: 'operation-parent',
218
+ invocationId: 'invocation-child',
219
+ rootInvocationId: 'invocation-root',
220
+ parentInvocationId: 'invocation-parent',
221
+ traceId: 'trace-1',
222
+ spanId: 'span-1',
223
+ },
224
+ },
225
+ ],
226
+ }).records[0]
227
+ expect(formatFollowRecord(record).endsWith('\n')).toBe(true)
228
+ expect(JSON.parse(formatFollowRecord(record))).toEqual(record)
229
+ })
230
+ })
231
+
232
+ describe('follow output routing', () => {
233
+ const inputRecord = {
234
+ sequence: 2,
235
+ topic: 'function.invoke',
236
+ occurredAt: '2026-08-19T16:51:10.049Z',
237
+ payload: { outcome: 'completed' },
238
+ principal: 'principal-1',
239
+ correlation: {
240
+ invocationId: 'invocation-child',
241
+ rootInvocationId: 'invocation-root',
242
+ parentInvocationId: 'invocation-parent',
243
+ },
244
+ }
245
+ const admittedRecord = acceptJournalPage({ records: [inputRecord] }).records[0]
246
+
247
+ test('routes every effective machine mode through complete NDJSON records', async () => {
248
+ for (const { opts, tty } of [
249
+ { opts: { json: true }, tty: true },
250
+ { opts: { raw: true }, tty: true },
251
+ { opts: { format: 'json' as const }, tty: true },
252
+ { opts: { ci: true }, tty: true },
253
+ { opts: {}, tty: false },
254
+ { opts: { format: 'yaml' as const, json: true }, tty: true },
255
+ { opts: { format: 'yaml' as const, raw: true }, tty: true },
256
+ ]) {
257
+ const stdout = await captureFollowOutput({ ...opts, follow: true }, tty)
258
+ expect(stdout.endsWith('\n')).toBe(true)
259
+ expect(JSON.parse(stdout)).toEqual(admittedRecord)
260
+ }
261
+ })
262
+
263
+ test('keeps an unflagged TTY human-readable', async () => {
264
+ const stdout = await captureFollowOutput({ follow: true }, true)
265
+ expect(stdout).toContain('function.invoke')
266
+ expect(stdout).toContain('principal-1')
267
+ expect(stdout).not.toContain('invocation-child')
268
+ expect(() => JSON.parse(stdout)).toThrow()
269
+ })
270
+
271
+ test('rejects effective YAML before opening a Kernel session with INVALID_INPUT', async () => {
272
+ let runCalls = 0
273
+ await expect(
274
+ followLogs(
275
+ { follow: true, format: 'yaml' },
276
+ {
277
+ run: async () => {
278
+ runCalls += 1
279
+ },
280
+ pause: async () => {},
281
+ },
282
+ ),
283
+ ).rejects.toThrow('--follow does not support YAML')
284
+ expect(runCalls).toBe(0)
285
+
286
+ const originalExit = process.exit
287
+ const originalStderrWrite = process.stderr.write
288
+ let stderr = ''
289
+ process.stderr.write = ((chunk: string | Uint8Array) => {
290
+ stderr += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
291
+ return true
292
+ }) as typeof process.stderr.write
293
+ process.exit = ((code?: string | number | null) => {
294
+ throw new Error(`exit:${String(code)}`)
295
+ }) as typeof process.exit
296
+ try {
297
+ await expect(logsCommand.action({ follow: true, format: 'yaml' })).rejects.toThrow('exit:1')
298
+ expect(JSON.parse(stderr)).toEqual({
299
+ error: 'INVALID_INPUT',
300
+ message: '--follow does not support YAML; use --json for an NDJSON stream',
301
+ })
302
+ } finally {
303
+ process.exit = originalExit
304
+ process.stderr.write = originalStderrWrite
305
+ }
306
+ })
307
+
308
+ async function captureFollowOutput(
309
+ opts: Parameters<typeof followLogs>[0],
310
+ tty: boolean,
311
+ ): Promise<string> {
312
+ const originalWrite = process.stdout.write
313
+ const originalTty = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY')
314
+ let stdout = ''
315
+ let pages = 0
316
+ process.stdout.write = ((chunk: string | Uint8Array) => {
317
+ stdout += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
318
+ return true
319
+ }) as typeof process.stdout.write
320
+ Object.defineProperty(process.stdout, 'isTTY', { configurable: true, value: tty })
321
+ try {
322
+ await expect(
323
+ followLogs(opts, {
324
+ run: async (input) => {
325
+ await input.fn({
326
+ session: {
327
+ call: async () => {
328
+ pages += 1
329
+ if (pages === 1) return { records: [inputRecord] }
330
+ throw new Error('end of controlled stream')
331
+ },
332
+ },
333
+ } as never)
334
+ },
335
+ pause: async () => {},
336
+ }),
337
+ ).rejects.toThrow('end of controlled stream')
338
+ return stdout
339
+ } finally {
340
+ process.stdout.write = originalWrite
341
+ if (originalTty === undefined) delete (process.stdout as { isTTY?: boolean }).isTTY
342
+ else Object.defineProperty(process.stdout, 'isTTY', originalTty)
343
+ }
344
+ }
97
345
  })