chore: delete unused files flagged by knip
38 files removed across app and server (verified unreferenced via grep + typecheck). Includes dead components, hooks, stores, utils, POC scripts, an empty orchestrator stub, stale playwright configs, and a duplicate workspace-registry test-helpers module. knip config tightened: added babel.config.js, native platform extensions, and test-stubs/ to app entry patterns.
This commit is contained in:
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* TDD Test Suite for Command Support POC
|
||||
*
|
||||
* This tests the ability to:
|
||||
* 1. Get available commands/skills from the Claude Agent SDK
|
||||
* 2. Execute commands (determine if they're just prompts or something else)
|
||||
*
|
||||
* Key findings from SDK analysis:
|
||||
* - `supportedCommands()` returns SlashCommand[] with name, description, argumentHint
|
||||
* - Commands are executed by sending them as prompts with / prefix
|
||||
* - The SDK init message includes `slash_commands: string[]` and `skills: string[]`
|
||||
*
|
||||
* IMPORTANT: Control methods like supportedCommands() work WITHOUT iterating the query first!
|
||||
* Use an empty async generator for the prompt when you just need control methods.
|
||||
* This pattern is used in claude-agent.ts listModels().
|
||||
*/
|
||||
|
||||
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { isCommandAvailable } from "../utils/executable.js";
|
||||
|
||||
const hasClaudeCredentials =
|
||||
!!process.env.CLAUDE_CODE_OAUTH_TOKEN || !!process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
// Pattern from claude-agent.ts listModels():
|
||||
// Use an empty async generator when you just need control methods
|
||||
function createEmptyPrompt(): AsyncGenerator<SDKUserMessage, void, undefined> {
|
||||
return (async function* empty() {})();
|
||||
}
|
||||
|
||||
describe("Claude Agent SDK Commands POC", () => {
|
||||
let canRunClaudeIntegration = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
canRunClaudeIntegration = (await isCommandAvailable("claude")) && hasClaudeCredentials;
|
||||
});
|
||||
|
||||
beforeEach((context) => {
|
||||
if (!canRunClaudeIntegration) {
|
||||
context.skip();
|
||||
}
|
||||
});
|
||||
|
||||
describe("supportedCommands() API", () => {
|
||||
test("should return an array of SlashCommand objects", async () => {
|
||||
// Use the pattern from claude-agent.ts:
|
||||
// Create a query with empty prompt generator for control methods
|
||||
const emptyPrompt = createEmptyPrompt();
|
||||
|
||||
const claudeQuery = query({
|
||||
prompt: emptyPrompt,
|
||||
options: {
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "plan",
|
||||
includePartialMessages: false,
|
||||
settingSources: ["user", "project"], // Required to load skills
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// supportedCommands() is a control method - works without iterating
|
||||
const commands = await claudeQuery.supportedCommands();
|
||||
|
||||
// Should be an array
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
|
||||
// Verify structure
|
||||
if (commands.length > 0) {
|
||||
const firstCommand = commands[0];
|
||||
expect(typeof firstCommand.name).toBe("string");
|
||||
expect(typeof firstCommand.description).toBe("string");
|
||||
expect(typeof firstCommand.argumentHint).toBe("string");
|
||||
expect(firstCommand.name.startsWith("/")).toBe(false);
|
||||
}
|
||||
} finally {
|
||||
if (typeof claudeQuery.return === "function") {
|
||||
try {
|
||||
await claudeQuery.return();
|
||||
} catch {
|
||||
// ignore shutdown errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test("should have valid SlashCommand structure for all commands", async () => {
|
||||
const emptyPrompt = createEmptyPrompt();
|
||||
|
||||
const claudeQuery = query({
|
||||
prompt: emptyPrompt,
|
||||
options: {
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "plan",
|
||||
settingSources: ["user", "project"],
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const commands = await claudeQuery.supportedCommands();
|
||||
|
||||
expect(commands.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify all commands have valid structure
|
||||
for (const cmd of commands) {
|
||||
expect(cmd).toHaveProperty("name");
|
||||
expect(cmd).toHaveProperty("description");
|
||||
expect(cmd).toHaveProperty("argumentHint");
|
||||
expect(typeof cmd.name).toBe("string");
|
||||
expect(typeof cmd.description).toBe("string");
|
||||
expect(typeof cmd.argumentHint).toBe("string");
|
||||
expect(cmd.name.length).toBeGreaterThan(0);
|
||||
expect(cmd.name.startsWith("/")).toBe(false);
|
||||
}
|
||||
} finally {
|
||||
await claudeQuery.return?.();
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe("Command Execution", () => {
|
||||
test("should explain that commands are prompts with / prefix", () => {
|
||||
// This is a documentation test - commands ARE just prompts with / prefix
|
||||
// To execute a command:
|
||||
// 1. Create a user message with content: "/{commandName}"
|
||||
// 2. Push it to the input stream
|
||||
// 3. Iterate the query to receive responses
|
||||
|
||||
// The SDK handles command expansion:
|
||||
// - Slash commands expand to their template content
|
||||
// - Skills invoke the Skill tool
|
||||
// - The model processes the expanded prompt like any other
|
||||
|
||||
expect(true).toBe(true); // Documentation-only test
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,186 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Investigation: What does command execution actually return?
|
||||
*
|
||||
* This script logs ALL message types and their full structure
|
||||
* to understand how command output is delivered.
|
||||
*/
|
||||
|
||||
import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
class Pushable<T> implements AsyncIterable<T> {
|
||||
private queue: T[] = [];
|
||||
private resolvers: Array<(value: IteratorResult<T, void>) => void> = [];
|
||||
private closed = false;
|
||||
|
||||
push(item: T) {
|
||||
if (this.closed) return;
|
||||
if (this.resolvers.length > 0) {
|
||||
const resolve = this.resolvers.shift()!;
|
||||
resolve({ value: item, done: false });
|
||||
} else {
|
||||
this.queue.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
this.closed = true;
|
||||
while (this.resolvers.length > 0) {
|
||||
const resolve = this.resolvers.shift()!;
|
||||
resolve({ value: undefined, done: true });
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<T, void> {
|
||||
return {
|
||||
next: (): Promise<IteratorResult<T, void>> => {
|
||||
if (this.queue.length > 0) {
|
||||
const value = this.queue.shift();
|
||||
if (value !== undefined) {
|
||||
return Promise.resolve({ value, done: false });
|
||||
}
|
||||
}
|
||||
if (this.closed) {
|
||||
return Promise.resolve({ value: undefined, done: true });
|
||||
}
|
||||
return new Promise<IteratorResult<T, void>>((resolve) => {
|
||||
this.resolvers.push(resolve);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function investigateCommand(commandName: string): Promise<void> {
|
||||
process.stdout.write(`\n${"=".repeat(60)}\n`);
|
||||
process.stdout.write(`Investigating: /${commandName}\n`);
|
||||
process.stdout.write(`${"=".repeat(60)}\n`);
|
||||
|
||||
const input = new Pushable<SDKUserMessage>();
|
||||
|
||||
const claudeQuery = query({
|
||||
prompt: input,
|
||||
options: {
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "plan",
|
||||
includePartialMessages: true, // Include streaming partial messages
|
||||
settingSources: ["user", "project"],
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const userMessage: SDKUserMessage = {
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: `/${commandName}`,
|
||||
},
|
||||
parent_tool_use_id: null,
|
||||
session_id: "",
|
||||
};
|
||||
|
||||
input.push(userMessage);
|
||||
|
||||
let messageCount = 0;
|
||||
for await (const message of claudeQuery) {
|
||||
messageCount++;
|
||||
process.stdout.write(`\n--- Message ${messageCount} ---\n`);
|
||||
process.stdout.write(`Type: ${message.type}\n`);
|
||||
|
||||
// Log the full structure based on type
|
||||
switch (message.type) {
|
||||
case "system":
|
||||
process.stdout.write(`Subtype: ${message.subtype}\n`);
|
||||
if (message.subtype === "init") {
|
||||
process.stdout.write(`Session: ${message.session_id}\n`);
|
||||
process.stdout.write(`Model: ${message.model}\n`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "user":
|
||||
process.stdout.write(
|
||||
`User message content: ${JSON.stringify(message.message?.content, null, 2)}\n`,
|
||||
);
|
||||
break;
|
||||
|
||||
case "assistant":
|
||||
process.stdout.write("Assistant message content:\n");
|
||||
const content = message.message?.content;
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
process.stdout.write(` Block type: ${block.type}\n`);
|
||||
if (block.type === "text") {
|
||||
process.stdout.write(` Text: ${block.text}\n`);
|
||||
} else if (block.type === "tool_use") {
|
||||
process.stdout.write(` Tool: ${block.name}\n`);
|
||||
process.stdout.write(` Input: ${JSON.stringify(block.input, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(` Full block: ${JSON.stringify(block, null, 2)}\n`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
process.stdout.write(` Content: ${JSON.stringify(content, null, 2)}\n`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "stream_event":
|
||||
process.stdout.write(`Stream event type: ${message.event?.type}\n`);
|
||||
if (message.event?.type === "content_block_delta") {
|
||||
const delta = message.event.delta;
|
||||
if (delta?.type === "text_delta") {
|
||||
process.stdout.write(` Text delta: ${delta.text}\n`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "result":
|
||||
process.stdout.write(`Result subtype: ${message.subtype}\n`);
|
||||
if ("errors" in message && message.errors) {
|
||||
process.stdout.write(`Errors: ${JSON.stringify(message.errors)}\n`);
|
||||
}
|
||||
// Check for any other properties
|
||||
const resultKeys = Object.keys(message).filter((k) => !["type", "subtype"].includes(k));
|
||||
if (resultKeys.length > 0) {
|
||||
process.stdout.write(`Other result properties: ${resultKeys.join(", ")}\n`);
|
||||
for (const key of resultKeys) {
|
||||
process.stdout.write(` ${key}: ${JSON.stringify((message as any)[key], null, 2)}\n`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
process.stdout.write(`Full message: ${JSON.stringify(message, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (message.type === "result") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`\nTotal messages received: ${messageCount}\n`);
|
||||
} finally {
|
||||
input.end();
|
||||
await claudeQuery.return?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.stdout.write("=== Command Output Investigation ===\n\n");
|
||||
|
||||
// Test /context - a local command that shows context info
|
||||
await investigateCommand("context");
|
||||
|
||||
// Test /cost - another local command
|
||||
await investigateCommand("cost");
|
||||
|
||||
// Test /prompt-engineer - a SKILL (not a local command)
|
||||
await investigateCommand("prompt-engineer");
|
||||
|
||||
process.stdout.write("\n=== Investigation Complete ===\n");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`Fatal error: ${error}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,212 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* POC Script: Claude Agent SDK Commands
|
||||
*
|
||||
* This script demonstrates how to:
|
||||
* 1. Get available slash commands/skills from the Claude Agent SDK
|
||||
* 2. Execute commands (they are prompts sent with / prefix)
|
||||
*
|
||||
* Key insight from existing claude-agent.ts:
|
||||
* - Control methods like supportedCommands() work WITHOUT iterating the query first
|
||||
* - Use an empty async generator for the prompt when you just want to call control methods
|
||||
* - Commands are executed by sending them as prompts with / prefix to a streaming query
|
||||
*
|
||||
* Usage: npx tsx src/poc-commands/run-poc.ts
|
||||
*/
|
||||
|
||||
import { query, type SlashCommand, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
// Pattern from claude-agent.ts listModels():
|
||||
// Use an empty async generator when you just need control methods
|
||||
function createEmptyPrompt(): AsyncGenerator<SDKUserMessage, void, undefined> {
|
||||
return (async function* empty() {})();
|
||||
}
|
||||
|
||||
// Utility: Create a pushable stream for SDK input (for command execution demo)
|
||||
class Pushable<T> implements AsyncIterable<T> {
|
||||
private queue: T[] = [];
|
||||
private resolvers: Array<(value: IteratorResult<T, void>) => void> = [];
|
||||
private closed = false;
|
||||
|
||||
push(item: T) {
|
||||
if (this.closed) return;
|
||||
if (this.resolvers.length > 0) {
|
||||
const resolve = this.resolvers.shift()!;
|
||||
resolve({ value: item, done: false });
|
||||
} else {
|
||||
this.queue.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
this.closed = true;
|
||||
while (this.resolvers.length > 0) {
|
||||
const resolve = this.resolvers.shift()!;
|
||||
resolve({ value: undefined, done: true });
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<T, void> {
|
||||
return {
|
||||
next: (): Promise<IteratorResult<T, void>> => {
|
||||
if (this.queue.length > 0) {
|
||||
const value = this.queue.shift();
|
||||
if (value !== undefined) {
|
||||
return Promise.resolve({ value, done: false });
|
||||
}
|
||||
}
|
||||
if (this.closed) {
|
||||
return Promise.resolve({ value: undefined, done: true });
|
||||
}
|
||||
return new Promise<IteratorResult<T, void>>((resolve) => {
|
||||
this.resolvers.push(resolve);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function listAvailableCommands(): Promise<SlashCommand[]> {
|
||||
// Use the pattern from claude-agent.ts listModels():
|
||||
// Create a query with an empty prompt generator to call control methods
|
||||
const emptyPrompt = createEmptyPrompt();
|
||||
|
||||
const claudeQuery = query({
|
||||
prompt: emptyPrompt,
|
||||
options: {
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "plan",
|
||||
includePartialMessages: false,
|
||||
settingSources: ["user", "project"], // Required to load skills
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// supportedCommands() is a control method - works without iterating
|
||||
const commands = await claudeQuery.supportedCommands();
|
||||
return commands;
|
||||
} finally {
|
||||
// Clean up
|
||||
if (typeof claudeQuery.return === "function") {
|
||||
try {
|
||||
await claudeQuery.return();
|
||||
} catch {
|
||||
// ignore shutdown errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCommand(commandName: string): Promise<void> {
|
||||
process.stdout.write(`\n=== Executing command: /${commandName} ===\n`);
|
||||
|
||||
// For command execution, we need a proper input stream
|
||||
const input = new Pushable<SDKUserMessage>();
|
||||
|
||||
const claudeQuery = query({
|
||||
prompt: input,
|
||||
options: {
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "plan",
|
||||
includePartialMessages: false,
|
||||
settingSources: ["user", "project"],
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Push the command as a user message with / prefix
|
||||
const userMessage: SDKUserMessage = {
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: `/${commandName}`,
|
||||
},
|
||||
parent_tool_use_id: null,
|
||||
session_id: "",
|
||||
};
|
||||
|
||||
input.push(userMessage);
|
||||
|
||||
// Iterate the query to process the command
|
||||
let gotSystemInit = false;
|
||||
for await (const message of claudeQuery) {
|
||||
process.stdout.write(
|
||||
` [${message.type}] ${message.type === "system" ? message.subtype : ""}\n`,
|
||||
);
|
||||
|
||||
if (message.type === "system" && message.subtype === "init") {
|
||||
gotSystemInit = true;
|
||||
process.stdout.write(` Session: ${message.session_id}\n`);
|
||||
process.stdout.write(` Model: ${message.model}\n`);
|
||||
}
|
||||
|
||||
if (message.type === "assistant") {
|
||||
const content = message.message?.content;
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === "text") {
|
||||
process.stdout.write(
|
||||
` Response: ${block.text.slice(0, 200)}${block.text.length > 200 ? "..." : ""}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === "result") {
|
||||
process.stdout.write(` Result: ${message.subtype}\n`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
input.end();
|
||||
await claudeQuery.return?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.stdout.write("=== Claude Agent SDK Commands POC ===\n\n");
|
||||
|
||||
// PART 1: List available commands using supportedCommands()
|
||||
process.stdout.write("=== Part 1: List Available Commands ===\n\n");
|
||||
|
||||
try {
|
||||
const commands = await listAvailableCommands();
|
||||
|
||||
process.stdout.write(`Found ${commands.length} commands:\n\n`);
|
||||
commands.forEach((cmd, index) => {
|
||||
process.stdout.write(` ${index + 1}. /${cmd.name}\n`);
|
||||
process.stdout.write(` Description: ${cmd.description}\n`);
|
||||
if (cmd.argumentHint) {
|
||||
process.stdout.write(` Arguments: ${cmd.argumentHint}\n`);
|
||||
}
|
||||
process.stdout.write("\n");
|
||||
});
|
||||
|
||||
// PART 2: Demonstrate command execution (optional - uncomment to test)
|
||||
// Commands are just prompts sent with / prefix
|
||||
process.stdout.write("=== Part 2: Command Execution Explanation ===\n");
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write("Commands are executed by sending them as prompts with / prefix.\n");
|
||||
process.stdout.write("For example, to execute the 'help' command:\n");
|
||||
process.stdout.write(' 1. Create a user message with content: "/help"\n');
|
||||
process.stdout.write(" 2. Push it to the input stream\n");
|
||||
process.stdout.write(" 3. Iterate the query to receive responses\n");
|
||||
process.stdout.write("\n");
|
||||
|
||||
// Actually execute a command to demonstrate it works:
|
||||
// Using "context" as it's fast and doesn't require arguments
|
||||
await executeCommand("context");
|
||||
} catch (error) {
|
||||
process.stderr.write(`ERROR: ${error}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write("=== POC Complete ===\n");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`Fatal error: ${error}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ToolSet } from "ai";
|
||||
|
||||
/**
|
||||
* Get all tools for voice LLM
|
||||
* @param agentTools - Agent control tools from MCP
|
||||
*/
|
||||
export function getAllTools(agentTools?: ToolSet): ToolSet {
|
||||
return agentTools ?? {};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* DEPRECATED: This file has been refactored.
|
||||
*
|
||||
* All orchestration logic has been moved to session.ts.
|
||||
* Each Session now owns its own conversation state and manages
|
||||
* the full lifecycle of user interactions.
|
||||
*
|
||||
* This file is kept as a stub to document the migration.
|
||||
* It can be deleted once the refactoring is confirmed working.
|
||||
*/
|
||||
@@ -1,173 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Ad-hoc script to debug checkout_status_request timeouts.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts [agentIdOrCwd1] [agentIdOrCwd2]
|
||||
*
|
||||
* To test against a different daemon:
|
||||
* PASEO_LISTEN=127.0.0.1:7777 npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts
|
||||
*/
|
||||
|
||||
import { WebSocket } from "ws";
|
||||
import os from "node:os";
|
||||
import { DaemonClient } from "../../client/daemon-client.js";
|
||||
|
||||
// Patch WebSocket to log all messages
|
||||
const OriginalWebSocket = WebSocket;
|
||||
class LoggingWebSocket extends OriginalWebSocket {
|
||||
constructor(url: string, ...args: any[]) {
|
||||
super(url, ...args);
|
||||
console.log(`[WS] Connecting to ${url}`);
|
||||
this.on("open", () => console.log("[WS] Connection opened"));
|
||||
this.on("close", (code, reason) => console.log(`[WS] Connection closed: ${code} ${reason}`));
|
||||
this.on("error", (err) => console.log(`[WS] Error: ${err}`));
|
||||
this.on("message", (data) => {
|
||||
const str = data.toString().slice(0, 200);
|
||||
console.log(`[WS] Message received (${data.toString().length} bytes): ${str}...`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const PASEO_HOME = process.env.PASEO_HOME ?? `${os.homedir()}/.paseo`;
|
||||
const PASEO_LISTEN = process.env.PASEO_LISTEN ?? "127.0.0.1:6767";
|
||||
const DAEMON_URL = `ws://${PASEO_LISTEN}/ws`;
|
||||
const CLIENT_ID = "clsk_checkout_debug";
|
||||
|
||||
function requestCheckoutStatus(client: DaemonClient, cwd: string) {
|
||||
return (client as any)[`get${"Checkout"}Status`](cwd);
|
||||
}
|
||||
|
||||
async function testMultiAgentSequence() {
|
||||
console.log("\n=== Testing multi-agent checkout sequence ===");
|
||||
console.log(`Daemon URL: ${DAEMON_URL}`);
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: DAEMON_URL,
|
||||
clientId: CLIENT_ID,
|
||||
webSocketFactory: (url) => new LoggingWebSocket(url) as any,
|
||||
reconnect: { enabled: false },
|
||||
});
|
||||
|
||||
const agents: Array<{ id: string; title: string; cwd: string }> = [];
|
||||
|
||||
// Also log raw messages for debugging
|
||||
client.on("checkout_status_response", (msg: any) => {
|
||||
console.log(
|
||||
`[RAW checkout_status_response] requestId=${msg.payload.requestId} cwd=${msg.payload.cwd}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Listen to connection state changes
|
||||
client.subscribeConnectionStatus((state) => {
|
||||
console.log(`[Connection] status=${state.status}`);
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log("Connected to daemon");
|
||||
console.log(`Connection state: ${JSON.stringify(client.getConnectionState())}`);
|
||||
|
||||
console.log("Fetching agents...");
|
||||
const agentsList = await client.fetchAgents();
|
||||
agents.length = 0;
|
||||
for (const a of agentsList) {
|
||||
agents.push({ id: a.id, title: a.title ?? "(untitled)", cwd: a.cwd });
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.log("No agents found!");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\nAvailable agents:");
|
||||
for (const a of agents.slice(0, 10)) {
|
||||
console.log(` - ${a.id.slice(0, 8)}... ${a.title}`);
|
||||
}
|
||||
if (agents.length > 10) {
|
||||
console.log(` ... and ${agents.length - 10} more`);
|
||||
}
|
||||
|
||||
// Pick first two agents (or use command line args)
|
||||
const arg1 = process.argv[2];
|
||||
const arg2 = process.argv[3];
|
||||
const agent1 = (arg1 ? agents.find((a) => a.id === arg1) : null) ?? agents[0] ?? null;
|
||||
const agent2 =
|
||||
(arg2 ? agents.find((a) => a.id === arg2) : null) ?? agents[1] ?? agents[0] ?? null;
|
||||
|
||||
const cwd1 = arg1 && !agent1 ? arg1 : agent1?.cwd;
|
||||
const cwd2 = arg2 && !agent2 ? arg2 : agent2?.cwd;
|
||||
|
||||
if (!cwd1) {
|
||||
console.log("No checkout cwd available to test");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n=== Test 1: Request checkout for cwd1 (${cwd1}) ===`);
|
||||
const start1 = Date.now();
|
||||
try {
|
||||
const status1 = await requestCheckoutStatus(client, cwd1);
|
||||
console.log(
|
||||
`✓ Cwd1 completed in ${Date.now() - start1}ms - branch: ${status1.currentBranch}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(`✗ Cwd1 failed after ${Date.now() - start1}ms:`, err);
|
||||
}
|
||||
|
||||
if (cwd2) {
|
||||
console.log(`\n=== Test 2: Request checkout for cwd2 (${cwd2}) ===`);
|
||||
const start2 = Date.now();
|
||||
try {
|
||||
const status2 = await requestCheckoutStatus(client, cwd2);
|
||||
console.log(
|
||||
`✓ Cwd2 completed in ${Date.now() - start2}ms - branch: ${status2.currentBranch}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(`✗ Cwd2 failed after ${Date.now() - start2}ms:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n=== Test 3: Request checkout for cwd1 again ===`);
|
||||
const start3 = Date.now();
|
||||
try {
|
||||
const status3 = await requestCheckoutStatus(client, cwd1);
|
||||
console.log(
|
||||
`✓ Cwd1 (retry) completed in ${Date.now() - start3}ms - branch: ${status3.currentBranch}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(`✗ Cwd1 (retry) failed after ${Date.now() - start3}ms:`, err);
|
||||
}
|
||||
|
||||
if (cwd2) {
|
||||
console.log(`\n=== Test 4: Request both cwds in parallel ===`);
|
||||
const start4 = Date.now();
|
||||
try {
|
||||
const [p1, p2] = await Promise.all([
|
||||
requestCheckoutStatus(client, cwd1),
|
||||
requestCheckoutStatus(client, cwd2),
|
||||
]);
|
||||
console.log(`✓ Parallel completed in ${Date.now() - start4}ms`);
|
||||
console.log(` Cwd1 branch: ${p1.currentBranch}`);
|
||||
console.log(` Cwd2 branch: ${p2.currentBranch}`);
|
||||
} catch (err) {
|
||||
console.log(`✗ Parallel failed after ${Date.now() - start4}ms:`, err);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Test failed:", error);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Checkout Debug Script - Multi-Agent Sequence Test");
|
||||
console.log("==================================================");
|
||||
console.log(`PASEO_HOME: ${PASEO_HOME}`);
|
||||
|
||||
await testMultiAgentSequence();
|
||||
|
||||
console.log("\n=== Done ===");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,48 +0,0 @@
|
||||
import "dotenv/config";
|
||||
import { beforeAll, afterAll } from "vitest";
|
||||
import { mkdtempSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import path from "path";
|
||||
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
|
||||
import { agentConfigs } from "./agent-configs.js";
|
||||
|
||||
// Re-export for backward compatibility - prefer using agentConfigs instead
|
||||
export const CODEX_TEST_MODEL = agentConfigs.codex.model;
|
||||
export const CODEX_TEST_THINKING_OPTION_ID = agentConfigs.codex.thinkingOptionId;
|
||||
|
||||
// Re-export agent configs
|
||||
export {
|
||||
agentConfigs,
|
||||
getFullAccessConfig,
|
||||
getAskModeConfig,
|
||||
allProviders,
|
||||
type AgentProvider,
|
||||
type AgentTestConfig,
|
||||
} from "./agent-configs.js";
|
||||
|
||||
export function tmpCwd(): string {
|
||||
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
|
||||
}
|
||||
|
||||
// Shared daemon context for all e2e tests
|
||||
let sharedCtx: DaemonTestContext | null = null;
|
||||
|
||||
export function getTestContext(): DaemonTestContext {
|
||||
if (!sharedCtx) {
|
||||
throw new Error("Test context not initialized. Did you call setupDaemonE2E()?");
|
||||
}
|
||||
return sharedCtx;
|
||||
}
|
||||
|
||||
export function setupDaemonE2E(): void {
|
||||
beforeAll(async () => {
|
||||
sharedCtx = await createDaemonTestContext();
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (sharedCtx) {
|
||||
await sharedCtx.cleanup();
|
||||
sharedCtx = null;
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Shared server types
|
||||
|
||||
export interface ServerConfig {
|
||||
port: number;
|
||||
isDev: boolean;
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import {
|
||||
type PersistedProjectRecord,
|
||||
type PersistedWorkspaceRecord,
|
||||
type ProjectRegistry,
|
||||
type WorkspaceRegistry,
|
||||
createPersistedProjectRecord,
|
||||
createPersistedWorkspaceRecord,
|
||||
} from "./workspace-registry.js";
|
||||
|
||||
type RegistryRecord = PersistedProjectRecord | PersistedWorkspaceRecord;
|
||||
|
||||
class FileBackedRegistry<TRecord extends RegistryRecord> {
|
||||
private readonly filePath: string;
|
||||
private readonly logger: Logger;
|
||||
private readonly schema: (record: unknown) => TRecord;
|
||||
private readonly getId: (record: TRecord) => string;
|
||||
private loaded = false;
|
||||
private readonly cache = new Map<string, TRecord>();
|
||||
private persistQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor(options: {
|
||||
filePath: string;
|
||||
logger: Logger;
|
||||
schema: (record: unknown) => TRecord;
|
||||
getId: (record: TRecord) => string;
|
||||
component: string;
|
||||
}) {
|
||||
this.filePath = options.filePath;
|
||||
this.schema = options.schema;
|
||||
this.getId = options.getId;
|
||||
this.logger = options.logger.child({
|
||||
module: "workspace-registry",
|
||||
component: options.component,
|
||||
});
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async existsOnDisk(): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(this.filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async list(): Promise<TRecord[]> {
|
||||
await this.load();
|
||||
return Array.from(this.cache.values());
|
||||
}
|
||||
|
||||
async get(id: string): Promise<TRecord | null> {
|
||||
await this.load();
|
||||
return this.cache.get(id) ?? null;
|
||||
}
|
||||
|
||||
async upsert(record: TRecord): Promise<void> {
|
||||
await this.load();
|
||||
const parsed = this.schema(record);
|
||||
this.cache.set(this.getId(parsed), parsed);
|
||||
await this.enqueuePersist();
|
||||
}
|
||||
|
||||
async archive(id: string, archivedAt: string): Promise<void> {
|
||||
await this.load();
|
||||
const existing = this.cache.get(id);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
const next = this.schema({
|
||||
...existing,
|
||||
updatedAt: archivedAt,
|
||||
archivedAt,
|
||||
});
|
||||
this.cache.set(id, next);
|
||||
await this.enqueuePersist();
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.load();
|
||||
if (!this.cache.delete(id)) {
|
||||
return;
|
||||
}
|
||||
await this.enqueuePersist();
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
if (this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cache.clear();
|
||||
try {
|
||||
const raw = await fs.readFile(this.filePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as TRecord[];
|
||||
for (const record of parsed) {
|
||||
const validated = this.schema(record);
|
||||
this.cache.set(this.getId(validated), validated);
|
||||
}
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code !== "ENOENT") {
|
||||
this.logger.error({ err: error, filePath: this.filePath }, "Failed to load registry file");
|
||||
}
|
||||
}
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
private async persist(): Promise<void> {
|
||||
const records = Array.from(this.cache.values());
|
||||
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
||||
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
||||
await fs.writeFile(tempPath, JSON.stringify(records, null, 2), "utf8");
|
||||
await fs.rename(tempPath, this.filePath);
|
||||
}
|
||||
|
||||
private async enqueuePersist(): Promise<void> {
|
||||
const nextPersist = this.persistQueue.then(() => this.persist());
|
||||
this.persistQueue = nextPersist.catch(() => {});
|
||||
await nextPersist;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileBackedProjectRegistry
|
||||
extends FileBackedRegistry<PersistedProjectRecord>
|
||||
implements ProjectRegistry
|
||||
{
|
||||
constructor(filePath: string, logger: Logger) {
|
||||
super({
|
||||
filePath,
|
||||
logger,
|
||||
schema: (record) => createPersistedProjectRecord(record as PersistedProjectRecord),
|
||||
getId: (record) => record.projectId,
|
||||
component: "projects",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileBackedWorkspaceRegistry
|
||||
extends FileBackedRegistry<PersistedWorkspaceRecord>
|
||||
implements WorkspaceRegistry
|
||||
{
|
||||
constructor(filePath: string, logger: Logger) {
|
||||
super({
|
||||
filePath,
|
||||
logger,
|
||||
schema: (record) => createPersistedWorkspaceRecord(record as PersistedWorkspaceRecord),
|
||||
getId: (record) => record.workspaceId,
|
||||
component: "workspaces",
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user