|
1 | 1 | import { test, expect, describe, afterEach } from "bun:test"; |
2 | | -import type { |
3 | | - AgentRuntime, |
4 | | - AgentRuntimeEndpoint, |
5 | | - GetAgentRuntimeEndpointResponse, |
6 | | - GetAgentRuntimeResponse, |
7 | | -} from "@aws-sdk/client-bedrock-agentcore-control"; |
8 | | -import { createRootHandler } from "../handlers"; |
9 | | -import { DebugKey, EndpointKey, JsonKey, RegionKey } from "../handlers/keys"; |
10 | | -import { CommandKey, compile, type Context, ValueContext } from "../router"; |
11 | | -import { |
12 | | - cleanupScreens, |
13 | | - createSilentLogger, |
14 | | - renderScreen, |
15 | | - TestCoreClient, |
16 | | - testIO, |
17 | | - tick, |
18 | | - waitFor, |
19 | | - waitForText, |
20 | | -} from "../testing"; |
21 | | -import { JsonRendererKey, renderTuiAt } from "../tui"; |
| 2 | +import { cleanupScreens, renderScreen, tick, waitForText } from "../testing"; |
22 | 3 |
|
23 | 4 | afterEach(cleanupScreens); |
24 | 5 |
|
25 | | -const runtimeEndpointUrl = "https://runtime.test"; |
26 | | - |
27 | | -function runtime(overrides: Partial<AgentRuntime> = {}): AgentRuntime { |
28 | | - return { |
29 | | - agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123", |
30 | | - agentRuntimeId: "runtime-123", |
31 | | - agentRuntimeVersion: "7", |
32 | | - agentRuntimeName: "checkout", |
33 | | - description: "Checkout Runtime", |
34 | | - lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"), |
35 | | - status: "READY", |
36 | | - ...overrides, |
37 | | - }; |
38 | | -} |
39 | | - |
40 | | -function getRuntimeResponse( |
41 | | - overrides: Partial<GetAgentRuntimeResponse> = {}, |
42 | | -): GetAgentRuntimeResponse { |
43 | | - return { |
44 | | - agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123", |
45 | | - agentRuntimeName: "checkout", |
46 | | - agentRuntimeId: "runtime-123", |
47 | | - agentRuntimeVersion: "7", |
48 | | - createdAt: new Date("2026-07-19T01:02:03.000Z"), |
49 | | - lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"), |
50 | | - roleArn: "arn:aws:iam::123456789012:role/runtime-role", |
51 | | - networkConfiguration: { networkMode: "PUBLIC" }, |
52 | | - status: "READY", |
53 | | - lifecycleConfiguration: { |
54 | | - idleRuntimeSessionTimeout: 900, |
55 | | - maxLifetime: 28_800, |
56 | | - }, |
57 | | - ...overrides, |
58 | | - }; |
59 | | -} |
60 | | - |
61 | | -function endpoint(overrides: Partial<AgentRuntimeEndpoint> = {}): AgentRuntimeEndpoint { |
62 | | - return { |
63 | | - name: "prod", |
64 | | - liveVersion: "7", |
65 | | - agentRuntimeEndpointArn: |
66 | | - "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123/endpoint/prod", |
67 | | - agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123", |
68 | | - status: "READY", |
69 | | - id: "prod", |
70 | | - createdAt: new Date("2026-07-19T01:02:03.000Z"), |
71 | | - lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"), |
72 | | - ...overrides, |
73 | | - }; |
74 | | -} |
75 | | - |
76 | | -function getEndpointResponse( |
77 | | - overrides: Partial<GetAgentRuntimeEndpointResponse> = {}, |
78 | | -): GetAgentRuntimeEndpointResponse { |
79 | | - return { |
80 | | - liveVersion: "7", |
81 | | - targetVersion: "8", |
82 | | - agentRuntimeEndpointArn: |
83 | | - "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123/endpoint/prod", |
84 | | - agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123", |
85 | | - status: "READY", |
86 | | - createdAt: new Date("2026-07-19T01:02:03.000Z"), |
87 | | - lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"), |
88 | | - name: "prod", |
89 | | - id: "prod", |
90 | | - ...overrides, |
91 | | - }; |
92 | | -} |
93 | | - |
94 | | -function runtimeCore(): TestCoreClient { |
95 | | - const core = new TestCoreClient(); |
96 | | - core.runtime.setListResponse({ agentRuntimes: [runtime()] }); |
97 | | - core.runtime.setGetResponse(getRuntimeResponse()); |
98 | | - core.runtime.setListEndpointsResponse({ runtimeEndpoints: [endpoint()] }); |
99 | | - core.runtime.setGetEndpointResponse(getEndpointResponse()); |
100 | | - return core; |
101 | | -} |
102 | | - |
103 | | -function runtimeContext(core: TestCoreClient): Context { |
104 | | - const rootCommand = compile( |
105 | | - createRootHandler(core, { io: testIO().io, logger: createSilentLogger() }), |
106 | | - ValueContext.EmptyContext(), |
107 | | - ); |
108 | | - |
109 | | - return ValueContext.EmptyContext() |
110 | | - .withValue(CommandKey, rootCommand) |
111 | | - .withValue(RegionKey, "us-east-1") |
112 | | - .withValue(EndpointKey, runtimeEndpointUrl) |
113 | | - .withValue(JsonKey, false) |
114 | | - .withValue(DebugKey, false) |
115 | | - .withValue(JsonRendererKey, { renderJson: () => {} }); |
116 | | -} |
117 | | - |
118 | | -function expectEndpointPropagation(core: TestCoreClient, methods: readonly string[]): void { |
119 | | - for (const method of methods) { |
120 | | - const calls = core.runtime.calls.filter((call) => call.method === method); |
121 | | - expect(calls.length).toBeGreaterThan(0); |
122 | | - for (const call of calls) { |
123 | | - expect(call.args.at(-1)).toEqual({ |
124 | | - region: "us-east-1", |
125 | | - endpointUrl: runtimeEndpointUrl, |
126 | | - }); |
127 | | - } |
128 | | - } |
129 | | -} |
130 | | - |
131 | | -interface TtyInput extends NodeJS.ReadStream { |
132 | | - write(chunk: string): boolean; |
133 | | -} |
134 | | - |
135 | | -function ttyTestIO(): { streams: ReturnType<typeof testIO>; stdin: TtyInput } { |
136 | | - const streams = testIO({ isTTY: true }); |
137 | | - const stdin = streams.io.stdin as TtyInput; |
138 | | - stdin.setRawMode = function () { |
139 | | - return this; |
140 | | - }; |
141 | | - stdin.ref = function () { |
142 | | - return this; |
143 | | - }; |
144 | | - stdin.unref = function () { |
145 | | - return this; |
146 | | - }; |
147 | | - Object.defineProperties(streams.io.stdout, { |
148 | | - columns: { configurable: true, value: 100 }, |
149 | | - rows: { configurable: true, value: 40 }, |
150 | | - }); |
151 | | - return { streams, stdin }; |
152 | | -} |
153 | | - |
154 | 6 | // RouterScreen is the interactive command menu. These tests mount it through the |
155 | 7 | // real Root at a command path and drive it with key presses, asserting on the |
156 | 8 | // rendered frames — behavior a user would see, not internal state. |
@@ -282,90 +134,4 @@ describe("navigation", () => { |
282 | 134 | expect(r.lastFrame()).toContain("❯ harness"); |
283 | 135 | r.unmount(); |
284 | 136 | }); |
285 | | - |
286 | | - test("navigates from Root through Runtime endpoints and escapes each boundary", async () => { |
287 | | - const core = runtimeCore(); |
288 | | - const r = renderScreen("/agentcore", { |
289 | | - core, |
290 | | - ctx: runtimeContext(core), |
291 | | - }); |
292 | | - |
293 | | - await waitForText(r.lastFrame, "❯ harness"); |
294 | | - await r.press("down"); |
295 | | - await r.press("return"); |
296 | | - await waitForText(r.lastFrame, "agentcore → runtime → inspect AgentCore Runtimes"); |
297 | | - |
298 | | - await r.press("down"); |
299 | | - await r.press("return"); |
300 | | - await waitForText(r.lastFrame, "agentcore → runtime → list"); |
301 | | - await waitForText(r.lastFrame, "checkout"); |
302 | | - |
303 | | - await r.press("return"); |
304 | | - await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123"); |
305 | | - await waitForText(r.lastFrame, "show the full JSON definition"); |
306 | | - |
307 | | - await r.press("down"); |
308 | | - await r.press("down"); |
309 | | - await r.press("return"); |
310 | | - await waitForText(r.lastFrame, "agentcore → runtime → endpoint → list → runtime-123"); |
311 | | - await waitForText(r.lastFrame, "prod"); |
312 | | - |
313 | | - await r.press("return"); |
314 | | - await waitForText(r.lastFrame, "agentcore → runtime → endpoint → get → runtime-123 → prod"); |
315 | | - await waitForText(r.lastFrame, '"agentRuntimeEndpointArn"'); |
316 | | - |
317 | | - await r.press("escape"); |
318 | | - await waitFor(() => { |
319 | | - const frame = r.lastFrame() ?? ""; |
320 | | - return ( |
321 | | - frame.includes("agentcore → runtime → endpoint → list → runtime-123") && |
322 | | - !frame.includes("agentcore → runtime → endpoint → get") |
323 | | - ); |
324 | | - }); |
325 | | - await r.press("escape"); |
326 | | - await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123"); |
327 | | - await r.press("escape"); |
328 | | - await waitForText(r.lastFrame, "agentcore → runtime → list"); |
329 | | - await r.press("escape"); |
330 | | - await waitForText(r.lastFrame, "agentcore → runtime → inspect AgentCore Runtimes"); |
331 | | - await r.press("escape"); |
332 | | - await waitForText(r.lastFrame, "the platform for production AI agents"); |
333 | | - |
334 | | - expectEndpointPropagation(core, [ |
335 | | - "listRuntimes", |
336 | | - "getRuntime", |
337 | | - "listRuntimeEndpoints", |
338 | | - "getRuntimeEndpoint", |
339 | | - ]); |
340 | | - }); |
341 | | -}); |
342 | | - |
343 | | -describe("Runtime TUI exit", () => { |
344 | | - test("Ctrl+C exits a production Runtime list and ignores input after exit", async () => { |
345 | | - const core = new TestCoreClient(); |
346 | | - core.runtime.setListResponse({ |
347 | | - agentRuntimes: [runtime()], |
348 | | - nextToken: "page-2", |
349 | | - }); |
350 | | - const { streams, stdin } = ttyTestIO(); |
351 | | - const renderPromise = renderTuiAt( |
352 | | - "/agentcore/runtime/list", |
353 | | - runtimeContext(core), |
354 | | - core, |
355 | | - streams.io, |
356 | | - ); |
357 | | - const listCalls = () => core.runtime.calls.filter((call) => call.method === "listRuntimes"); |
358 | | - |
359 | | - await waitFor(() => listCalls().length > 0); |
360 | | - await tick(); |
361 | | - const callsBeforeExit = listCalls().length; |
362 | | - |
363 | | - stdin.write(String.fromCharCode(3)); |
364 | | - await expect(renderPromise).resolves.toBeUndefined(); |
365 | | - |
366 | | - stdin.write("l"); |
367 | | - await tick(); |
368 | | - expect(listCalls()).toHaveLength(callsBeforeExit); |
369 | | - expect(listCalls().some((call) => call.args[0] === "page-2")).toBe(false); |
370 | | - }); |
371 | 137 | }); |
0 commit comments