@evermore.work/adapter-utils 2026.509.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing.d.ts +2 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +16 -0
- package/dist/billing.js.map +1 -0
- package/dist/billing.test.d.ts +2 -0
- package/dist/billing.test.d.ts.map +1 -0
- package/dist/billing.test.js +14 -0
- package/dist/billing.test.js.map +1 -0
- package/dist/command-managed-runtime.d.ts +45 -0
- package/dist/command-managed-runtime.d.ts.map +1 -0
- package/dist/command-managed-runtime.js +164 -0
- package/dist/command-managed-runtime.js.map +1 -0
- package/dist/command-managed-runtime.test.d.ts +2 -0
- package/dist/command-managed-runtime.test.d.ts.map +1 -0
- package/dist/command-managed-runtime.test.js +102 -0
- package/dist/command-managed-runtime.test.js.map +1 -0
- package/dist/command-redaction.d.ts +3 -0
- package/dist/command-redaction.d.ts.map +1 -0
- package/dist/command-redaction.js +17 -0
- package/dist/command-redaction.js.map +1 -0
- package/dist/execution-target-sandbox.test.d.ts +2 -0
- package/dist/execution-target-sandbox.test.d.ts.map +1 -0
- package/dist/execution-target-sandbox.test.js +392 -0
- package/dist/execution-target-sandbox.test.js.map +1 -0
- package/dist/execution-target.d.ts +150 -0
- package/dist/execution-target.d.ts.map +1 -0
- package/dist/execution-target.js +791 -0
- package/dist/execution-target.js.map +1 -0
- package/dist/execution-target.test.d.ts +2 -0
- package/dist/execution-target.test.d.ts.map +1 -0
- package/dist/execution-target.test.js +314 -0
- package/dist/execution-target.test.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/log-redaction.d.ts +9 -0
- package/dist/log-redaction.d.ts.map +1 -0
- package/dist/log-redaction.js +88 -0
- package/dist/log-redaction.js.map +1 -0
- package/dist/remote-execution-env.d.ts +2 -0
- package/dist/remote-execution-env.d.ts.map +1 -0
- package/dist/remote-execution-env.js +46 -0
- package/dist/remote-execution-env.js.map +1 -0
- package/dist/remote-managed-runtime.d.ts +31 -0
- package/dist/remote-managed-runtime.d.ts.map +1 -0
- package/dist/remote-managed-runtime.js +81 -0
- package/dist/remote-managed-runtime.js.map +1 -0
- package/dist/sandbox-callback-bridge.d.ts +132 -0
- package/dist/sandbox-callback-bridge.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge.js +925 -0
- package/dist/sandbox-callback-bridge.js.map +1 -0
- package/dist/sandbox-callback-bridge.test.d.ts +2 -0
- package/dist/sandbox-callback-bridge.test.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge.test.js +719 -0
- package/dist/sandbox-callback-bridge.test.js.map +1 -0
- package/dist/sandbox-managed-runtime.d.ts +54 -0
- package/dist/sandbox-managed-runtime.d.ts.map +1 -0
- package/dist/sandbox-managed-runtime.js +234 -0
- package/dist/sandbox-managed-runtime.js.map +1 -0
- package/dist/sandbox-managed-runtime.test.d.ts +2 -0
- package/dist/sandbox-managed-runtime.test.d.ts.map +1 -0
- package/dist/sandbox-managed-runtime.test.js +118 -0
- package/dist/sandbox-managed-runtime.test.js.map +1 -0
- package/dist/sandbox-shell.d.ts +2 -0
- package/dist/sandbox-shell.d.ts.map +1 -0
- package/dist/sandbox-shell.js +4 -0
- package/dist/sandbox-shell.js.map +1 -0
- package/dist/server-utils.d.ts +253 -0
- package/dist/server-utils.d.ts.map +1 -0
- package/dist/server-utils.js +1522 -0
- package/dist/server-utils.js.map +1 -0
- package/dist/server-utils.test.d.ts +2 -0
- package/dist/server-utils.test.d.ts.map +1 -0
- package/dist/server-utils.test.js +685 -0
- package/dist/server-utils.test.js.map +1 -0
- package/dist/session-compaction.d.ts +25 -0
- package/dist/session-compaction.d.ts.map +1 -0
- package/dist/session-compaction.js +154 -0
- package/dist/session-compaction.js.map +1 -0
- package/dist/ssh-fixture.test.d.ts +2 -0
- package/dist/ssh-fixture.test.d.ts.map +1 -0
- package/dist/ssh-fixture.test.js +214 -0
- package/dist/ssh-fixture.test.js.map +1 -0
- package/dist/ssh.d.ts +111 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +1098 -0
- package/dist/ssh.js.map +1 -0
- package/dist/types.d.ts +465 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { applyEvermoreWorkspaceEnv, appendWithByteCap, buildInvocationEnvForLogs, DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE, materializeEvermoreSkillCopy, renderEvermoreWakePrompt, runningProcesses, runChildProcess, sanitizeSshRemoteEnv, shapeEvermoreWorkspaceEnvForExecution, stringifyEvermoreWakePayload, } from "./server-utils.js";
|
|
7
|
+
function isPidAlive(pid) {
|
|
8
|
+
try {
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function waitForPidExit(pid, timeoutMs = 2_000) {
|
|
17
|
+
const deadline = Date.now() + timeoutMs;
|
|
18
|
+
while (Date.now() < deadline) {
|
|
19
|
+
if (!isPidAlive(pid))
|
|
20
|
+
return true;
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
22
|
+
}
|
|
23
|
+
return !isPidAlive(pid);
|
|
24
|
+
}
|
|
25
|
+
async function waitForTextMatch(read, pattern, timeoutMs = 1_000) {
|
|
26
|
+
const deadline = Date.now() + timeoutMs;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
const value = read();
|
|
29
|
+
const match = value.match(pattern);
|
|
30
|
+
if (match)
|
|
31
|
+
return match;
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
33
|
+
}
|
|
34
|
+
return read().match(pattern);
|
|
35
|
+
}
|
|
36
|
+
describe("buildInvocationEnvForLogs", () => {
|
|
37
|
+
it("redacts inline secrets from resolved command metadata", () => {
|
|
38
|
+
const loggedEnv = buildInvocationEnvForLogs({ SAFE_VALUE: "visible" }, {
|
|
39
|
+
resolvedCommand: "env OPENAI_API_KEY=sk-live-example custom-acp --token ghp_example_secret",
|
|
40
|
+
});
|
|
41
|
+
expect(loggedEnv.SAFE_VALUE).toBe("visible");
|
|
42
|
+
expect(loggedEnv.EVERMORE_RESOLVED_COMMAND).toBe("env OPENAI_API_KEY=***REDACTED*** custom-acp --token ***REDACTED***");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("sanitizeSshRemoteEnv", () => {
|
|
46
|
+
it("drops inherited host shell identity variables for SSH remote execution", () => {
|
|
47
|
+
expect(sanitizeSshRemoteEnv({
|
|
48
|
+
PATH: "/host/bin:/usr/bin",
|
|
49
|
+
HOME: "/Users/local",
|
|
50
|
+
NVM_DIR: "/Users/local/.nvm",
|
|
51
|
+
TMPDIR: "/var/folders/local/T",
|
|
52
|
+
XDG_CONFIG_HOME: "/Users/local/.config",
|
|
53
|
+
SAFE_VALUE: "visible",
|
|
54
|
+
}, {
|
|
55
|
+
PATH: "/host/bin:/usr/bin",
|
|
56
|
+
HOME: "/Users/local",
|
|
57
|
+
NVM_DIR: "/Users/local/.nvm",
|
|
58
|
+
TMPDIR: "/var/folders/local/T",
|
|
59
|
+
XDG_CONFIG_HOME: "/Users/local/.config",
|
|
60
|
+
})).toEqual({
|
|
61
|
+
SAFE_VALUE: "visible",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it("preserves explicit remote overrides even for filtered key names", () => {
|
|
65
|
+
expect(sanitizeSshRemoteEnv({
|
|
66
|
+
PATH: "/custom/remote/bin:/usr/bin",
|
|
67
|
+
HOME: "/home/agent",
|
|
68
|
+
TMPDIR: "/tmp",
|
|
69
|
+
SAFE_VALUE: "visible",
|
|
70
|
+
}, {
|
|
71
|
+
PATH: "/host/bin:/usr/bin",
|
|
72
|
+
HOME: "/Users/local",
|
|
73
|
+
TMPDIR: "/var/folders/local/T",
|
|
74
|
+
})).toEqual({
|
|
75
|
+
PATH: "/custom/remote/bin:/usr/bin",
|
|
76
|
+
HOME: "/home/agent",
|
|
77
|
+
TMPDIR: "/tmp",
|
|
78
|
+
SAFE_VALUE: "visible",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it("filters identity keys via case-insensitive match against the inherited env", () => {
|
|
82
|
+
expect(sanitizeSshRemoteEnv({
|
|
83
|
+
// Caller passed PATH in upper case while the inherited (Windows-style)
|
|
84
|
+
// host env exposes it as Path. The lookup must still treat them as
|
|
85
|
+
// equal so the leaked host PATH gets stripped.
|
|
86
|
+
PATH: "/host/bin:/usr/bin",
|
|
87
|
+
HOME: "/host/home",
|
|
88
|
+
}, {
|
|
89
|
+
Path: "/host/bin:/usr/bin",
|
|
90
|
+
home: "/host/home",
|
|
91
|
+
})).toEqual({});
|
|
92
|
+
});
|
|
93
|
+
it("preserves explicitly-set identity keys when the inherited env disagrees in case but not in value", () => {
|
|
94
|
+
expect(sanitizeSshRemoteEnv({
|
|
95
|
+
PATH: "/explicit/remote/bin",
|
|
96
|
+
}, {
|
|
97
|
+
Path: "/host/bin:/usr/bin",
|
|
98
|
+
})).toEqual({ PATH: "/explicit/remote/bin" });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("materializeEvermoreSkillCopy", () => {
|
|
102
|
+
it("refuses to materialize into an ancestor of the source", async () => {
|
|
103
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "evermore-skill-copy-"));
|
|
104
|
+
try {
|
|
105
|
+
const source = path.join(root, "parent", "skill");
|
|
106
|
+
await fs.mkdir(source, { recursive: true });
|
|
107
|
+
await fs.writeFile(path.join(source, "SKILL.md"), "# skill\n", "utf8");
|
|
108
|
+
await expect(materializeEvermoreSkillCopy(source, path.join(root, "parent"))).rejects.toThrow(/ancestor/);
|
|
109
|
+
await expect(fs.readFile(path.join(source, "SKILL.md"), "utf8")).resolves.toBe("# skill\n");
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
it("does not delete and recopy an unchanged materialized skill target", async () => {
|
|
116
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "evermore-skill-copy-"));
|
|
117
|
+
try {
|
|
118
|
+
const source = path.join(root, "source");
|
|
119
|
+
const target = path.join(root, "target");
|
|
120
|
+
await fs.mkdir(source, { recursive: true });
|
|
121
|
+
await fs.writeFile(path.join(source, "SKILL.md"), "# skill\n", "utf8");
|
|
122
|
+
const first = await materializeEvermoreSkillCopy(source, target);
|
|
123
|
+
expect(first.copiedFiles).toBe(1);
|
|
124
|
+
await fs.writeFile(path.join(target, "local-marker.txt"), "keep\n", "utf8");
|
|
125
|
+
const second = await materializeEvermoreSkillCopy(source, target);
|
|
126
|
+
expect(second.copiedFiles).toBe(0);
|
|
127
|
+
await expect(fs.readFile(path.join(target, "local-marker.txt"), "utf8")).resolves.toBe("keep\n");
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
it("breaks stale materialization locks left by dead processes", async () => {
|
|
134
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "evermore-skill-copy-"));
|
|
135
|
+
try {
|
|
136
|
+
const source = path.join(root, "source");
|
|
137
|
+
const target = path.join(root, "target");
|
|
138
|
+
const lock = `${target}.lock`;
|
|
139
|
+
await fs.mkdir(source, { recursive: true });
|
|
140
|
+
await fs.writeFile(path.join(source, "SKILL.md"), "# skill\n", "utf8");
|
|
141
|
+
await fs.mkdir(lock, { recursive: true });
|
|
142
|
+
await fs.writeFile(path.join(lock, "owner.json"), JSON.stringify({ pid: 999_999_999, createdAt: "2000-01-01T00:00:00.000Z" }), "utf8");
|
|
143
|
+
await expect(materializeEvermoreSkillCopy(source, target)).resolves.toMatchObject({ copiedFiles: 1 });
|
|
144
|
+
await expect(fs.readFile(path.join(target, "SKILL.md"), "utf8")).resolves.toBe("# skill\n");
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("runChildProcess", () => {
|
|
152
|
+
it("does not arm a timeout when timeoutSec is 0", async () => {
|
|
153
|
+
const result = await runChildProcess(randomUUID(), process.execPath, ["-e", "setTimeout(() => process.stdout.write('done'), 150);"], {
|
|
154
|
+
cwd: process.cwd(),
|
|
155
|
+
env: {},
|
|
156
|
+
timeoutSec: 0,
|
|
157
|
+
graceSec: 1,
|
|
158
|
+
onLog: async () => { },
|
|
159
|
+
});
|
|
160
|
+
expect(result.exitCode).toBe(0);
|
|
161
|
+
expect(result.timedOut).toBe(false);
|
|
162
|
+
expect(result.stdout).toBe("done");
|
|
163
|
+
});
|
|
164
|
+
it("waits for onSpawn before sending stdin to the child", async () => {
|
|
165
|
+
const spawnDelayMs = 150;
|
|
166
|
+
const startedAt = Date.now();
|
|
167
|
+
let onSpawnCompletedAt = 0;
|
|
168
|
+
const result = await runChildProcess(randomUUID(), process.execPath, [
|
|
169
|
+
"-e",
|
|
170
|
+
"let data='';process.stdin.setEncoding('utf8');process.stdin.on('data',chunk=>data+=chunk);process.stdin.on('end',()=>process.stdout.write(data));",
|
|
171
|
+
], {
|
|
172
|
+
cwd: process.cwd(),
|
|
173
|
+
env: {},
|
|
174
|
+
stdin: "hello from stdin",
|
|
175
|
+
timeoutSec: 5,
|
|
176
|
+
graceSec: 1,
|
|
177
|
+
onLog: async () => { },
|
|
178
|
+
onSpawn: async () => {
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, spawnDelayMs));
|
|
180
|
+
onSpawnCompletedAt = Date.now();
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const finishedAt = Date.now();
|
|
184
|
+
expect(result.exitCode).toBe(0);
|
|
185
|
+
expect(result.stdout).toBe("hello from stdin");
|
|
186
|
+
expect(onSpawnCompletedAt).toBeGreaterThanOrEqual(startedAt + spawnDelayMs);
|
|
187
|
+
expect(finishedAt - startedAt).toBeGreaterThanOrEqual(spawnDelayMs);
|
|
188
|
+
});
|
|
189
|
+
it.skipIf(process.platform === "win32")("kills descendant processes on timeout via the process group", async () => {
|
|
190
|
+
let descendantPid = null;
|
|
191
|
+
const result = await runChildProcess(randomUUID(), process.execPath, [
|
|
192
|
+
"-e",
|
|
193
|
+
[
|
|
194
|
+
"const { spawn } = require('node:child_process');",
|
|
195
|
+
"const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' });",
|
|
196
|
+
"process.stdout.write(String(child.pid));",
|
|
197
|
+
"setInterval(() => {}, 1000);",
|
|
198
|
+
].join(" "),
|
|
199
|
+
], {
|
|
200
|
+
cwd: process.cwd(),
|
|
201
|
+
env: {},
|
|
202
|
+
timeoutSec: 1,
|
|
203
|
+
graceSec: 1,
|
|
204
|
+
onLog: async () => { },
|
|
205
|
+
onSpawn: async () => { },
|
|
206
|
+
});
|
|
207
|
+
descendantPid = Number.parseInt(result.stdout.trim(), 10);
|
|
208
|
+
expect(result.timedOut).toBe(true);
|
|
209
|
+
expect(Number.isInteger(descendantPid) && descendantPid > 0).toBe(true);
|
|
210
|
+
expect(await waitForPidExit(descendantPid, 2_000)).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
it.skipIf(process.platform === "win32")("cleans up a lingering process group after terminal output and child exit", async () => {
|
|
213
|
+
const result = await runChildProcess(randomUUID(), process.execPath, [
|
|
214
|
+
"-e",
|
|
215
|
+
[
|
|
216
|
+
"const { spawn } = require('node:child_process');",
|
|
217
|
+
"const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: ['ignore', 'inherit', 'ignore'] });",
|
|
218
|
+
"process.stdout.write(`descendant:${child.pid}\\n`);",
|
|
219
|
+
"process.stdout.write(`${JSON.stringify({ type: 'result', result: 'done' })}\\n`);",
|
|
220
|
+
"setTimeout(() => process.exit(0), 25);",
|
|
221
|
+
].join(" "),
|
|
222
|
+
], {
|
|
223
|
+
cwd: process.cwd(),
|
|
224
|
+
env: {},
|
|
225
|
+
timeoutSec: 0,
|
|
226
|
+
graceSec: 1,
|
|
227
|
+
onLog: async () => { },
|
|
228
|
+
terminalResultCleanup: {
|
|
229
|
+
graceMs: 100,
|
|
230
|
+
hasTerminalResult: ({ stdout }) => stdout.includes('"type":"result"'),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
const descendantPid = Number.parseInt(result.stdout.match(/descendant:(\d+)/)?.[1] ?? "", 10);
|
|
234
|
+
expect(result.timedOut).toBe(false);
|
|
235
|
+
expect(result.exitCode).toBe(0);
|
|
236
|
+
expect(Number.isInteger(descendantPid) && descendantPid > 0).toBe(true);
|
|
237
|
+
expect(await waitForPidExit(descendantPid, 2_000)).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
it.skipIf(process.platform === "win32")("cleans up a still-running child after terminal output", async () => {
|
|
240
|
+
const result = await runChildProcess(randomUUID(), process.execPath, [
|
|
241
|
+
"-e",
|
|
242
|
+
[
|
|
243
|
+
"process.stdout.write(`${JSON.stringify({ type: 'result', result: 'done' })}\\n`);",
|
|
244
|
+
"setInterval(() => {}, 1000);",
|
|
245
|
+
].join(" "),
|
|
246
|
+
], {
|
|
247
|
+
cwd: process.cwd(),
|
|
248
|
+
env: {},
|
|
249
|
+
timeoutSec: 0,
|
|
250
|
+
graceSec: 1,
|
|
251
|
+
onLog: async () => { },
|
|
252
|
+
terminalResultCleanup: {
|
|
253
|
+
graceMs: 100,
|
|
254
|
+
hasTerminalResult: ({ stdout }) => stdout.includes('"type":"result"'),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
expect(result.timedOut).toBe(false);
|
|
258
|
+
expect(result.signal).toBe("SIGTERM");
|
|
259
|
+
expect(result.stdout).toContain('"type":"result"');
|
|
260
|
+
});
|
|
261
|
+
it.skipIf(process.platform === "win32")("does not clean up noisy runs that have no terminal output", async () => {
|
|
262
|
+
const runId = randomUUID();
|
|
263
|
+
let observed = "";
|
|
264
|
+
const resultPromise = runChildProcess(runId, process.execPath, [
|
|
265
|
+
"-e",
|
|
266
|
+
[
|
|
267
|
+
"const { spawn } = require('node:child_process');",
|
|
268
|
+
"const child = spawn(process.execPath, ['-e', \"setInterval(() => process.stdout.write('noise\\\\n'), 50)\"], { stdio: ['ignore', 'inherit', 'ignore'] });",
|
|
269
|
+
"process.stdout.write(`descendant:${child.pid}\\n`);",
|
|
270
|
+
"setTimeout(() => process.exit(0), 25);",
|
|
271
|
+
].join(" "),
|
|
272
|
+
], {
|
|
273
|
+
cwd: process.cwd(),
|
|
274
|
+
env: {},
|
|
275
|
+
timeoutSec: 0,
|
|
276
|
+
graceSec: 1,
|
|
277
|
+
onLog: async (_stream, chunk) => {
|
|
278
|
+
observed += chunk;
|
|
279
|
+
},
|
|
280
|
+
terminalResultCleanup: {
|
|
281
|
+
graceMs: 50,
|
|
282
|
+
hasTerminalResult: ({ stdout }) => stdout.includes('"type":"result"'),
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const pidMatch = await waitForTextMatch(() => observed, /descendant:(\d+)/);
|
|
286
|
+
const descendantPid = Number.parseInt(pidMatch?.[1] ?? "", 10);
|
|
287
|
+
expect(Number.isInteger(descendantPid) && descendantPid > 0).toBe(true);
|
|
288
|
+
const race = await Promise.race([
|
|
289
|
+
resultPromise.then(() => "settled"),
|
|
290
|
+
new Promise((resolve) => setTimeout(() => resolve("pending"), 300)),
|
|
291
|
+
]);
|
|
292
|
+
expect(race).toBe("pending");
|
|
293
|
+
expect(isPidAlive(descendantPid)).toBe(true);
|
|
294
|
+
const running = runningProcesses.get(runId);
|
|
295
|
+
try {
|
|
296
|
+
if (running?.processGroupId) {
|
|
297
|
+
process.kill(-running.processGroupId, "SIGKILL");
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
running?.child.kill("SIGKILL");
|
|
301
|
+
}
|
|
302
|
+
await resultPromise;
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
runningProcesses.delete(runId);
|
|
306
|
+
if (isPidAlive(descendantPid)) {
|
|
307
|
+
try {
|
|
308
|
+
process.kill(descendantPid, "SIGKILL");
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Ignore cleanup races.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
describe("renderEvermoreWakePrompt", () => {
|
|
318
|
+
it("keeps the default local-agent prompt action-oriented", () => {
|
|
319
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("Start actionable work in this heartbeat");
|
|
320
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("do not stop at a plan");
|
|
321
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("clear final disposition");
|
|
322
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("evidence, not valid liveness paths by themselves");
|
|
323
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("keep `in_progress` only when a live continuation path exists");
|
|
324
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("Prefer the smallest verification that proves the change");
|
|
325
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("Use child issues");
|
|
326
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("instead of polling agents, sessions, or processes");
|
|
327
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("Create child issues directly when you know what needs to be done");
|
|
328
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("POST /api/issues/{issueId}/interactions");
|
|
329
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("kind suggest_tasks, ask_user_questions, or request_confirmation");
|
|
330
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("confirmation:{issueId}:plan:{revisionId}");
|
|
331
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("Wait for acceptance before creating implementation subtasks");
|
|
332
|
+
expect(DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE).toContain("Respect budget, pause/cancel, approval gates, and company boundaries");
|
|
333
|
+
});
|
|
334
|
+
it("adds the execution contract to scoped wake prompts", () => {
|
|
335
|
+
const prompt = renderEvermoreWakePrompt({
|
|
336
|
+
reason: "issue_assigned",
|
|
337
|
+
issue: {
|
|
338
|
+
id: "issue-1",
|
|
339
|
+
identifier: "EVR-1580",
|
|
340
|
+
title: "Update prompts",
|
|
341
|
+
status: "in_progress",
|
|
342
|
+
},
|
|
343
|
+
commentWindow: {
|
|
344
|
+
requestedCount: 0,
|
|
345
|
+
includedCount: 0,
|
|
346
|
+
missingCount: 0,
|
|
347
|
+
},
|
|
348
|
+
comments: [],
|
|
349
|
+
fallbackFetchNeeded: false,
|
|
350
|
+
});
|
|
351
|
+
expect(prompt).toContain("## Evermore Wake Payload");
|
|
352
|
+
expect(prompt).toContain("Execution contract: take concrete action in this heartbeat");
|
|
353
|
+
expect(prompt).toContain("clear final disposition");
|
|
354
|
+
expect(prompt).toContain("evidence, not valid liveness paths by themselves");
|
|
355
|
+
expect(prompt).toContain("Use child issues for long or parallel delegated work instead of polling");
|
|
356
|
+
expect(prompt).toContain("named unblock owner/action");
|
|
357
|
+
});
|
|
358
|
+
it("renders planning-mode directives for assignment and comment wakes", () => {
|
|
359
|
+
const assignmentPrompt = renderEvermoreWakePrompt({
|
|
360
|
+
reason: "issue_assigned",
|
|
361
|
+
issue: {
|
|
362
|
+
id: "issue-1",
|
|
363
|
+
identifier: "EVR-3404",
|
|
364
|
+
title: "Plan first",
|
|
365
|
+
status: "in_progress",
|
|
366
|
+
workMode: "planning",
|
|
367
|
+
},
|
|
368
|
+
commentWindow: { requestedCount: 0, includedCount: 0, missingCount: 0 },
|
|
369
|
+
comments: [],
|
|
370
|
+
fallbackFetchNeeded: false,
|
|
371
|
+
});
|
|
372
|
+
expect(assignmentPrompt).toContain("- issue work mode: planning");
|
|
373
|
+
expect(assignmentPrompt).toContain("Make the plan only. Do not write code or perform implementation work.");
|
|
374
|
+
const commentPrompt = renderEvermoreWakePrompt({
|
|
375
|
+
reason: "issue_commented",
|
|
376
|
+
issue: {
|
|
377
|
+
id: "issue-1",
|
|
378
|
+
identifier: "EVR-3404",
|
|
379
|
+
title: "Plan first",
|
|
380
|
+
status: "in_progress",
|
|
381
|
+
workMode: "planning",
|
|
382
|
+
},
|
|
383
|
+
commentIds: ["comment-1"],
|
|
384
|
+
latestCommentId: "comment-1",
|
|
385
|
+
commentWindow: { requestedCount: 1, includedCount: 1, missingCount: 0 },
|
|
386
|
+
comments: [{ id: "comment-1", body: "Revise the plan" }],
|
|
387
|
+
fallbackFetchNeeded: false,
|
|
388
|
+
});
|
|
389
|
+
expect(commentPrompt).toContain("Update the plan only. Do not write code or perform implementation work.");
|
|
390
|
+
});
|
|
391
|
+
it("does not render stale accepted-plan continuation guidance for later planning comment wakes", () => {
|
|
392
|
+
const prompt = renderEvermoreWakePrompt({
|
|
393
|
+
reason: "issue_commented",
|
|
394
|
+
issue: {
|
|
395
|
+
id: "issue-1",
|
|
396
|
+
identifier: "EVR-3404",
|
|
397
|
+
title: "Plan first",
|
|
398
|
+
status: "in_progress",
|
|
399
|
+
workMode: "planning",
|
|
400
|
+
},
|
|
401
|
+
interactionKind: "request_confirmation",
|
|
402
|
+
interactionStatus: "accepted",
|
|
403
|
+
commentIds: ["comment-1"],
|
|
404
|
+
latestCommentId: "comment-1",
|
|
405
|
+
commentWindow: { requestedCount: 1, includedCount: 1, missingCount: 0 },
|
|
406
|
+
comments: [{ id: "comment-1", body: "Revise the plan" }],
|
|
407
|
+
fallbackFetchNeeded: false,
|
|
408
|
+
});
|
|
409
|
+
expect(prompt).toContain("Update the plan only. Do not write code or perform implementation work.");
|
|
410
|
+
expect(prompt).not.toContain("accepted-plan continuation");
|
|
411
|
+
expect(prompt).not.toContain("Create child issues from the approved plan only");
|
|
412
|
+
});
|
|
413
|
+
it("renders accepted-plan continuation guidance for planning issues", () => {
|
|
414
|
+
const prompt = renderEvermoreWakePrompt({
|
|
415
|
+
reason: "issue_commented",
|
|
416
|
+
issue: {
|
|
417
|
+
id: "issue-1",
|
|
418
|
+
identifier: "EVR-3404",
|
|
419
|
+
title: "Plan first",
|
|
420
|
+
status: "in_progress",
|
|
421
|
+
workMode: "planning",
|
|
422
|
+
},
|
|
423
|
+
interactionKind: "request_confirmation",
|
|
424
|
+
interactionStatus: "accepted",
|
|
425
|
+
commentWindow: { requestedCount: 0, includedCount: 0, missingCount: 0 },
|
|
426
|
+
comments: [],
|
|
427
|
+
fallbackFetchNeeded: false,
|
|
428
|
+
});
|
|
429
|
+
expect(prompt).toContain("accepted-plan continuation");
|
|
430
|
+
expect(prompt).toContain("Create child issues from the approved plan only");
|
|
431
|
+
expect(prompt).toContain("may create child implementation issues");
|
|
432
|
+
expect(prompt).toContain("must not start implementation work on the planning issue itself");
|
|
433
|
+
});
|
|
434
|
+
it("keeps accepted-plan guidance when stale comment ids have no loaded comments", () => {
|
|
435
|
+
const prompt = renderEvermoreWakePrompt({
|
|
436
|
+
reason: "issue_commented",
|
|
437
|
+
issue: {
|
|
438
|
+
id: "issue-1",
|
|
439
|
+
identifier: "EVR-3404",
|
|
440
|
+
title: "Plan first",
|
|
441
|
+
status: "in_progress",
|
|
442
|
+
workMode: "planning",
|
|
443
|
+
},
|
|
444
|
+
interactionKind: "request_confirmation",
|
|
445
|
+
interactionStatus: "accepted",
|
|
446
|
+
commentIds: ["stale-comment-1"],
|
|
447
|
+
latestCommentId: "stale-comment-1",
|
|
448
|
+
commentWindow: { requestedCount: 1, includedCount: 0, missingCount: 1 },
|
|
449
|
+
comments: [],
|
|
450
|
+
fallbackFetchNeeded: true,
|
|
451
|
+
});
|
|
452
|
+
expect(prompt).toContain("accepted-plan continuation");
|
|
453
|
+
expect(prompt).toContain("Create child issues from the approved plan only");
|
|
454
|
+
expect(prompt).not.toContain("Update the plan only");
|
|
455
|
+
});
|
|
456
|
+
it("renders dependency-blocked interaction guidance", () => {
|
|
457
|
+
const prompt = renderEvermoreWakePrompt({
|
|
458
|
+
reason: "issue_commented",
|
|
459
|
+
issue: {
|
|
460
|
+
id: "issue-1",
|
|
461
|
+
identifier: "EVR-1703",
|
|
462
|
+
title: "Blocked parent",
|
|
463
|
+
status: "todo",
|
|
464
|
+
},
|
|
465
|
+
dependencyBlockedInteraction: true,
|
|
466
|
+
unresolvedBlockerIssueIds: ["blocker-1"],
|
|
467
|
+
unresolvedBlockerSummaries: [
|
|
468
|
+
{
|
|
469
|
+
id: "blocker-1",
|
|
470
|
+
identifier: "EVR-1723",
|
|
471
|
+
title: "Finish blocker",
|
|
472
|
+
status: "todo",
|
|
473
|
+
priority: "medium",
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
commentWindow: {
|
|
477
|
+
requestedCount: 1,
|
|
478
|
+
includedCount: 1,
|
|
479
|
+
missingCount: 0,
|
|
480
|
+
},
|
|
481
|
+
commentIds: ["comment-1"],
|
|
482
|
+
latestCommentId: "comment-1",
|
|
483
|
+
comments: [{ id: "comment-1", body: "hello" }],
|
|
484
|
+
fallbackFetchNeeded: false,
|
|
485
|
+
});
|
|
486
|
+
expect(prompt).toContain("dependency-blocked interaction: yes");
|
|
487
|
+
expect(prompt).toContain("respond or triage the human comment");
|
|
488
|
+
expect(prompt).toContain("EVR-1723 Finish blocker (todo)");
|
|
489
|
+
});
|
|
490
|
+
it("renders loose review request instructions for execution handoffs", () => {
|
|
491
|
+
const prompt = renderEvermoreWakePrompt({
|
|
492
|
+
reason: "execution_review_requested",
|
|
493
|
+
issue: {
|
|
494
|
+
id: "issue-1",
|
|
495
|
+
identifier: "EVR-2011",
|
|
496
|
+
title: "Review request handoff",
|
|
497
|
+
status: "in_review",
|
|
498
|
+
},
|
|
499
|
+
executionStage: {
|
|
500
|
+
wakeRole: "reviewer",
|
|
501
|
+
stageId: "stage-1",
|
|
502
|
+
stageType: "review",
|
|
503
|
+
currentParticipant: { type: "agent", agentId: "agent-1" },
|
|
504
|
+
returnAssignee: { type: "agent", agentId: "agent-2" },
|
|
505
|
+
reviewRequest: {
|
|
506
|
+
instructions: "Please focus on edge cases and leave a short risk summary.",
|
|
507
|
+
},
|
|
508
|
+
allowedActions: ["approve", "request_changes"],
|
|
509
|
+
},
|
|
510
|
+
fallbackFetchNeeded: false,
|
|
511
|
+
});
|
|
512
|
+
expect(prompt).toContain("Review request instructions:");
|
|
513
|
+
expect(prompt).toContain("Please focus on edge cases and leave a short risk summary.");
|
|
514
|
+
expect(prompt).toContain("You are waking as the active reviewer for this issue.");
|
|
515
|
+
});
|
|
516
|
+
it("includes continuation and child issue summaries in structured wake context", () => {
|
|
517
|
+
const payload = {
|
|
518
|
+
reason: "issue_children_completed",
|
|
519
|
+
issue: {
|
|
520
|
+
id: "parent-1",
|
|
521
|
+
identifier: "EVR-100",
|
|
522
|
+
title: "Integrate child work",
|
|
523
|
+
status: "in_progress",
|
|
524
|
+
priority: "medium",
|
|
525
|
+
},
|
|
526
|
+
continuationSummary: {
|
|
527
|
+
key: "continuation-summary",
|
|
528
|
+
title: "Continuation Summary",
|
|
529
|
+
body: "# Continuation Summary\n\n## Next Action\n\n- Integrate child outputs.",
|
|
530
|
+
updatedAt: "2026-04-18T12:00:00.000Z",
|
|
531
|
+
},
|
|
532
|
+
livenessContinuation: {
|
|
533
|
+
attempt: 2,
|
|
534
|
+
maxAttempts: 2,
|
|
535
|
+
sourceRunId: "run-1",
|
|
536
|
+
state: "plan_only",
|
|
537
|
+
reason: "Run described future work without concrete action evidence",
|
|
538
|
+
instruction: "Take the first concrete action now.",
|
|
539
|
+
},
|
|
540
|
+
childIssueSummaries: [
|
|
541
|
+
{
|
|
542
|
+
id: "child-1",
|
|
543
|
+
identifier: "EVR-101",
|
|
544
|
+
title: "Implement helper",
|
|
545
|
+
status: "done",
|
|
546
|
+
priority: "medium",
|
|
547
|
+
summary: "Added the helper route and tests.",
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
};
|
|
551
|
+
expect(JSON.parse(stringifyEvermoreWakePayload(payload) ?? "{}")).toMatchObject({
|
|
552
|
+
continuationSummary: {
|
|
553
|
+
body: expect.stringContaining("Continuation Summary"),
|
|
554
|
+
},
|
|
555
|
+
livenessContinuation: {
|
|
556
|
+
attempt: 2,
|
|
557
|
+
maxAttempts: 2,
|
|
558
|
+
sourceRunId: "run-1",
|
|
559
|
+
state: "plan_only",
|
|
560
|
+
instruction: "Take the first concrete action now.",
|
|
561
|
+
},
|
|
562
|
+
childIssueSummaries: [
|
|
563
|
+
{
|
|
564
|
+
identifier: "EVR-101",
|
|
565
|
+
summary: "Added the helper route and tests.",
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
});
|
|
569
|
+
const prompt = renderEvermoreWakePrompt(payload);
|
|
570
|
+
expect(prompt).toContain("Issue continuation summary:");
|
|
571
|
+
expect(prompt).toContain("Integrate child outputs.");
|
|
572
|
+
expect(prompt).toContain("Run liveness continuation:");
|
|
573
|
+
expect(prompt).toContain("- attempt: 2/2");
|
|
574
|
+
expect(prompt).toContain("- source run: run-1");
|
|
575
|
+
expect(prompt).toContain("- liveness state: plan_only");
|
|
576
|
+
expect(prompt).toContain("- reason: Run described future work without concrete action evidence");
|
|
577
|
+
expect(prompt).toContain("- instruction: Take the first concrete action now.");
|
|
578
|
+
expect(prompt).toContain("Direct child issue summaries:");
|
|
579
|
+
expect(prompt).toContain("EVR-101 Implement helper (done)");
|
|
580
|
+
expect(prompt).toContain("Added the helper route and tests.");
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
describe("applyEvermoreWorkspaceEnv", () => {
|
|
584
|
+
it("adds shared workspace env vars including AGENT_HOME", () => {
|
|
585
|
+
const env = applyEvermoreWorkspaceEnv({}, {
|
|
586
|
+
workspaceCwd: "/tmp/workspace",
|
|
587
|
+
workspaceSource: "project_primary",
|
|
588
|
+
workspaceStrategy: "git_worktree",
|
|
589
|
+
workspaceId: "workspace-1",
|
|
590
|
+
workspaceRepoUrl: "https://github.com/phuctm97/evermore.git",
|
|
591
|
+
workspaceRepoRef: "main",
|
|
592
|
+
workspaceBranch: "feature/test",
|
|
593
|
+
workspaceWorktreePath: "/tmp/worktree",
|
|
594
|
+
agentHome: "/tmp/agent-home",
|
|
595
|
+
});
|
|
596
|
+
expect(env).toEqual({
|
|
597
|
+
EVERMORE_WORKSPACE_CWD: "/tmp/workspace",
|
|
598
|
+
EVERMORE_WORKSPACE_SOURCE: "project_primary",
|
|
599
|
+
EVERMORE_WORKSPACE_STRATEGY: "git_worktree",
|
|
600
|
+
EVERMORE_WORKSPACE_ID: "workspace-1",
|
|
601
|
+
EVERMORE_WORKSPACE_REPO_URL: "https://github.com/phuctm97/evermore.git",
|
|
602
|
+
EVERMORE_WORKSPACE_REPO_REF: "main",
|
|
603
|
+
EVERMORE_WORKSPACE_BRANCH: "feature/test",
|
|
604
|
+
EVERMORE_WORKSPACE_WORKTREE_PATH: "/tmp/worktree",
|
|
605
|
+
AGENT_HOME: "/tmp/agent-home",
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
it("skips empty workspace env values", () => {
|
|
609
|
+
const env = applyEvermoreWorkspaceEnv({}, {
|
|
610
|
+
workspaceCwd: "",
|
|
611
|
+
workspaceSource: null,
|
|
612
|
+
agentHome: "",
|
|
613
|
+
});
|
|
614
|
+
expect(env).toEqual({});
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
describe("shapeEvermoreWorkspaceEnvForExecution", () => {
|
|
618
|
+
it("rewrites workspace env paths for remote execution", () => {
|
|
619
|
+
const shaped = shapeEvermoreWorkspaceEnvForExecution({
|
|
620
|
+
workspaceCwd: "/tmp/workspace",
|
|
621
|
+
workspaceWorktreePath: "/tmp/worktree",
|
|
622
|
+
workspaceHints: [
|
|
623
|
+
{
|
|
624
|
+
workspaceId: "workspace-1",
|
|
625
|
+
cwd: "/tmp/workspace",
|
|
626
|
+
repoUrl: "https://github.com/phuctm97/evermore.git",
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
workspaceId: "workspace-2",
|
|
630
|
+
cwd: "/tmp/other-workspace",
|
|
631
|
+
repoUrl: "https://github.com/phuctm97/evermore.git",
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
workspaceId: "workspace-3",
|
|
635
|
+
repoUrl: "https://github.com/phuctm97/evermore.git",
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
executionTargetIsRemote: true,
|
|
639
|
+
executionCwd: "/remote/workspace",
|
|
640
|
+
});
|
|
641
|
+
expect(shaped).toEqual({
|
|
642
|
+
workspaceCwd: "/remote/workspace",
|
|
643
|
+
workspaceWorktreePath: null,
|
|
644
|
+
workspaceHints: [
|
|
645
|
+
{
|
|
646
|
+
workspaceId: "workspace-1",
|
|
647
|
+
cwd: "/remote/workspace",
|
|
648
|
+
repoUrl: "https://github.com/phuctm97/evermore.git",
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
workspaceId: "workspace-2",
|
|
652
|
+
repoUrl: "https://github.com/phuctm97/evermore.git",
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
workspaceId: "workspace-3",
|
|
656
|
+
repoUrl: "https://github.com/phuctm97/evermore.git",
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
it("leaves local execution workspace paths unchanged", () => {
|
|
662
|
+
const workspaceHints = [{ workspaceId: "workspace-1", cwd: "/tmp/workspace" }];
|
|
663
|
+
const shaped = shapeEvermoreWorkspaceEnvForExecution({
|
|
664
|
+
workspaceCwd: "/tmp/workspace",
|
|
665
|
+
workspaceWorktreePath: "/tmp/worktree",
|
|
666
|
+
workspaceHints,
|
|
667
|
+
executionTargetIsRemote: false,
|
|
668
|
+
executionCwd: "/remote/workspace",
|
|
669
|
+
});
|
|
670
|
+
expect(shaped).toEqual({
|
|
671
|
+
workspaceCwd: "/tmp/workspace",
|
|
672
|
+
workspaceWorktreePath: "/tmp/worktree",
|
|
673
|
+
workspaceHints,
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
describe("appendWithByteCap", () => {
|
|
678
|
+
it("keeps valid UTF-8 when trimming through multibyte text", () => {
|
|
679
|
+
const output = appendWithByteCap("prefix ", "hello — world", 7);
|
|
680
|
+
expect(output).not.toContain("\uFFFD");
|
|
681
|
+
expect(Buffer.from(output, "utf8").toString("utf8")).toBe(output);
|
|
682
|
+
expect(Buffer.byteLength(output, "utf8")).toBeLessThanOrEqual(7);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
//# sourceMappingURL=server-utils.test.js.map
|