import React from "react"; import { render } from "ink-testing-library"; import { writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { TuiApp } from "../src/adapters/channel/tui/app/TuiApp.js"; import { createGateway } from "../src/gateway/index.js"; import { createModelRuntime } from "../src/model/index.js"; import { createDefaultPermissionContext, PermissionRuntime } from "../src/permission/index.js"; import { loadPilotConfig } from "../src/pilot/index.js"; import { createRouterRuntime } from "../src/router/index.js"; import { SequentialToolScheduler, ToolRegistry, ToolRuntime, type PilotDeckToolDefinition, } from "../src/tool/index.js"; import type { AgentRuntimeConfig } from "../src/agent/index.js"; import { createAgentSession } from "../src/agent/index.js"; const PROVIDER = process.env.PILOTDECK_E2E_PROVIDER ?? "edgeclaw"; const MODEL = process.env.PILOTDECK_E2E_MODEL ?? "moonshotai/kimi-k2.6"; const PROMPT = process.env.PILOTDECK_E2E_PROMPT ?? "Use add_numbers to compute 17 + 25, then tell me the result."; const addNumbersTool: PilotDeckToolDefinition = { name: "add_numbers", description: "Add two numbers and return the result.", kind: "custom", inputSchema: { type: "object", required: ["a", "b"], additionalProperties: false, properties: { a: { type: "number" }, b: { type: "number" }, }, }, isReadOnly: () => true, isConcurrencySafe: () => true, execute: async (input) => { const { a, b } = input as { a: number; b: number }; return { content: [{ type: "text", text: String(a + b) }], data: { sum: a + b } }; }, }; async function main(): Promise { const snapshot = loadPilotConfig(); const provider = snapshot.config.model.providers[PROVIDER]; if (!provider?.models[MODEL]) { throw new Error(`Provider ${PROVIDER} or model ${MODEL} is not configured.`); } const cwd = process.cwd(); const registry = new ToolRegistry(); registry.register(addNumbersTool); const permissionRuntime = new PermissionRuntime(); const toolRuntime = new ToolRuntime(registry, permissionRuntime); const scheduler = new SequentialToolScheduler(toolRuntime); const modelRuntime = createModelRuntime(snapshot.config.model); const config: AgentRuntimeConfig = { provider: PROVIDER, model: MODEL, cwd, systemPrompt: "You are PilotDeck running an end-to-end TUI test. When asked for arithmetic, you MUST call the provided add_numbers tool exactly once instead of computing it yourself, then report the answer in plain text.", maxOutputTokens: 1024, temperature: 0, permissionMode: "default", permissionContext: createDefaultPermissionContext({ cwd, mode: "default", canPrompt: false, bypassAvailable: true, }), metadata: { test: "tui-e2e-record" }, }; const router = createRouterRuntime( snapshot.config.router ?? { scenarios: { default: { id: `${PROVIDER}/${MODEL}`, provider: PROVIDER, model: MODEL }, }, zeroUsageRetry: { enabled: true, maxAttempts: 5 }, }, { modelRuntime }, ); const baseGateway = createGateway({ session: { create: async ({ sessionKey }) => createAgentSession({ sessionId: sessionKey, config, dependencies: { router, tools: { registry, scheduler }, }, }), }, serverInfo: { mode: "in_process", projectKey: cwd }, }); const gateway = process.env.PILOTDECK_E2E_TRACE === "1" ? wrapWithTrace(baseGateway) : baseGateway; const tree = ( ); const instance = render(tree); const writeFrame = (label: string) => { process.stdout.write(`\n--- ${label} (frame #${instance.frames.length}) ---\n`); process.stdout.write(`${instance.lastFrame() ?? ""}\n`); }; await wait(120); writeFrame("cold start"); for (const ch of PROMPT) { instance.stdin.write(ch); await wait(8); } await wait(120); writeFrame("after typing prompt"); instance.stdin.write("\r"); writeFrame("submit"); const finalFrame = await waitForCompletedFrame(instance, 120_000); writeFrame("final"); const logPath = resolve(process.cwd(), "artifacts/tui-e2e-frames.log"); writeFileSync(logPath, instance.frames.map((frame, index) => `--- frame ${index} ---\n${frame}\n`).join("\n")); process.stdout.write(`\nSaved ${instance.frames.length} frames to ${logPath}\n`); instance.unmount(); if (!finalFrame) { throw new Error("Timed out waiting for the assistant final frame."); } } function wrapWithTrace(gateway: ReturnType): ReturnType { return new Proxy(gateway, { get(target, prop, receiver) { const original = Reflect.get(target, prop, receiver); if (prop !== "submitTurn" || typeof original !== "function") { return original; } return (...args: Parameters) => { const startedAt = Date.now(); let lastAt = startedAt; let textChars = 0; const iterable = original.apply(target, args) as AsyncIterable; const ms = (now: number) => `${(now - startedAt).toString().padStart(5)} ms`; process.stdout.write(`\n[trace] submitTurn() called\n`); return (async function* () { for await (const event of iterable) { const now = Date.now(); const delta = now - lastAt; lastAt = now; const ev = event as { type: string; text?: string; name?: string; toolCallId?: string }; const type = ev.type; let detail = ""; if (type === "assistant_text_delta") { textChars += ev.text?.length ?? 0; detail = ` text+=${ev.text?.length ?? 0} total=${textChars}`; } else if (type === "tool_call_started" || type === "tool_call_finished") { detail = ` ${ev.name ?? ev.toolCallId ?? ""}`; } else if (type === "error") { detail = ` "${(event as { message?: string }).message ?? ""}"`; } process.stdout.write(`[trace ${ms(now)} +${delta.toString().padStart(4)}ms] ${type}${detail}\n`); yield event; } const total = Date.now() - startedAt; process.stdout.write(`[trace] turn finished after ${total} ms (${textChars} assistant chars)\n`); })(); }; }, }); } function wait(ms: number): Promise { return new Promise((resolveTimer) => setTimeout(resolveTimer, ms)); } async function waitForFrame( instance: ReturnType, pattern: RegExp, timeoutMs: number, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const frame = instance.lastFrame(); if (frame && pattern.test(frame)) { return frame; } await wait(120); } return undefined; } async function waitForCompletedFrame( instance: ReturnType, timeoutMs: number, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const frame = instance.lastFrame() ?? ""; const hasResult = /\b42\b/.test(frame); const stillThinking = /✦ thinking/.test(frame); if (hasResult && !stillThinking) { return frame; } await wait(150); } return undefined; } async function waitForStableFrame( instance: ReturnType, timeoutMs: number, stableMs = 600, ): Promise { const deadline = Date.now() + timeoutMs; let last = instance.lastFrame(); let lastChange = Date.now(); while (Date.now() < deadline) { const current = instance.lastFrame(); if (current !== last) { last = current; lastChange = Date.now(); } else if (Date.now() - lastChange >= stableMs) { return current; } await wait(120); } return last; } main().catch((error) => { console.error(error); process.exitCode = 1; });