WIP: Refactor and polyfill updates
- Add crypto polyfill for Expo - Update daemon client and websocket server - Simplify codex agent tests
This commit is contained in:
Generated
+13
@@ -12154,6 +12154,18 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-crypto": {
|
||||||
|
"version": "15.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz",
|
||||||
|
"integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-dev-client": {
|
"node_modules/expo-dev-client": {
|
||||||
"version": "6.0.16",
|
"version": "6.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.16.tgz",
|
||||||
@@ -21554,6 +21566,7 @@
|
|||||||
"expo-build-properties": "^1.0.9",
|
"expo-build-properties": "^1.0.9",
|
||||||
"expo-clipboard": "~8.0.7",
|
"expo-clipboard": "~8.0.7",
|
||||||
"expo-constants": "~18.0.9",
|
"expo-constants": "~18.0.9",
|
||||||
|
"expo-crypto": "^15.0.8",
|
||||||
"expo-dev-client": "^6.0.15",
|
"expo-dev-client": "^6.0.15",
|
||||||
"expo-file-system": "~19.0.17",
|
"expo-file-system": "~19.0.17",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Polyfill crypto.randomUUID for React Native before any other imports
|
||||||
|
import { polyfillCrypto } from "./src/polyfills/crypto";
|
||||||
|
polyfillCrypto();
|
||||||
|
|
||||||
// Configure Unistyles before Expo Router pulls in any components using StyleSheet.
|
// Configure Unistyles before Expo Router pulls in any components using StyleSheet.
|
||||||
import "./src/styles/unistyles";
|
import "./src/styles/unistyles";
|
||||||
import "expo-router/entry";
|
import "expo-router/entry";
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"expo-build-properties": "^1.0.9",
|
"expo-build-properties": "^1.0.9",
|
||||||
"expo-clipboard": "~8.0.7",
|
"expo-clipboard": "~8.0.7",
|
||||||
"expo-constants": "~18.0.9",
|
"expo-constants": "~18.0.9",
|
||||||
|
"expo-crypto": "^15.0.8",
|
||||||
"expo-dev-client": "^6.0.15",
|
"expo-dev-client": "^6.0.15",
|
||||||
"expo-file-system": "~19.0.17",
|
"expo-file-system": "~19.0.17",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
|
|||||||
@@ -103,11 +103,14 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
borderTopRightRadius: theme.borderRadius.sm,
|
borderTopRightRadius: theme.borderRadius.sm,
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
paddingVertical: theme.spacing[4],
|
paddingVertical: theme.spacing[4],
|
||||||
|
minWidth: 0,
|
||||||
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
|
overflowWrap: "anywhere",
|
||||||
},
|
},
|
||||||
bubblePressed: {
|
bubblePressed: {
|
||||||
opacity: 0.85,
|
opacity: 0.85,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import * as ExpoCrypto from "expo-crypto";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Crypto {
|
||||||
|
randomUUID(): `${string}-${string}-${string}-${string}-${string}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function polyfillCrypto(): void {
|
||||||
|
if (typeof globalThis.crypto === "undefined") {
|
||||||
|
(globalThis as any).crypto = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof globalThis.crypto.randomUUID !== "function") {
|
||||||
|
globalThis.crypto.randomUUID = () =>
|
||||||
|
ExpoCrypto.randomUUID() as `${string}-${string}-${string}-${string}-${string}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof globalThis.crypto.getRandomValues !== "function") {
|
||||||
|
globalThis.crypto.getRandomValues = <T extends ArrayBufferView>(array: T): T => {
|
||||||
|
return ExpoCrypto.getRandomValues(array as any) as T;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { nanoid } from "nanoid";
|
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import {
|
import {
|
||||||
AgentCreateFailedStatusPayloadSchema,
|
AgentCreateFailedStatusPayloadSchema,
|
||||||
@@ -754,7 +753,7 @@ export class DaemonClientV2 {
|
|||||||
text: string,
|
text: string,
|
||||||
options?: SendMessageOptions
|
options?: SendMessageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const messageId = options?.messageId ?? nanoid();
|
const messageId = options?.messageId ?? crypto.randomUUID();
|
||||||
const message = SessionInboundMessageSchema.parse({
|
const message = SessionInboundMessageSchema.parse({
|
||||||
type: "send_agent_message",
|
type: "send_agent_message",
|
||||||
agentId,
|
agentId,
|
||||||
@@ -1354,7 +1353,7 @@ export class DaemonClientV2 {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
private createRequestId(requestId?: string): string {
|
private createRequestId(requestId?: string): string {
|
||||||
return requestId ?? nanoid();
|
return requestId ?? crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanupTransport(): void {
|
private cleanupTransport(): void {
|
||||||
|
|||||||
@@ -719,6 +719,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
||||||
this.input?.end();
|
this.input?.end();
|
||||||
|
await this.query?.interrupt?.();
|
||||||
await this.query?.return?.();
|
await this.query?.return?.();
|
||||||
this.query = null;
|
this.query = null;
|
||||||
this.input = null;
|
this.input = null;
|
||||||
|
|||||||
@@ -740,7 +740,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"captures tool call inputs/outputs for commands, file changes, file reads, MCP tools, and web search",
|
"captures tool call inputs/outputs for commands, file changes, file reads, and MCP tools",
|
||||||
async () => {
|
async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const restoreSessionDir = useTempCodexSessionDir();
|
const restoreSessionDir = useTempCodexSessionDir();
|
||||||
@@ -751,13 +751,9 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
const config = {
|
const config = {
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd,
|
cwd,
|
||||||
modeId: "auto",
|
modeId: "full-access",
|
||||||
approvalPolicy: "on-request",
|
|
||||||
networkAccess: true,
|
|
||||||
extra: {
|
extra: {
|
||||||
codex: {
|
codex: {
|
||||||
search: true,
|
|
||||||
features: { web_search_request: true },
|
|
||||||
mcp_servers: {
|
mcp_servers: {
|
||||||
test: {
|
test: {
|
||||||
command: process.execPath,
|
command: process.execPath,
|
||||||
@@ -770,38 +766,22 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
} satisfies AgentSessionConfig;
|
} satisfies AgentSessionConfig;
|
||||||
|
|
||||||
let session: AgentSession | null = null;
|
let session: AgentSession | null = null;
|
||||||
let permissionRequest: AgentPermissionRequest | null = null;
|
|
||||||
let permissionResolved = false;
|
|
||||||
const toolCalls: ToolCallItem[] = [];
|
const toolCalls: ToolCallItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session = await client.createSession(config);
|
session = await client.createSession(config);
|
||||||
|
|
||||||
const prompt = [
|
const prompt = [
|
||||||
"1. Run the command `bash -lc \"printf 'stdout-marker'\"` using your shell tool.",
|
"1. Run the command `printf 'stdout-marker'` using your shell tool.",
|
||||||
"2. Run the command `bash -lc \"printf 'stderr-marker' 1>&2\"` using your shell tool.",
|
"2. Run the command `printf 'stderr-marker' 1>&2` using your shell tool.",
|
||||||
"3. Use apply_patch (not the shell) to create a new file named tool-create.txt containing only the line 'alpha'.",
|
"3. Use apply_patch to create a new file named tool-create.txt containing only the line 'alpha'.",
|
||||||
"4. Use apply_patch (not the shell) to edit tool-create.txt, replacing 'alpha' with 'beta'.",
|
"4. Use apply_patch to edit tool-create.txt, replacing 'alpha' with 'beta'.",
|
||||||
"5. Read the file tool-create.txt and report its contents (you can use cat or any file reading method).",
|
"5. Read the file tool-create.txt using read_file tool.",
|
||||||
"6. Call the MCP tool test.echo with input {\"text\":\"mcp-ok\"}.",
|
"6. Call the MCP tool test.echo with input {\"text\":\"mcp-ok\"}.",
|
||||||
"7. Use the web_search tool to search for \"OpenAI Codex MCP\".",
|
"7. Reply DONE and stop.",
|
||||||
"8. Request approval to run the command `printf \"permit\" > tool-permission.txt`, then run it.",
|
|
||||||
"9. After all tools finish, reply DONE and stop.",
|
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
for await (const event of session.stream(prompt)) {
|
for await (const event of session.stream(prompt)) {
|
||||||
if (event.type === "permission_requested" && !permissionRequest) {
|
|
||||||
permissionRequest = event.request;
|
|
||||||
await session.respondToPermission(permissionRequest.id, { behavior: "allow" });
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.type === "permission_resolved" &&
|
|
||||||
permissionRequest &&
|
|
||||||
event.requestId === permissionRequest.id &&
|
|
||||||
event.resolution.behavior === "allow"
|
|
||||||
) {
|
|
||||||
permissionResolved = true;
|
|
||||||
}
|
|
||||||
if (event.type === "timeline" && providerFromEvent(event) === "codex") {
|
if (event.type === "timeline" && providerFromEvent(event) === "codex") {
|
||||||
if (event.item.type === "tool_call" && event.item.name !== "permission") {
|
if (event.item.type === "tool_call" && event.item.name !== "permission") {
|
||||||
toolCalls.push(event.item);
|
toolCalls.push(event.item);
|
||||||
@@ -812,9 +792,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect.soft(permissionRequest).not.toBeNull();
|
|
||||||
expect.soft(permissionResolved).toBe(true);
|
|
||||||
|
|
||||||
const commandCalls = toolCalls.filter(
|
const commandCalls = toolCalls.filter(
|
||||||
(item) => item.name === "shell" && item.status === "completed"
|
(item) => item.name === "shell" && item.status === "completed"
|
||||||
);
|
);
|
||||||
@@ -868,15 +845,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
expect.soft(stringifyUnknown(mcpCall?.input)).toContain("mcp-ok");
|
expect.soft(stringifyUnknown(mcpCall?.input)).toContain("mcp-ok");
|
||||||
expect.soft(stringifyUnknown(mcpCall?.output)).toContain("mcp-ok");
|
expect.soft(stringifyUnknown(mcpCall?.output)).toContain("mcp-ok");
|
||||||
|
|
||||||
const webSearchCall = toolCalls.find(
|
|
||||||
(item) => item.name === "web_search"
|
|
||||||
);
|
|
||||||
expect.soft(webSearchCall).toBeTruthy();
|
|
||||||
expect.soft(stringifyUnknown(webSearchCall?.input)).toContain("OpenAI Codex MCP");
|
|
||||||
// NOTE: Codex MCP web_search does not return search results in the event.
|
|
||||||
// The search happens internally but results are not exposed via MCP events.
|
|
||||||
// Only verify that the search was performed (input contains query).
|
|
||||||
|
|
||||||
const callIdStatuses = new Map<string, Set<string>>();
|
const callIdStatuses = new Map<string, Set<string>>();
|
||||||
for (const toolCall of toolCalls) {
|
for (const toolCall of toolCalls) {
|
||||||
if (!toolCall.callId) {
|
if (!toolCall.callId) {
|
||||||
@@ -914,7 +882,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
restoreSessionDir();
|
restoreSessionDir();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
240_000
|
120_000
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -3136,41 +3136,38 @@ class CodexMcpAgentSession implements AgentSession {
|
|||||||
? (rawMetadata as Record<string, unknown>).codex_elicitation
|
? (rawMetadata as Record<string, unknown>).codex_elicitation
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
let action: ElicitResult["action"];
|
// Use MCP ElicitResult format only if explicitly tagged as non-exec-approval
|
||||||
let content: Record<string, unknown> | undefined;
|
// Default to Codex exec-approval format (what Codex SDK actually sends)
|
||||||
let decision: CodexExecApprovalDecision | undefined;
|
if (codexElicitation && codexElicitation !== "exec-approval") {
|
||||||
let reason: string | undefined;
|
const action: ElicitResult["action"] =
|
||||||
|
|
||||||
if (codexElicitation === "exec-approval") {
|
|
||||||
decision =
|
|
||||||
response.behavior === "allow"
|
|
||||||
? "approved"
|
|
||||||
: response.interrupt
|
|
||||||
? "abort"
|
|
||||||
: "denied";
|
|
||||||
reason = response.behavior === "deny" ? response.message : undefined;
|
|
||||||
const responsePayload: CodexExecApprovalResponse = {
|
|
||||||
decision,
|
|
||||||
...(reason ? { reason } : {}),
|
|
||||||
};
|
|
||||||
pending.resolve(responsePayload);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
action =
|
|
||||||
response.behavior === "allow"
|
response.behavior === "allow"
|
||||||
? "accept"
|
? "accept"
|
||||||
: response.interrupt
|
: response.interrupt
|
||||||
? "cancel"
|
? "cancel"
|
||||||
: "decline";
|
: "decline";
|
||||||
content =
|
const content =
|
||||||
response.behavior === "allow" && response.updatedInput
|
response.behavior === "allow" && response.updatedInput
|
||||||
? response.updatedInput
|
? response.updatedInput
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const responsePayload: ElicitResult = {
|
||||||
|
action,
|
||||||
|
...(content ? { content } : {}),
|
||||||
|
};
|
||||||
|
pending.resolve(responsePayload);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const responsePayload: ElicitResult = {
|
// Default: Codex exec-approval format
|
||||||
action,
|
const decision: CodexExecApprovalDecision =
|
||||||
...(content ? { content } : {}),
|
response.behavior === "allow"
|
||||||
|
? "approved"
|
||||||
|
: response.interrupt
|
||||||
|
? "abort"
|
||||||
|
: "denied";
|
||||||
|
const reason = response.behavior === "deny" ? response.message : undefined;
|
||||||
|
const responsePayload: CodexExecApprovalResponse = {
|
||||||
|
decision,
|
||||||
|
...(reason ? { reason } : {}),
|
||||||
};
|
};
|
||||||
pending.resolve(responsePayload);
|
pending.resolve(responsePayload);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,7 +294,17 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
const cleanupPromises: Promise<void>[] = [];
|
const cleanupPromises: Promise<void>[] = [];
|
||||||
this.sessions.forEach((session, ws) => {
|
this.sessions.forEach((session, ws) => {
|
||||||
cleanupPromises.push(session.cleanup());
|
cleanupPromises.push(session.cleanup());
|
||||||
ws.close();
|
// Wait for WebSocket to actually close before resolving
|
||||||
|
cleanupPromises.push(
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
if (ws.readyState === WebSocket.CLOSED) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.once("close", () => resolve());
|
||||||
|
ws.close();
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await Promise.all(cleanupPromises);
|
await Promise.all(cleanupPromises);
|
||||||
this.wss.close();
|
this.wss.close();
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ export default defineConfig({
|
|||||||
globals: true,
|
globals: true,
|
||||||
environment: "node",
|
environment: "node",
|
||||||
setupFiles: [path.resolve(__dirname, "./src/test-utils/vitest-setup.ts")],
|
setupFiles: [path.resolve(__dirname, "./src/test-utils/vitest-setup.ts")],
|
||||||
pool: "threads",
|
pool: "forks",
|
||||||
poolOptions: {
|
poolOptions: {
|
||||||
threads: {
|
forks: {
|
||||||
singleThread: true,
|
singleFork: false,
|
||||||
|
maxForks: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user