@robota-sdk/agent-transport 3.0.0-beta.72 → 3.0.0-beta.74

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 (171) hide show
  1. package/dist/node/headless/index.cjs +1 -1
  2. package/dist/node/headless/index.d.ts +1 -1
  3. package/dist/node/headless/index.js +1 -1
  4. package/dist/node/{headless-DCtHvyVf.cjs → headless-BeHAOlIM.cjs} +4 -3
  5. package/dist/node/{headless-C6tj35h3.js → headless-D02zUEGh.js} +4 -3
  6. package/dist/node/headless-D02zUEGh.js.map +1 -0
  7. package/dist/node/http/index.cjs +1 -1
  8. package/dist/node/http/index.d.ts +1 -1
  9. package/dist/node/http/index.js +1 -1
  10. package/dist/node/{http-Br10Ps8m.js → http-2Jiuflc1.js} +1 -1
  11. package/dist/node/http-2Jiuflc1.js.map +1 -0
  12. package/dist/node/http-CBAvefLw.cjs +1 -0
  13. package/dist/node/{index-BVNhOeeU.d.ts → index-BQLN_Lc9.d.ts} +5 -3
  14. package/dist/node/index-BQLN_Lc9.d.ts.map +1 -0
  15. package/dist/node/{index-C9LWCL4l.d.ts → index-BnAGE-u9.d.ts} +2 -3
  16. package/dist/node/index-BnAGE-u9.d.ts.map +1 -0
  17. package/dist/node/{index-COWvtBa2.d.ts → index-BrQ4gGw0.d.ts} +3 -3
  18. package/dist/node/index-BrQ4gGw0.d.ts.map +1 -0
  19. package/dist/node/{index-X2Zg8FEY.d.ts → index-CoeBF21y.d.ts} +3 -3
  20. package/dist/node/index-CoeBF21y.d.ts.map +1 -0
  21. package/dist/node/{index-27HV5PJB.d.ts → index-DE3-dHqw.d.ts} +8 -3
  22. package/dist/node/index-DE3-dHqw.d.ts.map +1 -0
  23. package/dist/node/{index-BRgV_MPB.d.ts → index-DHt-2VQ-.d.ts} +2 -3
  24. package/dist/node/index-DHt-2VQ-.d.ts.map +1 -0
  25. package/dist/node/{index-nBlMTFkZ.d.ts → index-DMwKN5Le.d.ts} +2 -3
  26. package/dist/node/index-DMwKN5Le.d.ts.map +1 -0
  27. package/dist/node/{index-TMAlNHuM.d.ts → index-IvYaYY6v.d.ts} +5 -3
  28. package/dist/node/index-IvYaYY6v.d.ts.map +1 -0
  29. package/dist/node/{index-BRchlFBE.d.ts → index-WKTgvhlg.d.ts} +8 -3
  30. package/dist/node/index-WKTgvhlg.d.ts.map +1 -0
  31. package/dist/node/{index-C5KNEBO9.d.ts → index-c0M42fsA.d.ts} +2 -3
  32. package/dist/node/index-c0M42fsA.d.ts.map +1 -0
  33. package/dist/node/index.cjs +1 -1
  34. package/dist/node/index.d.ts +6 -7
  35. package/dist/node/index.d.ts.map +1 -1
  36. package/dist/node/index.js +1 -1
  37. package/dist/node/index.js.map +1 -1
  38. package/dist/node/mcp/index.cjs +1 -1
  39. package/dist/node/mcp/index.d.ts +1 -1
  40. package/dist/node/mcp/index.js +1 -1
  41. package/dist/node/mcp-BOglBJNy.cjs +1 -0
  42. package/dist/node/{mcp-BAujHOMr.js → mcp-D3BBVK7C.js} +1 -1
  43. package/dist/node/mcp-D3BBVK7C.js.map +1 -0
  44. package/dist/node/{chunk-Bmb41Sf3.cjs → rolldown-runtime-CMqjfN_6.cjs} +1 -1
  45. package/dist/node/testing/index.cjs +1 -0
  46. package/dist/node/testing/index.d.ts +21 -0
  47. package/dist/node/testing/index.d.ts.map +1 -0
  48. package/dist/node/testing/index.js +2 -0
  49. package/dist/node/testing/index.js.map +1 -0
  50. package/dist/node/tui/index.cjs +1 -1
  51. package/dist/node/tui/index.d.ts +1 -1
  52. package/dist/node/tui/index.js +1 -1
  53. package/dist/node/{tui-4hA-SMtS.js → tui-Btb1q88j.js} +5 -5
  54. package/dist/node/tui-Btb1q88j.js.map +1 -0
  55. package/dist/node/tui-SbUT7Zlt.cjs +24 -0
  56. package/dist/node/ws/index.cjs +1 -1
  57. package/dist/node/ws/index.d.ts +1 -1
  58. package/dist/node/ws/index.js +1 -1
  59. package/dist/node/{ws-BWel8nzl.js → ws-Dc2RUwVs.js} +1 -1
  60. package/dist/node/ws-Dc2RUwVs.js.map +1 -0
  61. package/dist/node/ws-QNMQn5kg.cjs +1 -0
  62. package/package.json +35 -22
  63. package/src/headless/HeadlessInteractionChannel.ts +9 -1
  64. package/src/headless/__tests__/headless-channel-options.test.ts +106 -0
  65. package/src/headless/__tests__/headless-provider-failure.integration.test.ts +143 -0
  66. package/src/headless/__tests__/headless-runner-initialization.test.ts +1 -1
  67. package/src/headless/__tests__/headless-runner.test.ts +24 -3
  68. package/src/headless/__tests__/headless-transport.test.ts +1 -2
  69. package/src/headless/headless-runner.ts +3 -2
  70. package/src/headless/headless-stream-json.ts +5 -5
  71. package/src/headless/headless-transport.ts +1 -2
  72. package/src/http/__tests__/http-transport.test.ts +1 -1
  73. package/src/http/__tests__/routes.test.ts +1 -1
  74. package/src/http/http-transport.ts +1 -2
  75. package/src/http/routes.ts +1 -1
  76. package/src/mcp/__tests__/mcp-server.test.ts +1 -1
  77. package/src/mcp/__tests__/mcp-transport.test.ts +1 -1
  78. package/src/mcp/mcp-server.ts +1 -1
  79. package/src/mcp/mcp-transport.ts +1 -2
  80. package/src/testing/__tests__/scripted-provider.test.ts +73 -0
  81. package/src/testing/index.ts +7 -0
  82. package/src/testing/scripted-provider.ts +73 -0
  83. package/src/transport-registry.ts +1 -1
  84. package/src/tui/App.tsx +38 -29
  85. package/src/tui/BackgroundTaskPanel.tsx +1 -1
  86. package/src/tui/CjkTextInput.tsx +4 -8
  87. package/src/tui/ExecutionWorkspaceDetailPane.tsx +1 -1
  88. package/src/tui/ExecutionWorkspaceSwitcher.tsx +1 -1
  89. package/src/tui/InputArea.tsx +15 -7
  90. package/src/tui/InteractivePrompt.tsx +2 -2
  91. package/src/tui/PluginTUI.tsx +1 -1
  92. package/src/tui/SessionPicker.tsx +1 -1
  93. package/src/tui/SessionStatusBar.tsx +1 -1
  94. package/src/tui/SlashAutocomplete.tsx +1 -1
  95. package/src/tui/StatusBar.tsx +1 -7
  96. package/src/tui/StreamingIndicator.tsx +1 -1
  97. package/src/tui/TransportTUI.tsx +1 -1
  98. package/src/tui/TuiInteractionChannel.ts +60 -38
  99. package/src/tui/UsageSummaryEntry.tsx +1 -1
  100. package/src/tui/__tests__/PluginTUI.test.tsx +1 -1
  101. package/src/tui/__tests__/SlashAutocomplete.test.tsx +1 -1
  102. package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +1 -1
  103. package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +5 -2
  104. package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +1 -1
  105. package/src/tui/__tests__/background-task-panel.test.tsx +1 -1
  106. package/src/tui/__tests__/background-task-row-format.test.ts +1 -1
  107. package/src/tui/__tests__/channel-factory-integration.test.ts +138 -0
  108. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +1 -1
  109. package/src/tui/__tests__/execution-workspace-view-model.test.ts +1 -1
  110. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +1 -1
  111. package/src/tui/__tests__/input-area-flow.test.ts +1 -1
  112. package/src/tui/__tests__/pty/pty-driver.ts +135 -0
  113. package/src/tui/__tests__/pty/tui-pty.ptytest.ts +61 -0
  114. package/src/tui/__tests__/render-channel-options.test.ts +32 -0
  115. package/src/tui/__tests__/session-init-poller.test.ts +102 -0
  116. package/src/tui/__tests__/session-switch-channel.test.tsx +307 -0
  117. package/src/tui/__tests__/slash-routing-effects.test.ts +4 -1
  118. package/src/tui/__tests__/status-activity.test.ts +3 -3
  119. package/src/tui/__tests__/status-bar.test.tsx +7 -6
  120. package/src/tui/__tests__/tui-channel-init-failure.test.ts +57 -0
  121. package/src/tui/__tests__/tui-state-manager.test.ts +1 -1
  122. package/src/tui/background-task-row-format.ts +1 -1
  123. package/src/tui/execution-workspace-view-model.ts +1 -1
  124. package/src/tui/flows/input-area-flow.ts +1 -1
  125. package/src/tui/flows/permission-prompt-flow.ts +1 -1
  126. package/src/tui/flows/session-init-poller.ts +77 -0
  127. package/src/tui/hooks/command-effect-handler.ts +4 -1
  128. package/src/tui/hooks/command-effect-queue.ts +1 -1
  129. package/src/tui/hooks/side-effects-types.ts +2 -2
  130. package/src/tui/hooks/useAutocomplete.ts +3 -2
  131. package/src/tui/hooks/usePluginCallbacks.ts +1 -1
  132. package/src/tui/hooks/usePluginScreenData.ts +1 -1
  133. package/src/tui/hooks/useSideEffects.ts +1 -1
  134. package/src/tui/hooks/useSlashRouting.ts +3 -3
  135. package/src/tui/hooks/useStatusLineSettings.ts +1 -1
  136. package/src/tui/hooks/useTuiChannel.ts +3 -3
  137. package/src/tui/plugin-tui-handlers.ts +1 -1
  138. package/src/tui/render.tsx +38 -25
  139. package/src/tui/status-activity.ts +2 -2
  140. package/src/tui/tui-cli-adapter.ts +3 -3
  141. package/src/tui/tui-state-manager.ts +2 -2
  142. package/src/tui/tui-transport.ts +4 -2
  143. package/src/ws/__tests__/ws-handler.test.ts +6 -4
  144. package/src/ws/__tests__/ws-transport.test.ts +1 -1
  145. package/src/ws/ws-background-messages.ts +1 -1
  146. package/src/ws/ws-handler.ts +4 -4
  147. package/src/ws/ws-protocol.ts +6 -4
  148. package/src/ws/ws-transport-configurable.ts +4 -2
  149. package/src/ws/ws-transport.ts +1 -2
  150. package/dist/node/headless-C6tj35h3.js.map +0 -1
  151. package/dist/node/http-Br10Ps8m.js.map +0 -1
  152. package/dist/node/http-Da6Kw4oy.cjs +0 -1
  153. package/dist/node/index-27HV5PJB.d.ts.map +0 -1
  154. package/dist/node/index-BRchlFBE.d.ts.map +0 -1
  155. package/dist/node/index-BRgV_MPB.d.ts.map +0 -1
  156. package/dist/node/index-BVNhOeeU.d.ts.map +0 -1
  157. package/dist/node/index-C5KNEBO9.d.ts.map +0 -1
  158. package/dist/node/index-C9LWCL4l.d.ts.map +0 -1
  159. package/dist/node/index-COWvtBa2.d.ts.map +0 -1
  160. package/dist/node/index-TMAlNHuM.d.ts.map +0 -1
  161. package/dist/node/index-X2Zg8FEY.d.ts.map +0 -1
  162. package/dist/node/index-nBlMTFkZ.d.ts.map +0 -1
  163. package/dist/node/mcp-BAujHOMr.js.map +0 -1
  164. package/dist/node/mcp-Bl8jUfev.cjs +0 -1
  165. package/dist/node/tui-4hA-SMtS.js.map +0 -1
  166. package/dist/node/tui-CcLmEJ1r.cjs +0 -24
  167. package/dist/node/ws-BWel8nzl.js.map +0 -1
  168. package/dist/node/ws-tCjj2gPu.cjs +0 -1
  169. package/src/tui/InkTerminal.ts +0 -42
  170. package/src/tui/hooks/use-interactive-session-init.ts +0 -91
  171. package/src/tui/hooks/usePermissionQueue.ts +0 -52
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Provider-failure exit-code integration tests (CLI-064).
3
+ *
4
+ * Drives a real InteractiveSession with a provider whose chat() throws (the 401 class
5
+ * observed in product verification) and asserts the headless transport surfaces the
6
+ * failure: non-zero exit code and an error envelope/stderr message — never exit 0.
7
+ */
8
+
9
+ import { mkdtempSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ import { InteractiveSession } from '@robota-sdk/agent-framework';
14
+ import { afterEach, describe, expect, it } from 'vitest';
15
+
16
+ import { createHeadlessTransport } from '../headless-transport.js';
17
+
18
+ import type { TInteractiveSessionOptions } from '@robota-sdk/agent-framework';
19
+
20
+ type TStandardSessionOptions = Extract<
21
+ TInteractiveSessionOptions,
22
+ { cwd: string; provider: unknown }
23
+ >;
24
+ type TTestProvider = TStandardSessionOptions['provider'];
25
+ type TResolvedConfig = NonNullable<TStandardSessionOptions['config']>;
26
+
27
+ const AUTH_FAILURE_MESSAGE =
28
+ '401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}';
29
+
30
+ function createConfig(): TResolvedConfig {
31
+ return {
32
+ defaultTrustLevel: 'moderate',
33
+ language: 'en',
34
+ provider: {
35
+ name: 'failing-test-provider',
36
+ model: 'failing-test-model',
37
+ apiKey: 'test-key',
38
+ },
39
+ permissions: { allow: [], deny: [] },
40
+ env: {},
41
+ };
42
+ }
43
+
44
+ function createAuthFailingProvider(): TTestProvider {
45
+ return {
46
+ name: 'failing-test-provider',
47
+ version: '1.0.0',
48
+ async chat() {
49
+ throw new Error(AUTH_FAILURE_MESSAGE);
50
+ },
51
+ async generateResponse() {
52
+ return { content: 'unused' };
53
+ },
54
+ supportsTools() {
55
+ return true;
56
+ },
57
+ validateConfig() {
58
+ return true;
59
+ },
60
+ };
61
+ }
62
+
63
+ function captureStream(stream: NodeJS.WriteStream): { writes: string[]; restore(): void } {
64
+ const writes: string[] = [];
65
+ const originalWrite = stream.write;
66
+ stream.write = ((chunk: string | Uint8Array, encodingOrCallback?: unknown) => {
67
+ writes.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
68
+ if (typeof encodingOrCallback === 'function') {
69
+ encodingOrCallback();
70
+ }
71
+ return true;
72
+ }) as typeof stream.write;
73
+ return {
74
+ writes,
75
+ restore() {
76
+ stream.write = originalWrite;
77
+ },
78
+ };
79
+ }
80
+
81
+ describe('headless provider failure exit codes (CLI-064)', () => {
82
+ let cwd: string | undefined;
83
+
84
+ afterEach(() => {
85
+ if (cwd) rmSync(cwd, { recursive: true, force: true });
86
+ cwd = undefined;
87
+ });
88
+
89
+ it('TC-02: text format exits 1 and writes the auth failure to stderr', async () => {
90
+ cwd = mkdtempSync(join(tmpdir(), 'robota-headless-fail-'));
91
+ const session = new InteractiveSession({
92
+ cwd,
93
+ provider: createAuthFailingProvider(),
94
+ config: createConfig(),
95
+ permissionMode: 'bypassPermissions',
96
+ bare: true,
97
+ });
98
+ const stdout = captureStream(process.stdout);
99
+ const stderr = captureStream(process.stderr);
100
+
101
+ try {
102
+ const transport = createHeadlessTransport({ outputFormat: 'text', prompt: 'say hi' });
103
+ session.attachTransport(transport);
104
+ await transport.start();
105
+
106
+ expect(transport.getExitCode()).toBe(1);
107
+ expect(stderr.writes.join('')).toContain('authentication_error');
108
+ } finally {
109
+ stdout.restore();
110
+ stderr.restore();
111
+ }
112
+ });
113
+
114
+ it('TC-02: json format exits 1 with subtype error and error_code api_error', async () => {
115
+ cwd = mkdtempSync(join(tmpdir(), 'robota-headless-fail-'));
116
+ const session = new InteractiveSession({
117
+ cwd,
118
+ provider: createAuthFailingProvider(),
119
+ config: createConfig(),
120
+ permissionMode: 'bypassPermissions',
121
+ bare: true,
122
+ });
123
+ const stdout = captureStream(process.stdout);
124
+ const stderr = captureStream(process.stderr);
125
+
126
+ try {
127
+ const transport = createHeadlessTransport({ outputFormat: 'json', prompt: 'say hi' });
128
+ session.attachTransport(transport);
129
+ await transport.start();
130
+
131
+ expect(transport.getExitCode()).toBe(1);
132
+ const parsed: unknown = JSON.parse(stdout.writes.join('').trim());
133
+ expect(parsed).toMatchObject({
134
+ type: 'result',
135
+ subtype: 'error',
136
+ error_code: 'api_error',
137
+ });
138
+ } finally {
139
+ stdout.restore();
140
+ stderr.restore();
141
+ }
142
+ });
143
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
2
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
3
3
  import { createHeadlessRunner } from '../headless-runner.js';
4
4
 
5
5
  describe('createHeadlessRunner initialization', () => {
@@ -1,7 +1,9 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
3
- import type { IExecutionResult } from '@robota-sdk/agent-framework';
4
- import type { TBackgroundJobGroupEvent } from '@robota-sdk/agent-framework';
2
+ import type {
3
+ IExecutionResult,
4
+ IInteractiveSession,
5
+ TBackgroundJobGroupEvent,
6
+ } from '@robota-sdk/agent-interface-transport';
5
7
  import type { TBackgroundTaskEvent } from '@robota-sdk/agent-framework';
6
8
  import { createHeadlessRunner } from '../headless-runner.js';
7
9
 
@@ -96,6 +98,25 @@ describe('createHeadlessRunner (text format)', () => {
96
98
  expect(stdoutWriteSpy).not.toHaveBeenCalled();
97
99
  });
98
100
 
101
+ it('TC-02 (CLI-064): text format writes the error message to stderr on error', async () => {
102
+ const stderrWrites: string[] = [];
103
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: unknown) => {
104
+ stderrWrites.push(String(chunk));
105
+ return true;
106
+ }) as never);
107
+ try {
108
+ const session = createMockSession('error');
109
+ const runner = createHeadlessRunner({ session, outputFormat: 'text' });
110
+
111
+ const exitCode = await runner.run('test prompt');
112
+
113
+ expect(exitCode).toBe(1);
114
+ expect(stderrWrites.join('')).toContain('test error');
115
+ } finally {
116
+ stderrSpy.mockRestore();
117
+ }
118
+ });
119
+
99
120
  it('passes the prompt to session.submit', async () => {
100
121
  const session = createMockSession('complete', 'ok');
101
122
  const runner = createHeadlessRunner({ session, outputFormat: 'text' });
@@ -1,7 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { createHeadlessTransport } from '../headless-transport.js';
3
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
4
- import type { IExecutionResult } from '@robota-sdk/agent-framework';
3
+ import type { IExecutionResult, IInteractiveSession } from '@robota-sdk/agent-interface-transport';
5
4
 
6
5
  function createMockSession(): IInteractiveSession {
7
6
  return {
@@ -1,6 +1,6 @@
1
1
  import { executeSlashCommandIfPresent, subscribeStreamJsonEvents } from './headless-stream-json.js';
2
2
 
3
- import type { IInteractiveSession, IExecutionResult } from '@robota-sdk/agent-framework';
3
+ import type { IExecutionResult, IInteractiveSession } from '@robota-sdk/agent-interface-transport';
4
4
 
5
5
  export type TOutputFormat = 'text' | 'json' | 'stream-json';
6
6
 
@@ -78,8 +78,9 @@ function runTextFormat(session: IInteractiveSession, prompt: string): Promise<nu
78
78
  if (result.response) process.stdout.write(result.response + '\n');
79
79
  resolve(0);
80
80
  };
81
- const onError = (_error: Error): void => {
81
+ const onError = (error: Error): void => {
82
82
  cleanup();
83
+ process.stderr.write(error.message + '\n');
83
84
  resolve(1);
84
85
  };
85
86
 
@@ -1,12 +1,12 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
+ import type { TBackgroundTaskEvent } from '@robota-sdk/agent-framework';
3
4
  import type {
4
- IInteractiveSession,
5
- IExecutionResult,
6
5
  ICommandResult,
6
+ IExecutionResult,
7
+ IInteractiveSession,
7
8
  TBackgroundJobGroupEvent,
8
- TBackgroundTaskEvent,
9
- } from '@robota-sdk/agent-framework';
9
+ } from '@robota-sdk/agent-interface-transport';
10
10
 
11
11
  type TSlashCommandExecution =
12
12
  | { readonly kind: 'not-slash' }
@@ -65,7 +65,7 @@ interface IStreamJsonHandlers {
65
65
  onError: (error: Error) => void;
66
66
  }
67
67
 
68
- export function writeStreamJsonEvent(
68
+ function writeStreamJsonEvent(
69
69
  session: IInteractiveSession,
70
70
  getSessionId: (s: IInteractiveSession) => string,
71
71
  event: TStreamJsonEvent,
@@ -8,8 +8,7 @@
8
8
  import { createHeadlessRunner } from './headless-runner.js';
9
9
 
10
10
  import type { TOutputFormat } from './headless-runner.js';
11
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
12
- import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
11
+ import type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';
13
12
 
14
13
  export interface IHeadlessTransportOptions {
15
14
  /** Output format: 'text', 'json', or 'stream-json'. */
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import { createHttpTransport } from '../http-transport.js';
3
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
3
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
4
4
 
5
5
  function createMockSession(): IInteractiveSession {
6
6
  return {
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { describe, it, expect, vi } from 'vitest';
7
7
  import { createAgentRoutes } from '../routes.js';
8
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
8
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
9
9
 
10
10
  function createMockSession(overrides?: Record<string, unknown>) {
11
11
  return {
@@ -7,8 +7,7 @@
7
7
 
8
8
  import { createAgentRoutes } from './routes.js';
9
9
 
10
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
11
- import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
10
+ import type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';
12
11
  import type { Hono } from 'hono';
13
12
 
14
13
  export interface IHttpTransportOptions {
@@ -8,7 +8,7 @@
8
8
  import { Hono } from 'hono';
9
9
  import { streamSSE } from 'hono/streaming';
10
10
 
11
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
11
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
12
12
  import type { Context } from 'hono';
13
13
 
14
14
  /** Callback that resolves an IInteractiveSession from the request context. */
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { describe, it, expect, vi } from 'vitest';
6
6
  import { createAgentMcpServer } from '../mcp-server.js';
7
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
7
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
8
8
 
9
9
  function createMockSession(commands?: Array<{ name: string; description: string }>) {
10
10
  return {
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import { createMcpTransport } from '../mcp-transport.js';
3
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
3
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
4
4
 
5
5
  function createMockSession(): IInteractiveSession {
6
6
  return {
@@ -9,7 +9,7 @@
9
9
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
10
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
11
11
 
12
- import type { IInteractiveSession, IExecutionResult } from '@robota-sdk/agent-framework';
12
+ import type { IExecutionResult, IInteractiveSession } from '@robota-sdk/agent-interface-transport';
13
13
 
14
14
  export interface IAgentMcpOptions {
15
15
  /** Name for the MCP server. */
@@ -8,8 +8,7 @@
8
8
  import { createAgentMcpServer } from './mcp-server.js';
9
9
 
10
10
  import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
12
- import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
11
+ import type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';
13
12
 
14
13
  export interface IMcpTransportOptions {
15
14
  /** Name for the MCP server. */
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Scripted provider fixture tests (CLI-074 TC-01).
3
+ *
4
+ * The fixture must replay declared turns in order, record every request's
5
+ * message array for assertions, and fail fast on script exhaustion — never
6
+ * silently improvise a response.
7
+ */
8
+
9
+ import { describe, expect, it } from 'vitest';
10
+
11
+ import { createScriptedProvider } from '../scripted-provider.js';
12
+
13
+ describe('createScriptedProvider (CLI-074)', () => {
14
+ it('TC-01: replays text turns in order and records requests', async () => {
15
+ const { provider, requests } = createScriptedProvider([
16
+ { text: 'first answer' },
17
+ { text: 'second answer' },
18
+ ]);
19
+
20
+ const first = await provider.chat([
21
+ { id: 'u1', role: 'user', content: 'hi', state: 'complete', timestamp: new Date() },
22
+ ]);
23
+ const second = await provider.chat([
24
+ { id: 'u2', role: 'user', content: 'again', state: 'complete', timestamp: new Date() },
25
+ ]);
26
+
27
+ expect(first.role).toBe('assistant');
28
+ expect(first.content).toBe('first answer');
29
+ expect(second.content).toBe('second answer');
30
+ expect(requests).toHaveLength(2);
31
+ expect(requests[0]?.map((message) => message.content)).toContain('hi');
32
+ expect(requests[1]?.map((message) => message.content)).toContain('again');
33
+ });
34
+
35
+ it('TC-01: replays tool_use turns as assistant toolCalls', async () => {
36
+ const { provider } = createScriptedProvider([
37
+ { toolCalls: [{ name: 'Read', args: { file_path: '/tmp/a.txt' } }] },
38
+ { text: 'done reading' },
39
+ ]);
40
+
41
+ const turn = await provider.chat([
42
+ { id: 'u1', role: 'user', content: 'read it', state: 'complete', timestamp: new Date() },
43
+ ]);
44
+
45
+ expect(turn.role).toBe('assistant');
46
+ if (turn.role !== 'assistant') throw new Error('unreachable');
47
+ expect(turn.content).toBeNull();
48
+ expect(turn.toolCalls).toHaveLength(1);
49
+ expect(turn.toolCalls?.[0]?.function.name).toBe('Read');
50
+ expect(JSON.parse(turn.toolCalls?.[0]?.function.arguments ?? '{}')).toEqual({
51
+ file_path: '/tmp/a.txt',
52
+ });
53
+ });
54
+
55
+ it('TC-01: throws on script exhaustion instead of improvising', async () => {
56
+ const { provider } = createScriptedProvider([{ text: 'only turn' }]);
57
+
58
+ await provider.chat([
59
+ { id: 'u1', role: 'user', content: 'one', state: 'complete', timestamp: new Date() },
60
+ ]);
61
+
62
+ await expect(
63
+ provider.chat([
64
+ { id: 'u2', role: 'user', content: 'two', state: 'complete', timestamp: new Date() },
65
+ ]),
66
+ ).rejects.toThrow(/script exhausted/i);
67
+ });
68
+
69
+ it('supportsTools is true and validateConfig passes (agent-loop prerequisites)', () => {
70
+ const { provider } = createScriptedProvider([{ text: 'x' }]);
71
+ expect(provider.supportsTools()).toBe(true);
72
+ });
73
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Test-only fixtures (CLI-074). Exported via the `./testing` subpath; never import
3
+ * from runtime code.
4
+ */
5
+
6
+ export { createScriptedProvider } from './scripted-provider.js';
7
+ export type { IScriptedProvider, TScriptedTurn } from './scripted-provider.js';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Deterministic scripted provider for E2E tests (CLI-074).
3
+ *
4
+ * Replays a declared sequence of assistant turns through the REAL agent loop —
5
+ * tool execution, permission gate, session persistence, and output transports all
6
+ * run unmocked. Test-only: exported via the `@robota-sdk/agent-transport/testing`
7
+ * subpath and never imported by runtime code.
8
+ */
9
+
10
+ import type { IAIProvider, IRawProviderResponse, TUniversalMessage } from '@robota-sdk/agent-core';
11
+
12
+ /** One scripted assistant turn: plain text or tool invocations. */
13
+ export type TScriptedTurn =
14
+ | { text: string }
15
+ | { toolCalls: ReadonlyArray<{ name: string; args: Record<string, unknown> }> };
16
+
17
+ export interface IScriptedProvider {
18
+ provider: IAIProvider;
19
+ /** Message arrays of every chat() call, in order, for request assertions. */
20
+ requests: TUniversalMessage[][];
21
+ }
22
+
23
+ export function createScriptedProvider(turns: readonly TScriptedTurn[]): IScriptedProvider {
24
+ const requests: TUniversalMessage[][] = [];
25
+ let cursor = 0;
26
+
27
+ const provider: IAIProvider = {
28
+ name: 'scripted-test-provider',
29
+ version: 'test',
30
+ async chat(messages: TUniversalMessage[]): Promise<TUniversalMessage> {
31
+ requests.push([...messages]);
32
+ const turn = turns[cursor];
33
+ if (turn === undefined) {
34
+ throw new Error(
35
+ `Scripted provider: script exhausted at call ${cursor + 1} (script declares ${turns.length} turn(s)) — extend the script instead of relying on improvised responses`,
36
+ );
37
+ }
38
+ cursor += 1;
39
+ if ('text' in turn) {
40
+ return {
41
+ id: `scripted-${cursor}`,
42
+ role: 'assistant',
43
+ content: turn.text,
44
+ state: 'complete',
45
+ timestamp: new Date(),
46
+ };
47
+ }
48
+ return {
49
+ id: `scripted-${cursor}`,
50
+ role: 'assistant',
51
+ content: null,
52
+ state: 'complete',
53
+ timestamp: new Date(),
54
+ toolCalls: turn.toolCalls.map((call, index) => ({
55
+ id: `scripted-call-${cursor}-${index}`,
56
+ type: 'function' as const,
57
+ function: { name: call.name, arguments: JSON.stringify(call.args) },
58
+ })),
59
+ };
60
+ },
61
+ async generateResponse(): Promise<IRawProviderResponse> {
62
+ return { content: 'scripted provider does not implement raw responses' };
63
+ },
64
+ supportsTools(): boolean {
65
+ return true;
66
+ },
67
+ validateConfig(): boolean {
68
+ return true;
69
+ },
70
+ };
71
+
72
+ return { provider, requests };
73
+ }
@@ -11,9 +11,9 @@ import { readSettings, writeSettings, type TSettingsData } from '@robota-sdk/age
11
11
  import { WsTransport } from './ws/index.js';
12
12
 
13
13
  import type { TUniversalValue } from '@robota-sdk/agent-core';
14
- import type { IInteractiveSession } from '@robota-sdk/agent-framework';
15
14
  import type {
16
15
  IConfigurableTransport,
16
+ IInteractiveSession,
17
17
  ITransportConfig,
18
18
  ITransportEntry,
19
19
  } from '@robota-sdk/agent-interface-transport';
package/src/tui/App.tsx CHANGED
@@ -31,16 +31,19 @@ import type { ITuiCliAdapter } from './tui-cli-adapter.js';
31
31
  import type { TuiInteractionChannel } from './TuiInteractionChannel.js';
32
32
  import type { TPermissionMode } from '@robota-sdk/agent-core';
33
33
  import type {
34
+ IExecutionDetailPage,
34
35
  IInteractiveSession,
35
36
  IInteractiveSessionStore,
36
- IExecutionDetailPage,
37
- } from '@robota-sdk/agent-framework';
38
- import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
37
+ ITransportRegistryView,
38
+ } from '@robota-sdk/agent-interface-transport';
39
39
 
40
40
  interface IProps {
41
41
  cwd: string;
42
- channel: TuiInteractionChannel;
43
- createChannel?: (resumeSessionId?: string) => TuiInteractionChannel;
42
+ /**
43
+ * Sole channel source (CLI-B12): App owns the channel lifecycle in React state.
44
+ * The initial channel and every session-switch replacement come from this factory.
45
+ */
46
+ createChannel: (resumeSessionId?: string) => TuiInteractionChannel;
44
47
  providerOverride?: string | undefined;
45
48
  providerType?: string | undefined;
46
49
  modelId?: string;
@@ -55,10 +58,15 @@ interface IProps {
55
58
  }
56
59
 
57
60
  export default function App(props: IProps): React.ReactElement {
61
+ // Lazy initializer: channel construction is side-effect-free (object wiring only);
62
+ // I/O starts in AppInner's effect via channel.start(). Runs once per mount.
58
63
  const [sessionState, setSessionState] = useState<{
59
64
  channel: TuiInteractionChannel;
60
65
  sessionId: string | undefined;
61
- }>({ channel: props.channel, sessionId: props.resumeSessionId });
66
+ }>(() => ({
67
+ channel: props.createChannel(props.resumeSessionId),
68
+ sessionId: props.resumeSessionId,
69
+ }));
62
70
  const [showInitialSessionPicker, setShowInitialSessionPicker] = useState(
63
71
  props.showSessionPickerOnStart ?? false,
64
72
  );
@@ -73,10 +81,10 @@ export default function App(props: IProps): React.ReactElement {
73
81
  resumeSessionId={sessionState.sessionId}
74
82
  onSessionSwitch={(sessionId) => {
75
83
  setShowInitialSessionPicker(false);
76
- const oldChannel = sessionState.channel;
77
- const newChannel = props.createChannel ? props.createChannel(sessionId) : props.channel;
78
- setSessionState({ channel: newChannel, sessionId });
79
- void oldChannel.stop();
84
+ // Stop the old channel BEFORE the new one becomes active so it can
85
+ // never receive events addressed to the new session (CLI-B12).
86
+ void sessionState.channel.stop();
87
+ setSessionState({ channel: props.createChannel(sessionId), sessionId });
80
88
  }}
81
89
  />
82
90
  </TuiCliAdapterProvider>
@@ -84,7 +92,10 @@ export default function App(props: IProps): React.ReactElement {
84
92
  }
85
93
 
86
94
  function AppInner(
87
- props: IProps & { onSessionSwitch: (sessionId: string) => void },
95
+ props: IProps & {
96
+ channel: TuiInteractionChannel;
97
+ onSessionSwitch: (sessionId: string) => void;
98
+ },
88
99
  ): React.ReactElement {
89
100
  const cwd = props.cwd;
90
101
  const { channel } = props;
@@ -436,22 +447,6 @@ function AppInner(
436
447
  />
437
448
  )}
438
449
  <ContextWarningBanner percentage={contextState.percentage} />
439
- <SessionStatusBar
440
- cwd={cwd}
441
- permissionMode={permissionMode}
442
- modelId={props.modelId}
443
- providerType={props.providerType}
444
- sessionId={sessionId}
445
- isThinking={isThinking}
446
- activeToolCount={activeTools.length}
447
- activeBackgroundTaskCount={activeBackgroundTaskCount}
448
- hasPendingPrompt={pendingPrompt !== null}
449
- contextState={contextState}
450
- sessionName={sessionName}
451
- settings={statusLineSettings}
452
- activeAgentLabel={activeAgentLabel}
453
- gitRefreshToken={gitRefreshToken}
454
- />
455
450
  <InputArea
456
451
  onSubmit={handleSubmitWithGitRefresh}
457
452
  onCancelQueue={handleCancelQueue}
@@ -472,8 +467,22 @@ function AppInner(
472
467
  sessionName={sessionName}
473
468
  history={history}
474
469
  />
475
- {/* Blank line for Korean IME — normal flow ensures it persists across remounts. */}
476
- <Text> </Text>
470
+ <SessionStatusBar
471
+ cwd={cwd}
472
+ permissionMode={permissionMode}
473
+ modelId={props.modelId}
474
+ providerType={props.providerType}
475
+ sessionId={sessionId}
476
+ isThinking={isThinking}
477
+ activeToolCount={activeTools.length}
478
+ activeBackgroundTaskCount={activeBackgroundTaskCount}
479
+ hasPendingPrompt={pendingPrompt !== null}
480
+ contextState={contextState}
481
+ sessionName={sessionName}
482
+ settings={statusLineSettings}
483
+ activeAgentLabel={activeAgentLabel}
484
+ gitRefreshToken={gitRefreshToken}
485
+ />
477
486
  </Box>
478
487
  );
479
488
  }
@@ -3,7 +3,7 @@ import React from 'react';
3
3
 
4
4
  import { formatBackgroundTaskRow } from './background-task-row-format.js';
5
5
 
6
- import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-framework';
6
+ import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-transport';
7
7
 
8
8
  interface IProps {
9
9
  entries: IExecutionWorkspaceEntry[];
@@ -79,14 +79,10 @@ export default function CjkTextInput({
79
79
  forceRender,
80
80
  });
81
81
 
82
- // Do NOT call setCursorPosition() passing y:0 moves the real terminal cursor
83
- // to the top of the entire ink output (logo area), which causes Terminal.app to
84
- // SIGSEGV when Korean IME queries attributedSubstringFromRange: at that position.
85
- // Without setCursorPosition, the IME candidate window appears at bottom-left
86
- // (same behavior as Claude Code, issue #19207), but Terminal.app does not crash.
87
- //
88
- // A correct fix would require knowing the total rendered height to pass the right
89
- // y coordinate, which ink does not expose to components.
82
+ // Real terminal cursor positioning is intentionally omitted.
83
+ // setCursorPosition(x, 0) crashes Terminal.app via Korean IME SIGSEGV.
84
+ // Correct fix requires the input row's y offset from the bottom of the render,
85
+ // which Ink does not expose. Tracked as a known limitation.
90
86
 
91
87
  return (
92
88
  <Text>
@@ -10,7 +10,7 @@ import type {
10
10
  IExecutionDetailPage,
11
11
  IExecutionWorkspaceEntry,
12
12
  TExecutionDetailRecordKind,
13
- } from '@robota-sdk/agent-framework';
13
+ } from '@robota-sdk/agent-interface-transport';
14
14
 
15
15
  const MAX_VISIBLE_DETAIL_RECORDS = 12;
16
16