Skip to content

Commit 4d6ac3c

Browse files
committed
test(runner): replay the concurrent-approval incident end to end
1 parent 4cdc95b commit 4d6ac3c

2 files changed

Lines changed: 414 additions & 2 deletions

File tree

services/runner/tests/unit/session-keepalive-approval.test.ts

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,348 @@ describe("runTurn: real approval park + respondPermission resume", () => {
17741774
await env.destroy();
17751775
});
17761776

1777+
it("replays the concurrent-approval incident with deferred closure and exactly-once real results", async () => {
1778+
const { calls, deps, captured } = pausableHarness();
1779+
deps.createOtel = createSandboxAgentOtel as any;
1780+
const incidentRequest: AgentRunRequest = {
1781+
...engineReq,
1782+
permissions: { default: "ask" },
1783+
customTools: [
1784+
{ name: "tool-a", permission: "ask" },
1785+
{ name: "tool-b", permission: "ask" },
1786+
],
1787+
messages: [{ role: "user", content: "run both writes in parallel" }],
1788+
};
1789+
const acquired = await acquireEnvironment(incidentRequest, deps);
1790+
assert.equal(acquired.ok, true);
1791+
if (!acquired.ok) return;
1792+
const env = acquired.env;
1793+
1794+
const firstTurn = runTurn(
1795+
env,
1796+
incidentRequest,
1797+
undefined,
1798+
undefined,
1799+
{ approvalParkMode: true },
1800+
);
1801+
await flush();
1802+
captured.onEvent!(
1803+
updateEvent({
1804+
sessionUpdate: "tool_call",
1805+
toolCallId: "tool-a",
1806+
title: "tool-a",
1807+
rawInput: { target: "a" },
1808+
}),
1809+
);
1810+
captured.onEvent!(
1811+
updateEvent({
1812+
sessionUpdate: "tool_call",
1813+
toolCallId: "tool-b",
1814+
title: "tool-b",
1815+
rawInput: { target: "b" },
1816+
}),
1817+
);
1818+
captured.onPermissionRequest!({
1819+
id: "permission-a",
1820+
availableReplies: ["once", "reject"],
1821+
toolCall: {
1822+
toolCallId: "tool-a",
1823+
name: "tool-a",
1824+
rawInput: { target: "a" },
1825+
},
1826+
});
1827+
for (let i = 0; i < 5 && !env.currentTurn?.pause.active; i += 1) {
1828+
await Promise.resolve();
1829+
}
1830+
assert.equal(env.currentTurn?.pause.active, true);
1831+
captured.onEvent!(
1832+
updateEvent({
1833+
sessionUpdate: "tool_call_update",
1834+
toolCallId: "tool-b",
1835+
status: "completed",
1836+
content: "tool-b cancellation closure",
1837+
}),
1838+
);
1839+
const firstResult = await firstTurn;
1840+
1841+
assert.equal(firstResult.ok, true);
1842+
assert.equal(firstResult.stopReason, "paused");
1843+
assert.equal(env.parkedApprovals.size, 1);
1844+
assert.deepEqual([...env.parkedApprovals.keys()], ["tool-a"]);
1845+
const gateA = env.parkedApprovals.get("tool-a")!;
1846+
const firstEvents = firstResult.events ?? [];
1847+
assert.equal(
1848+
firstEvents.filter(
1849+
(event) =>
1850+
event.type === "interaction_request" &&
1851+
(event.payload as { toolCallId?: string }).toolCallId === "tool-a",
1852+
).length,
1853+
1,
1854+
);
1855+
assert.deepEqual(
1856+
firstEvents.filter(
1857+
(event) => event.type === "tool_result" && event.id === "tool-b",
1858+
),
1859+
[
1860+
{
1861+
type: "tool_result",
1862+
id: "tool-b",
1863+
output: TOOL_NOT_EXECUTED_PAUSED,
1864+
isError: true,
1865+
},
1866+
],
1867+
);
1868+
assert.equal(
1869+
firstEvents.some(
1870+
(event) => event.type === "tool_result" && event.isError === false,
1871+
),
1872+
false,
1873+
"the never-started first-turn states have no success record",
1874+
);
1875+
1876+
env.clearTurn();
1877+
const secondTurn = runTurn(
1878+
env,
1879+
incidentRequest,
1880+
undefined,
1881+
undefined,
1882+
{
1883+
approvalParkMode: true,
1884+
resume: {
1885+
decisions: [
1886+
{
1887+
permissionId: gateA.permissionId,
1888+
reply: "once",
1889+
toolCallId: gateA.toolCallId,
1890+
toolName: gateA.toolName,
1891+
args: gateA.args,
1892+
interactionToken: gateA.interactionToken,
1893+
promptPromise: gateA.promptPromise,
1894+
},
1895+
],
1896+
carriedForward: [],
1897+
},
1898+
},
1899+
);
1900+
for (
1901+
let i = 0;
1902+
i < 5 &&
1903+
!calls.permissionReplies.some((reply) => reply.id === gateA.permissionId);
1904+
i += 1
1905+
) {
1906+
await Promise.resolve();
1907+
}
1908+
assert.equal(
1909+
calls.permissionReplies.filter((reply) => reply.id === gateA.permissionId)
1910+
.length,
1911+
1,
1912+
);
1913+
captured.onEvent!(
1914+
updateEvent({
1915+
sessionUpdate: "tool_call_update",
1916+
toolCallId: "tool-a",
1917+
status: "in_progress",
1918+
}),
1919+
);
1920+
captured.onPermissionRequest!({
1921+
id: "permission-b",
1922+
availableReplies: ["once", "reject"],
1923+
toolCall: {
1924+
toolCallId: "tool-b",
1925+
name: "tool-b",
1926+
rawInput: { target: "b" },
1927+
},
1928+
});
1929+
for (let i = 0; i < 5 && !env.currentTurn?.pause.active; i += 1) {
1930+
await Promise.resolve();
1931+
}
1932+
assert.equal(env.currentTurn?.pause.active, true);
1933+
captured.onEvent!(
1934+
updateEvent({
1935+
sessionUpdate: "tool_call_update",
1936+
toolCallId: "tool-a",
1937+
status: "completed",
1938+
content: "tool-a real output",
1939+
}),
1940+
);
1941+
const secondResult = await secondTurn;
1942+
1943+
assert.equal(secondResult.ok, true);
1944+
assert.equal(secondResult.stopReason, "paused");
1945+
assert.equal(env.parkedApprovals.size, 1);
1946+
assert.deepEqual([...env.parkedApprovals.keys()], ["tool-b"]);
1947+
const gateB = env.parkedApprovals.get("tool-b")!;
1948+
const secondEvents = secondResult.events ?? [];
1949+
assert.deepEqual(
1950+
secondEvents.filter(
1951+
(event) => event.type === "tool_result" && event.id === "tool-a",
1952+
),
1953+
[
1954+
{
1955+
type: "tool_result",
1956+
id: "tool-a",
1957+
output: "tool-a real output",
1958+
isError: false,
1959+
},
1960+
],
1961+
);
1962+
assert.equal(
1963+
secondEvents.filter(
1964+
(event) =>
1965+
event.type === "interaction_request" &&
1966+
(event.payload as { toolCallId?: string }).toolCallId === "tool-b",
1967+
).length,
1968+
1,
1969+
);
1970+
assert.deepEqual(
1971+
secondEvents.filter(
1972+
(event) =>
1973+
event.type === "interaction_response" &&
1974+
(event.payload as { toolCallId?: string }).toolCallId === "tool-a",
1975+
),
1976+
[
1977+
{
1978+
type: "interaction_response",
1979+
id: gateA.interactionToken,
1980+
kind: "user_approval",
1981+
payload: { toolCallId: "tool-a", approved: true },
1982+
},
1983+
],
1984+
);
1985+
1986+
env.clearTurn();
1987+
const thirdTurn = runTurn(
1988+
env,
1989+
incidentRequest,
1990+
undefined,
1991+
undefined,
1992+
{
1993+
approvalParkMode: true,
1994+
resume: {
1995+
decisions: [
1996+
{
1997+
permissionId: gateB.permissionId,
1998+
reply: "once",
1999+
toolCallId: gateB.toolCallId,
2000+
toolName: gateB.toolName,
2001+
args: gateB.args,
2002+
interactionToken: gateB.interactionToken,
2003+
promptPromise: gateB.promptPromise,
2004+
},
2005+
],
2006+
carriedForward: [],
2007+
},
2008+
},
2009+
);
2010+
for (
2011+
let i = 0;
2012+
i < 5 &&
2013+
!calls.permissionReplies.some((reply) => reply.id === gateB.permissionId);
2014+
i += 1
2015+
) {
2016+
await Promise.resolve();
2017+
}
2018+
assert.equal(
2019+
calls.permissionReplies.filter((reply) => reply.id === gateB.permissionId)
2020+
.length,
2021+
1,
2022+
);
2023+
captured.onEvent!(
2024+
updateEvent({
2025+
sessionUpdate: "tool_call_update",
2026+
toolCallId: "tool-b",
2027+
status: "completed",
2028+
content: "tool-b real output",
2029+
}),
2030+
);
2031+
calls.resolvePrompt!({
2032+
stopReason: "complete",
2033+
usage: { inputTokens: 1, outputTokens: 1 },
2034+
});
2035+
const thirdResult = await thirdTurn;
2036+
2037+
assert.equal(thirdResult.ok, true);
2038+
assert.equal(thirdResult.stopReason, "complete");
2039+
const thirdEvents = thirdResult.events ?? [];
2040+
assert.deepEqual(
2041+
thirdEvents.filter(
2042+
(event) => event.type === "tool_result" && event.id === "tool-b",
2043+
),
2044+
[
2045+
{
2046+
type: "tool_result",
2047+
id: "tool-b",
2048+
output: "tool-b real output",
2049+
isError: false,
2050+
},
2051+
],
2052+
);
2053+
assert.deepEqual(
2054+
thirdEvents.filter(
2055+
(event) =>
2056+
event.type === "interaction_response" &&
2057+
(event.payload as { toolCallId?: string }).toolCallId === "tool-b",
2058+
),
2059+
[
2060+
{
2061+
type: "interaction_response",
2062+
id: gateB.interactionToken,
2063+
kind: "user_approval",
2064+
payload: { toolCallId: "tool-b", approved: true },
2065+
},
2066+
],
2067+
);
2068+
2069+
const allEvents = [...firstEvents, ...secondEvents, ...thirdEvents];
2070+
const realResults = allEvents.filter(
2071+
(
2072+
event,
2073+
): event is Extract<AgentEvent, { type: "tool_result" }> =>
2074+
event.type === "tool_result" && event.isError === false,
2075+
);
2076+
assert.deepEqual(
2077+
realResults.map((event) => event.id).sort(),
2078+
["tool-a", "tool-b"],
2079+
"each call records exactly one real result",
2080+
);
2081+
assert.equal(
2082+
allEvents.some(
2083+
(event) =>
2084+
event.type === "tool_result" &&
2085+
event.id === "tool-a" &&
2086+
(event.output === TOOL_NOT_EXECUTED_PAUSED ||
2087+
event.output === APPROVED_EXECUTION_RESULT_UNKNOWN),
2088+
),
2089+
false,
2090+
"the approved tool-a result is never replaced by a sentinel",
2091+
);
2092+
assert.equal(
2093+
allEvents.some(
2094+
(event) =>
2095+
event.type === "tool_result" &&
2096+
event.output === "tool-b cancellation closure",
2097+
),
2098+
false,
2099+
"the cancellation closure is never recorded as success",
2100+
);
2101+
const permissionReplyCounts = new Map<string, number>();
2102+
for (const reply of calls.permissionReplies) {
2103+
permissionReplyCounts.set(
2104+
reply.id,
2105+
(permissionReplyCounts.get(reply.id) ?? 0) + 1,
2106+
);
2107+
}
2108+
assert.deepEqual(
2109+
permissionReplyCounts,
2110+
new Map([
2111+
[gateA.permissionId, 1],
2112+
[gateB.permissionId, 1],
2113+
]),
2114+
);
2115+
2116+
await env.destroy();
2117+
});
2118+
17772119
it("records the non-retry sentinel when an approved result misses the bound", async () => {
17782120
const { calls, deps, captured } = pausableHarness();
17792121
deps.resolveRunLimits = () => ({

0 commit comments

Comments
 (0)