mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
Rewrite RPC mode with typed protocol and client
- Move RPC files to modes/rpc/ directory - Add properly typed RpcCommand and RpcResponse types - Expose full AgentSession API via RPC commands: - State: get_state - Model: set_model, cycle_model, get_available_models - Thinking: set_thinking_level, cycle_thinking_level - Queue: set_queue_mode - Compaction: compact, set_auto_compaction - Bash: bash, abort_bash - Session: get_session_stats, export_html, switch_session, branch, etc. - Add RpcClient class for programmatic access - Rewrite tests to use RpcClient instead of raw process spawning - All commands support optional correlation ID for request/response matching
This commit is contained in:
parent
b2e1054e5e
commit
3559a43ba0
7 changed files with 1039 additions and 401 deletions
|
|
@ -1,130 +1,78 @@
|
|||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AgentEvent } from "@mariozechner/pi-agent-core";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import type { BashExecutionMessage } from "../src/core/messages.js";
|
||||
import type { CompactionEntry } from "../src/core/session-manager.js";
|
||||
import { RpcClient } from "../src/modes/rpc/rpc-client.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* RPC mode tests.
|
||||
* Regression test for issue #83: https://github.com/badlogic/pi-mono/issues/83
|
||||
*/
|
||||
describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_TOKEN)("RPC mode", () => {
|
||||
let agent: ChildProcess;
|
||||
let client: RpcClient;
|
||||
let sessionDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a unique temp directory for sessions
|
||||
sessionDir = join(tmpdir(), `pi-rpc-test-${Date.now()}`);
|
||||
client = new RpcClient({
|
||||
cliPath: join(__dirname, "..", "dist", "cli.js"),
|
||||
cwd: join(__dirname, ".."),
|
||||
env: { PI_CODING_AGENT_DIR: sessionDir },
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-5",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Kill the agent if still running
|
||||
if (agent && !agent.killed) {
|
||||
agent.kill("SIGKILL");
|
||||
}
|
||||
// Clean up session directory
|
||||
afterEach(async () => {
|
||||
await client.stop();
|
||||
if (sessionDir && existsSync(sessionDir)) {
|
||||
rmSync(sessionDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("should get state", async () => {
|
||||
await client.start();
|
||||
const state = await client.getState();
|
||||
|
||||
expect(state.model).toBeDefined();
|
||||
expect(state.model?.provider).toBe("anthropic");
|
||||
expect(state.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(state.isStreaming).toBe(false);
|
||||
expect(state.messageCount).toBe(0);
|
||||
}, 30000);
|
||||
|
||||
test("should save messages to session file", async () => {
|
||||
// Spawn agent in RPC mode with custom session directory
|
||||
agent = spawn(
|
||||
"node",
|
||||
["dist/cli.js", "--mode", "rpc", "--provider", "anthropic", "--model", "claude-sonnet-4-5"],
|
||||
{
|
||||
cwd: join(__dirname, ".."),
|
||||
env: {
|
||||
...process.env,
|
||||
PI_CODING_AGENT_DIR: sessionDir,
|
||||
},
|
||||
},
|
||||
);
|
||||
await client.start();
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
// Send prompt and wait for completion
|
||||
const events = await client.promptAndWait("Reply with just the word 'hello'");
|
||||
|
||||
// Parse agent events
|
||||
const rl = readline.createInterface({ input: agent.stdout!, terminal: false });
|
||||
|
||||
// Collect stderr for debugging
|
||||
let stderr = "";
|
||||
agent.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// Wait for agent_end which signals the full prompt/response cycle is complete
|
||||
const waitForAgentEnd = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error("Timeout waiting for agent_end")), 60000);
|
||||
|
||||
rl.on("line", (line: string) => {
|
||||
try {
|
||||
const event = JSON.parse(line) as AgentEvent;
|
||||
events.push(event);
|
||||
|
||||
// agent_end means the full prompt cycle completed (user msg + assistant response)
|
||||
if (event.type === "agent_end") {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON lines
|
||||
}
|
||||
});
|
||||
|
||||
rl.on("close", () => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error("Agent stdout closed before agent_end"));
|
||||
});
|
||||
});
|
||||
|
||||
// Send a simple prompt - the LLM will respond
|
||||
agent.stdin!.write(JSON.stringify({ type: "prompt", message: "Reply with just the word 'hello'" }) + "\n");
|
||||
|
||||
// Wait for full prompt/response cycle to complete
|
||||
await waitForAgentEnd;
|
||||
|
||||
// Check that message_end events were emitted
|
||||
// Should have message events
|
||||
const messageEndEvents = events.filter((e) => e.type === "message_end");
|
||||
expect(messageEndEvents.length).toBeGreaterThanOrEqual(2); // user + assistant
|
||||
|
||||
// Wait a bit for file writes to complete
|
||||
// Wait for file writes
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// Kill the agent gracefully
|
||||
agent.kill("SIGTERM");
|
||||
|
||||
// Find and verify the session file
|
||||
// Verify session file
|
||||
const sessionsPath = join(sessionDir, "sessions");
|
||||
expect(existsSync(sessionsPath), `Sessions path should exist: ${sessionsPath}. Stderr: ${stderr}`).toBe(true);
|
||||
expect(existsSync(sessionsPath)).toBe(true);
|
||||
|
||||
// Find the session directory (it's based on cwd)
|
||||
const sessionDirs = readdirSync(sessionsPath);
|
||||
expect(sessionDirs.length, `Should have at least one session dir. Stderr: ${stderr}`).toBeGreaterThan(0);
|
||||
expect(sessionDirs.length).toBeGreaterThan(0);
|
||||
|
||||
const cwdSessionDir = join(sessionsPath, sessionDirs[0]);
|
||||
const allFiles = readdirSync(cwdSessionDir);
|
||||
const sessionFiles = allFiles.filter((f) => f.endsWith(".jsonl"));
|
||||
expect(
|
||||
sessionFiles.length,
|
||||
`Should have exactly one session file. Dir: ${cwdSessionDir}, Files: ${JSON.stringify(allFiles)}, Stderr: ${stderr}`,
|
||||
).toBe(1);
|
||||
const sessionFiles = readdirSync(cwdSessionDir).filter((f) => f.endsWith(".jsonl"));
|
||||
expect(sessionFiles.length).toBe(1);
|
||||
|
||||
// Read and verify session content
|
||||
const sessionContent = readFileSync(join(cwdSessionDir, sessionFiles[0]), "utf8");
|
||||
const lines = sessionContent.trim().split("\n");
|
||||
|
||||
// Should have session header and at least 2 messages (user + assistant)
|
||||
expect(lines.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const entries = lines.map((line) => JSON.parse(line));
|
||||
const entries = sessionContent
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
// First entry should be session header
|
||||
expect(entries[0].type).toBe("session");
|
||||
|
|
@ -139,83 +87,20 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
|
|||
}, 90000);
|
||||
|
||||
test("should handle manual compaction", async () => {
|
||||
// Spawn agent in RPC mode
|
||||
agent = spawn(
|
||||
"node",
|
||||
["dist/cli.js", "--mode", "rpc", "--provider", "anthropic", "--model", "claude-sonnet-4-5"],
|
||||
{
|
||||
cwd: join(__dirname, ".."),
|
||||
env: {
|
||||
...process.env,
|
||||
PI_CODING_AGENT_DIR: sessionDir,
|
||||
},
|
||||
},
|
||||
);
|
||||
await client.start();
|
||||
|
||||
const events: (AgentEvent | CompactionEntry | { type: "error"; error: string })[] = [];
|
||||
// First send a prompt to have messages to compact
|
||||
await client.promptAndWait("Say hello");
|
||||
|
||||
const rl = readline.createInterface({ input: agent.stdout!, terminal: false });
|
||||
|
||||
let stderr = "";
|
||||
agent.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// Helper to wait for a specific event type
|
||||
const waitForEvent = (eventType: string, timeout = 60000) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`Timeout waiting for ${eventType}`)), timeout);
|
||||
|
||||
const checkExisting = () => {
|
||||
if (events.some((e) => e.type === eventType)) {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (checkExisting()) return;
|
||||
|
||||
const handler = (line: string) => {
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
events.push(event);
|
||||
if (event.type === eventType) {
|
||||
clearTimeout(timer);
|
||||
rl.off("line", handler);
|
||||
resolve();
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON
|
||||
}
|
||||
};
|
||||
rl.on("line", handler);
|
||||
});
|
||||
|
||||
// First, send a prompt to have some messages to compact
|
||||
agent.stdin!.write(JSON.stringify({ type: "prompt", message: "Say hello" }) + "\n");
|
||||
await waitForEvent("agent_end");
|
||||
|
||||
// Clear events to focus on compaction
|
||||
events.length = 0;
|
||||
|
||||
// Send compact command
|
||||
agent.stdin!.write(JSON.stringify({ type: "compact" }) + "\n");
|
||||
await waitForEvent("compaction");
|
||||
|
||||
// Verify compaction event
|
||||
const compactionEvent = events.find((e) => e.type === "compaction") as CompactionEntry | undefined;
|
||||
expect(compactionEvent).toBeDefined();
|
||||
expect(compactionEvent!.summary).toBeDefined();
|
||||
expect(compactionEvent!.tokensBefore).toBeGreaterThan(0);
|
||||
// Compact
|
||||
const result = await client.compact();
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.tokensBefore).toBeGreaterThan(0);
|
||||
|
||||
// Wait for file writes
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
agent.kill("SIGTERM");
|
||||
|
||||
// Verify compaction was saved to session file
|
||||
// Verify compaction in session file
|
||||
const sessionsPath = join(sessionDir, "sessions");
|
||||
const sessionDirs = readdirSync(sessionsPath);
|
||||
const cwdSessionDir = join(sessionsPath, sessionDirs[0]);
|
||||
|
|
@ -226,97 +111,34 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
|
|||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
// Should have a compaction entry
|
||||
const compactionEntries = entries.filter((e: { type: string }) => e.type === "compaction");
|
||||
expect(compactionEntries.length).toBe(1);
|
||||
expect(compactionEntries[0].summary).toBeDefined();
|
||||
}, 120000);
|
||||
|
||||
test("should execute bash command and add to context", async () => {
|
||||
// Spawn agent in RPC mode
|
||||
agent = spawn(
|
||||
"node",
|
||||
["dist/cli.js", "--mode", "rpc", "--provider", "anthropic", "--model", "claude-sonnet-4-5"],
|
||||
{
|
||||
cwd: join(__dirname, ".."),
|
||||
env: {
|
||||
...process.env,
|
||||
PI_CODING_AGENT_DIR: sessionDir,
|
||||
},
|
||||
},
|
||||
);
|
||||
test("should execute bash command", async () => {
|
||||
await client.start();
|
||||
|
||||
const events: (
|
||||
| AgentEvent
|
||||
| { type: "bash_end"; message: BashExecutionMessage }
|
||||
| { type: "error"; error: string }
|
||||
)[] = [];
|
||||
const result = await client.bash("echo hello");
|
||||
expect(result.output.trim()).toBe("hello");
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.cancelled).toBe(false);
|
||||
}, 30000);
|
||||
|
||||
const rl = readline.createInterface({ input: agent.stdout!, terminal: false });
|
||||
test("should add bash output to context", async () => {
|
||||
await client.start();
|
||||
|
||||
let stderr = "";
|
||||
agent.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
// First send a prompt to initialize session
|
||||
await client.promptAndWait("Say hi");
|
||||
|
||||
// Set up persistent event collector BEFORE sending any commands
|
||||
// This is critical for fast commands like bash that complete before
|
||||
// a per-call handler would be registered
|
||||
rl.on("line", (line: string) => {
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
events.push(event);
|
||||
} catch {
|
||||
// Ignore non-JSON
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to wait for a specific event type by polling collected events
|
||||
const waitForEvent = (eventType: string, timeout = 60000) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error(`Timeout waiting for ${eventType}. Stderr: ${stderr}`)),
|
||||
timeout,
|
||||
);
|
||||
const check = () => {
|
||||
if (events.some((e) => e.type === eventType)) {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(check, 50);
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
|
||||
// Send a bash command
|
||||
agent.stdin!.write(JSON.stringify({ type: "bash", command: "echo hello" }) + "\n");
|
||||
await waitForEvent("bash_end");
|
||||
|
||||
// Verify bash_end event
|
||||
const bashEvent = events.find((e) => e.type === "bash_end") as
|
||||
| { type: "bash_end"; message: BashExecutionMessage }
|
||||
| undefined;
|
||||
expect(bashEvent).toBeDefined();
|
||||
expect(bashEvent!.message.role).toBe("bashExecution");
|
||||
expect(bashEvent!.message.command).toBe("echo hello");
|
||||
expect(bashEvent!.message.output.trim()).toBe("hello");
|
||||
expect(bashEvent!.message.exitCode).toBe(0);
|
||||
expect(bashEvent!.message.cancelled).toBe(false);
|
||||
|
||||
// Clear events for next phase
|
||||
events.length = 0;
|
||||
|
||||
// Session only initializes after user+assistant exchange, so send a prompt
|
||||
agent.stdin!.write(JSON.stringify({ type: "prompt", message: "Say hi" }) + "\n");
|
||||
await waitForEvent("agent_end");
|
||||
// Run bash command
|
||||
const uniqueValue = `test-${Date.now()}`;
|
||||
await client.bash(`echo ${uniqueValue}`);
|
||||
|
||||
// Wait for file writes
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
agent.kill("SIGTERM");
|
||||
|
||||
// Verify bash execution was saved to session file
|
||||
// Verify bash message in session
|
||||
const sessionsPath = join(sessionDir, "sessions");
|
||||
const sessionDirs = readdirSync(sessionsPath);
|
||||
const cwdSessionDir = join(sessionsPath, sessionDirs[0]);
|
||||
|
|
@ -327,92 +149,27 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
|
|||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
// Should have a bashExecution message
|
||||
const bashMessages = entries.filter(
|
||||
(e: { type: string; message?: { role: string } }) =>
|
||||
e.type === "message" && e.message?.role === "bashExecution",
|
||||
);
|
||||
expect(bashMessages.length).toBe(1);
|
||||
expect(bashMessages[0].message.command).toBe("echo hello");
|
||||
expect(bashMessages[0].message.output.trim()).toBe("hello");
|
||||
expect(bashMessages[0].message.output).toContain(uniqueValue);
|
||||
}, 90000);
|
||||
|
||||
test("should include bash output in LLM context", async () => {
|
||||
// Spawn agent in RPC mode
|
||||
agent = spawn(
|
||||
"node",
|
||||
["dist/cli.js", "--mode", "rpc", "--provider", "anthropic", "--model", "claude-sonnet-4-5"],
|
||||
{
|
||||
cwd: join(__dirname, ".."),
|
||||
env: {
|
||||
...process.env,
|
||||
PI_CODING_AGENT_DIR: sessionDir,
|
||||
},
|
||||
},
|
||||
await client.start();
|
||||
|
||||
// Run a bash command with a unique value
|
||||
const uniqueValue = `unique-${Date.now()}`;
|
||||
await client.bash(`echo ${uniqueValue}`);
|
||||
|
||||
// Ask the LLM what the output was
|
||||
const events = await client.promptAndWait(
|
||||
"What was the exact output of the echo command I just ran? Reply with just the value, nothing else.",
|
||||
);
|
||||
|
||||
const events: (
|
||||
| AgentEvent
|
||||
| { type: "bash_end"; message: BashExecutionMessage }
|
||||
| { type: "error"; error: string }
|
||||
)[] = [];
|
||||
|
||||
const rl = readline.createInterface({ input: agent.stdout!, terminal: false });
|
||||
|
||||
let stderr = "";
|
||||
agent.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// Set up persistent event collector BEFORE sending any commands
|
||||
rl.on("line", (line: string) => {
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
events.push(event);
|
||||
} catch {
|
||||
// Ignore non-JSON
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to wait for a specific event type by polling collected events
|
||||
const waitForEvent = (eventType: string, timeout = 60000) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error(`Timeout waiting for ${eventType}. Stderr: ${stderr}`)),
|
||||
timeout,
|
||||
);
|
||||
const check = () => {
|
||||
if (events.some((e) => e.type === eventType)) {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(check, 50);
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
|
||||
// Wait for agent to initialize (session manager, etc.)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// First, run a bash command with a unique value
|
||||
const uniqueValue = `test-${Date.now()}`;
|
||||
agent.stdin!.write(JSON.stringify({ type: "bash", command: `echo ${uniqueValue}` }) + "\n");
|
||||
await waitForEvent("bash_end");
|
||||
|
||||
// Clear events but keep collecting new ones
|
||||
events.length = 0;
|
||||
|
||||
// Now ask the LLM what the output was - it should be in context
|
||||
agent.stdin!.write(
|
||||
JSON.stringify({
|
||||
type: "prompt",
|
||||
message: `What was the exact output of the echo command I just ran? Reply with just the value, nothing else.`,
|
||||
}) + "\n",
|
||||
);
|
||||
await waitForEvent("agent_end");
|
||||
|
||||
// Find the assistant's response
|
||||
// Find assistant's response
|
||||
const messageEndEvents = events.filter((e) => e.type === "message_end") as AgentEvent[];
|
||||
const assistantMessage = messageEndEvents.find(
|
||||
(e) => e.type === "message_end" && (e as any).message?.role === "assistant",
|
||||
|
|
@ -420,10 +177,109 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
|
|||
|
||||
expect(assistantMessage).toBeDefined();
|
||||
|
||||
// The assistant should mention the unique value from the bash output
|
||||
const textContent = assistantMessage.message.content.find((c: any) => c.type === "text");
|
||||
expect(textContent?.text).toContain(uniqueValue);
|
||||
}, 90000);
|
||||
|
||||
agent.kill("SIGTERM");
|
||||
test("should set and get thinking level", async () => {
|
||||
await client.start();
|
||||
|
||||
// Set thinking level
|
||||
await client.setThinkingLevel("high");
|
||||
|
||||
// Verify via state
|
||||
const state = await client.getState();
|
||||
expect(state.thinkingLevel).toBe("high");
|
||||
}, 30000);
|
||||
|
||||
test("should cycle thinking level", async () => {
|
||||
await client.start();
|
||||
|
||||
// Get initial level
|
||||
const initialState = await client.getState();
|
||||
const initialLevel = initialState.thinkingLevel;
|
||||
|
||||
// Cycle
|
||||
const result = await client.cycleThinkingLevel();
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.level).not.toBe(initialLevel);
|
||||
|
||||
// Verify via state
|
||||
const newState = await client.getState();
|
||||
expect(newState.thinkingLevel).toBe(result!.level);
|
||||
}, 30000);
|
||||
|
||||
test("should get available models", async () => {
|
||||
await client.start();
|
||||
|
||||
const models = await client.getAvailableModels();
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
|
||||
// All models should have required fields
|
||||
for (const model of models) {
|
||||
expect(model.provider).toBeDefined();
|
||||
expect(model.id).toBeDefined();
|
||||
expect(model.contextWindow).toBeGreaterThan(0);
|
||||
expect(typeof model.reasoning).toBe("boolean");
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test("should get session stats", async () => {
|
||||
await client.start();
|
||||
|
||||
// Send a prompt first
|
||||
await client.promptAndWait("Hello");
|
||||
|
||||
const stats = await client.getSessionStats();
|
||||
expect(stats.sessionFile).toBeDefined();
|
||||
expect(stats.sessionId).toBeDefined();
|
||||
expect(stats.userMessages).toBeGreaterThanOrEqual(1);
|
||||
expect(stats.assistantMessages).toBeGreaterThanOrEqual(1);
|
||||
}, 90000);
|
||||
|
||||
test("should reset session", async () => {
|
||||
await client.start();
|
||||
|
||||
// Send a prompt
|
||||
await client.promptAndWait("Hello");
|
||||
|
||||
// Verify messages exist
|
||||
let state = await client.getState();
|
||||
expect(state.messageCount).toBeGreaterThan(0);
|
||||
|
||||
// Reset
|
||||
await client.reset();
|
||||
|
||||
// Verify messages cleared
|
||||
state = await client.getState();
|
||||
expect(state.messageCount).toBe(0);
|
||||
}, 90000);
|
||||
|
||||
test("should export to HTML", async () => {
|
||||
await client.start();
|
||||
|
||||
// Send a prompt first
|
||||
await client.promptAndWait("Hello");
|
||||
|
||||
// Export
|
||||
const result = await client.exportHtml();
|
||||
expect(result.path).toBeDefined();
|
||||
expect(result.path.endsWith(".html")).toBe(true);
|
||||
expect(existsSync(result.path)).toBe(true);
|
||||
}, 90000);
|
||||
|
||||
test("should get last assistant text", async () => {
|
||||
await client.start();
|
||||
|
||||
// Initially null
|
||||
let text = await client.getLastAssistantText();
|
||||
expect(text).toBeNull();
|
||||
|
||||
// Send prompt
|
||||
await client.promptAndWait("Reply with just: test123");
|
||||
|
||||
// Should have text now
|
||||
text = await client.getLastAssistantText();
|
||||
expect(text).toContain("test123");
|
||||
}, 90000);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue