Skip to content

Commit 8719f49

Browse files
committed
WIP
Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent eb8cdff commit 8719f49

File tree

2 files changed

+186
-22
lines changed

2 files changed

+186
-22
lines changed

src/node/services/ipcMain.ts

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,44 +1264,94 @@ export class IpcMain {
12641264
const session = this.getOrCreateSession(workspaceId);
12651265
const signal = session.startScriptExecution();
12661266

1267+
let scriptMessage: MuxMessage | null = null;
1268+
let scriptMessagePersisted = false;
1269+
let scriptExecutionStartMs: number | null = null;
1270+
1271+
const buildScriptFailureResult = (
1272+
errorMessage: string,
1273+
elapsedMs: number
1274+
): BashToolResult => ({
1275+
success: false,
1276+
error: errorMessage,
1277+
output: "",
1278+
exitCode: 1,
1279+
wall_duration_ms: elapsedMs >= 0 ? elapsedMs : 0,
1280+
});
1281+
1282+
const persistScriptResult = async (result: BashToolResult): Promise<void> => {
1283+
assert(
1284+
scriptMessagePersisted,
1285+
"Script message must be appended before persisting result"
1286+
);
1287+
assert(scriptMessage, "Script message must exist before persisting result");
1288+
const metadata = scriptMessage.metadata?.muxMetadata;
1289+
assert(
1290+
metadata?.type === "script-execution",
1291+
"Unexpected script message metadata type"
1292+
);
1293+
1294+
metadata.result = result;
1295+
1296+
const updateResult = await this.historyService.updateHistory(
1297+
workspaceId,
1298+
scriptMessage
1299+
);
1300+
if (!updateResult.success) {
1301+
log.error(
1302+
`Failed to update script execution history for workspace ${workspaceId}: ${updateResult.error}`
1303+
);
1304+
}
1305+
1306+
if (this.mainWindow) {
1307+
const channel = getChatChannel(workspaceId);
1308+
this.mainWindow.webContents.send(channel, scriptMessage);
1309+
}
1310+
};
1311+
12671312
try {
12681313
// 1. Create and persist script execution message immediately
12691314
// This ensures the user sees "Executing script..." in the timeline while it runs
12701315
const command = `/script ${scriptName}${args.length > 0 ? " " + args.join(" ") : ""}`;
1271-
const scriptMessage: MuxMessage = {
1272-
id: `script-${Date.now()}`, // Generate a unique ID
1316+
const scriptMessageId = `script-${Date.now()}`;
1317+
const scriptTimestamp = Date.now();
1318+
const newScriptMessage: MuxMessage = {
1319+
id: scriptMessageId,
12731320
role: "user",
12741321
parts: [{ type: "text", text: `Executed script: ${command}` }],
12751322
metadata: {
1276-
timestamp: Date.now(),
1323+
timestamp: scriptTimestamp,
12771324
muxMetadata: {
12781325
type: "script-execution",
1279-
id: `script-${Date.now()}`, // Can match message ID
1280-
timestamp: Date.now(),
1281-
command: command,
1282-
scriptName: scriptName,
1283-
args: args,
1326+
id: scriptMessageId,
1327+
timestamp: scriptTimestamp,
1328+
command,
1329+
scriptName,
1330+
args,
12841331
},
12851332
},
12861333
};
1334+
scriptMessage = newScriptMessage;
12871335

12881336
const appendResult = await this.historyService.appendToHistory(
12891337
workspaceId,
1290-
scriptMessage
1338+
newScriptMessage
12911339
);
12921340

12931341
if (appendResult.success) {
1342+
scriptMessagePersisted = true;
12941343
// Broadcast the new message to the frontend immediately
12951344
const channel = getChatChannel(workspaceId);
12961345
if (this.mainWindow) {
1297-
this.mainWindow.webContents.send(channel, scriptMessage);
1346+
this.mainWindow.webContents.send(channel, newScriptMessage);
12981347
}
12991348
} else {
13001349
log.error("Failed to persist script execution:", appendResult.error);
13011350
return Err(`Failed to persist script execution: ${appendResult.error}`);
13021351
}
13031352

13041353
// 2. Execute the script
1354+
scriptExecutionStartMs = Date.now();
13051355
const execResult = await runWorkspaceScript(
13061356
runtimeInstance,
13071357
workspacePath,
@@ -1312,8 +1362,11 @@ export class IpcMain {
13121362
300, // timeout
13131363
signal
13141364
);
1365+
const elapsedMs = scriptExecutionStartMs ? Date.now() - scriptExecutionStartMs : 0;
1366+
scriptExecutionStartMs = null;
13151367

13161368
if (!execResult.success) {
1369+
await persistScriptResult(buildScriptFailureResult(execResult.error, elapsedMs));
13171370
return Err(execResult.error);
13181371
}
13191372

@@ -1327,22 +1380,22 @@ export class IpcMain {
13271380
};
13281381

13291382
// 4. Update the history message with the result
1330-
if (scriptMessage.metadata?.muxMetadata?.type === "script-execution") {
1331-
scriptMessage.metadata.muxMetadata.result = enhancedResult;
1332-
}
1333-
1334-
// Update history file
1335-
await this.historyService.updateHistory(workspaceId, scriptMessage);
1336-
1337-
// Broadcast the updated message (this puts the result in the UI)
1338-
if (this.mainWindow) {
1339-
const channel = getChatChannel(workspaceId);
1340-
this.mainWindow.webContents.send(channel, scriptMessage);
1341-
}
1383+
await persistScriptResult(enhancedResult);
13421384

13431385
return Ok(enhancedResult);
13441386
} catch (error) {
13451387
const message = error instanceof Error ? error.message : String(error);
1388+
const elapsedMs = scriptExecutionStartMs ? Date.now() - scriptExecutionStartMs : 0;
1389+
if (scriptMessagePersisted) {
1390+
try {
1391+
await persistScriptResult(buildScriptFailureResult(message, elapsedMs));
1392+
} catch (persistError) {
1393+
log.error(
1394+
`Failed to persist script execution failure result for workspace ${workspaceId}:`,
1395+
persistError
1396+
);
1397+
}
1398+
}
13461399
return Err(`Failed to execute script: ${message}`);
13471400
} finally {
13481401
session.endScriptExecution();
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { createTestEnvironment, cleanupTestEnvironment } from "./setup";
2+
import { createTempGitRepo, cleanupTempGitRepo, createWorkspace, readChatHistory } from "./helpers";
3+
import { IPC_CHANNELS, getChatChannel } from "../../src/common/constants/ipc-constants";
4+
import type { MuxMessage } from "../../src/common/types/message";
5+
6+
const TEST_TIMEOUT_MS = 20000;
7+
8+
describe("WORKSPACE_EXECUTE_SCRIPT failure handling", () => {
9+
test(
10+
"persists a failure result when runWorkspaceScript returns an error",
11+
async () => {
12+
const env = await createTestEnvironment();
13+
const tempGitRepo = await createTempGitRepo();
14+
let workspaceId: string | null = null;
15+
const missingScriptName = "missing-script";
16+
17+
try {
18+
const createResult = await createWorkspace(
19+
env.mockIpcRenderer,
20+
tempGitRepo,
21+
"script-failure"
22+
);
23+
24+
if (!createResult.success) {
25+
throw new Error(`Workspace creation failed: ${createResult.error}`);
26+
}
27+
28+
workspaceId = createResult.metadata.id;
29+
expect(workspaceId).toBeTruthy();
30+
31+
const invocationResult = await env.mockIpcRenderer.invoke(
32+
IPC_CHANNELS.WORKSPACE_EXECUTE_SCRIPT,
33+
workspaceId,
34+
missingScriptName
35+
);
36+
37+
expect(invocationResult.success).toBe(false);
38+
if (invocationResult.success) {
39+
throw new Error("Expected script execution to fail");
40+
}
41+
expect(invocationResult.error).toContain("Script not found");
42+
43+
const chatChannel = getChatChannel(workspaceId);
44+
const scriptMessages = env.sentEvents
45+
.filter((event) => event.channel === chatChannel)
46+
.map((event) => event.data as MuxMessage)
47+
.filter(
48+
(message) =>
49+
message.metadata?.muxMetadata?.type === "script-execution" &&
50+
message.metadata?.muxMetadata?.command?.includes(missingScriptName)
51+
);
52+
53+
expect(scriptMessages.length).toBeGreaterThan(0);
54+
const finalScriptMessage = scriptMessages[scriptMessages.length - 1];
55+
const finalMetadata = finalScriptMessage.metadata?.muxMetadata;
56+
expect(finalMetadata?.type).toBe("script-execution");
57+
if (!finalMetadata || finalMetadata.type !== "script-execution") {
58+
throw new Error("Expected script-execution metadata on final message");
59+
}
60+
const finalResult = finalMetadata.result;
61+
expect(finalResult).toBeDefined();
62+
if (!finalResult) {
63+
throw new Error("Expected script execution result on final message");
64+
}
65+
expect(finalResult.success).toBe(false);
66+
if (finalResult.success !== false) {
67+
throw new Error("Expected script execution to fail");
68+
}
69+
expect(finalResult.error).toContain("Script not found");
70+
71+
const history = (await readChatHistory(env.tempDir, workspaceId)) as Array<
72+
Record<string, any>
73+
>;
74+
const persistedScriptMessage = history
75+
.filter(
76+
(message) =>
77+
message.metadata?.muxMetadata?.type === "script-execution" &&
78+
message.metadata?.muxMetadata?.command?.includes(missingScriptName)
79+
)
80+
.pop();
81+
82+
expect(persistedScriptMessage).toBeDefined();
83+
if (!persistedScriptMessage) {
84+
throw new Error("Expected script execution message to be persisted");
85+
}
86+
const persistedMetadata = persistedScriptMessage.metadata?.muxMetadata;
87+
expect(persistedMetadata?.type).toBe("script-execution");
88+
if (!persistedMetadata || persistedMetadata.type !== "script-execution") {
89+
throw new Error("Expected script-execution metadata in history");
90+
}
91+
const persistedResult = persistedMetadata.result;
92+
expect(persistedResult).toBeDefined();
93+
if (!persistedResult) {
94+
throw new Error("Expected script execution result in history");
95+
}
96+
expect(persistedResult.success).toBe(false);
97+
if (persistedResult.success !== false) {
98+
throw new Error("Expected history result to indicate failure");
99+
}
100+
expect(persistedResult.error).toContain("Script not found");
101+
} finally {
102+
if (workspaceId) {
103+
await env.mockIpcRenderer.invoke(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId);
104+
}
105+
await cleanupTestEnvironment(env);
106+
await cleanupTempGitRepo(tempGitRepo);
107+
}
108+
},
109+
TEST_TIMEOUT_MS
110+
);
111+
});

0 commit comments

Comments
 (0)