| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- 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<void> {
- 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 = (
- <TuiApp
- gateway={gateway}
- connection="in_process"
- projectKey={cwd}
- cwd={cwd}
- model={`${PROVIDER} · ${MODEL}`}
- />
- );
- 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<typeof createGateway>): ReturnType<typeof createGateway> {
- 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<typeof gateway.submitTurn>) => {
- const startedAt = Date.now();
- let lastAt = startedAt;
- let textChars = 0;
- const iterable = original.apply(target, args) as AsyncIterable<unknown>;
- 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<void> {
- return new Promise((resolveTimer) => setTimeout(resolveTimer, ms));
- }
- async function waitForFrame(
- instance: ReturnType<typeof render>,
- pattern: RegExp,
- timeoutMs: number,
- ): Promise<string | undefined> {
- 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<typeof render>,
- timeoutMs: number,
- ): Promise<string | undefined> {
- 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<typeof render>,
- timeoutMs: number,
- stableMs = 600,
- ): Promise<string | undefined> {
- 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;
- });
|