Files
rea/tests/boundary/providers/kuna/kunaSingleFunctionRuns.test.ts
Kaoru0822 9be493880c fix(kuna): name the setting that owns a timed-out Kuna run (#4343)
* fix(kuna): name the setting that owns a timed-out Kuna run

A Kuna run past its deadline surfaced the generic "Analysis took too
long. Try a smaller request" message. The request is already one
function or one whole-program scan, and the same run takes as long
again, so the advice was wrong and REA_KUNA_TIMEOUT_MS or
REA_KUNA_PROJECT_TIMEOUT_MS was never mentioned.

Each Kuna command now carries a KunaDeadline: the configured
milliseconds and the setting that owns them, from KunaConfiguration.
A timeout reports, for example, "kuna decompile did not finish within
REA_KUNA_TIMEOUT_MS (120000 ms)." Its remediation says to raise that
setting and restart REA, and for a single-function decompile that the
session's project export can still answer. AnalysisTimeoutError now
accepts a producer message and remediation, which projection uses and
per-waiter scan errors keep.

Refs #1957

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(kuna): scope the provider_timeout statement to direct Kuna runs

A timed-out project export is reported through the export failure as
capability_unavailable, not provider_timeout.

Refs #1957

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kuna): verify native xrefs under the project deadline

The provider's xrefs scan runs under REA_KUNA_PROJECT_TIMEOUT_MS, but the
native oracle in verify:kuna aborted at REA_KUNA_TIMEOUT_MS. A scan that
fit the project deadline but not the function deadline failed
verification although the provider answered it. Use the same deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:38:16 -07:00

315 lines
9.9 KiB
TypeScript

import { expect, it as test } from "vitest";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import {
fakeKunaCalls,
fakeKunaExits,
setFakeKunaBehavior,
} from "./fakeKuna.js";
import {
FUNCTIONS,
run,
session,
unprovenCleanupLauncher,
} from "./kunaSessionFixture.js";
const it = test.skipIf(process.platform === "win32");
const decompilesOf = async (kuna: string) =>
(await fakeKunaCalls(kuna)).filter(({ argv }) => argv[0] === "decompile");
it("shares one single-function run among concurrent requests for one entry", async () => {
const { kuna, open } = await session("rea-kuna-single-shared-", {
project: "hang",
decompileDelayMs: 300,
});
const client = open();
try {
const answers = await Promise.all(
["main", "main", "0x1000"].map((procedure) =>
run(client, "procedure_pseudo_code", { procedure }),
),
);
for (const answer of answers)
expect(answer).toMatchObject({
result: "int main(void) // single-function\n{\n return 0;\n}",
rawResult: { source: { kind: "single-function" } },
});
expect(await decompilesOf(kuna)).toHaveLength(1);
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("keeps a shared run for the callers still waiting when one cancels", async () => {
const { kuna, open } = await session("rea-kuna-single-cancel-", {
project: "hang",
decompileDelayMs: 500,
});
const client = open();
const caller = new AbortController();
try {
const leaving = client.execute(
"procedure_pseudo_code",
{ procedure: "main" },
{ signal: caller.signal },
);
const staying = run(client, "procedure_pseudo_code", { procedure: "main" });
await expect.poll(() => decompilesOf(kuna)).toHaveLength(1);
caller.abort();
expect(await leaving).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
expect((await staying).rawResult).toMatchObject({
source: { kind: "single-function" },
});
const [call] = await decompilesOf(kuna);
expect((await fakeKunaExits(kuna)).has(call?.pid ?? 0)).toBe(true);
expect(await decompilesOf(kuna)).toHaveLength(1);
} finally {
caller.abort();
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("admits no more concurrent single-function runs than its slots", async () => {
const { kuna, open } = await session(
"rea-kuna-single-slots-",
{ project: "hang", decompileDelayMs: 200 },
{},
undefined,
1,
);
const client = open();
try {
await Promise.all(
["main", "parse"].map((procedure) =>
run(client, "procedure_pseudo_code", { procedure }),
),
);
const [first, second] = [...(await decompilesOf(kuna))].sort(
(left, right) => left.at - right.at,
);
const exits = await fakeKunaExits(kuna);
expect(first).toBeDefined();
expect(second).toBeDefined();
// The second run starts only after the first one's process finished.
expect(second?.at).toBeGreaterThanOrEqual(exits.get(first?.pid ?? 0) ?? 0);
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("answers a queued request from the export once it finishes first", async () => {
const { kuna, open } = await session(
"rea-kuna-single-yield-",
{ delayMs: 400, decompile: "hang" },
{},
undefined,
1,
);
const client = open();
const holder = new AbortController();
try {
// This run holds the only slot until its caller leaves.
const holding = client.execute(
"procedure_pseudo_code",
{ procedure: "main" },
{ signal: holder.signal },
);
await expect.poll(() => decompilesOf(kuna)).toHaveLength(1);
const queued = await run(client, "procedure_pseudo_code", {
procedure: "parse",
});
expect(queued).toMatchObject({
result: "int parse(void)\n{\n return 1;\n}",
rawResult: { source: { kind: "project-export" } },
});
expect(await decompilesOf(kuna)).toHaveLength(1);
holder.abort();
expect(await holding).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
} finally {
holder.abort();
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("keeps the slot of a run whose stop is unproven until close proves it", async () => {
const unproven = unprovenCleanupLauncher("decompile");
const { kuna, open } = await session(
"rea-kuna-single-retained-slot-",
{ project: "hang", decompile: "hang" },
{ REA_KUNA_TIMEOUT_MS: "300" },
unproven.launcher,
1,
);
const client = open();
const waiter = new AbortController();
try {
expect(
await client.execute("procedure_pseudo_code", { procedure: "main" }),
).toMatchObject({ ok: false, error: { cleanupIncomplete: true } });
// The timed-out tree may still run, so the next run must not start.
const queued = client.execute(
"procedure_pseudo_code",
{ procedure: "parse" },
{ signal: waiter.signal },
);
await new Promise((resolve) => setTimeout(resolve, 300));
expect(await decompilesOf(kuna)).toHaveLength(1);
waiter.abort();
expect(await queued).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
expect((await client.close()).ok).toBe(false);
unproven.release();
expect(await client.close()).toEqual({ ok: true, value: null });
// The proven stop returns the slot to the provider.
await setFakeKunaBehavior(kuna, { functions: FUNCTIONS, project: "hang" });
const next = open();
try {
expect(
(await run(next, "procedure_pseudo_code", { procedure: "parse" }))
.rawResult,
).toMatchObject({ source: { kind: "single-function" } });
} finally {
expect(await next.close()).toEqual({ ok: true, value: null });
}
} finally {
waiter.abort();
unproven.release();
await client.close();
}
});
it("answers a repeated request from the completed run while the export builds", async () => {
const { kuna, open } = await session("rea-kuna-single-kept-", {
project: "hang",
});
const client = open();
try {
const first = await run(client, "procedure_pseudo_code", {
procedure: "main",
});
// A repeat by name or by address reuses the text; no run re-analyzes.
for (const procedure of ["main", "0x1000"])
expect(
await run(client, "procedure_pseudo_code", { procedure }),
).toMatchObject({
result: first.result,
rawResult: { source: { kind: "single-function" } },
});
expect(await decompilesOf(kuna)).toHaveLength(1);
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("keeps answering repeats from the completed run after the export fails", async () => {
const { kuna, open } = await session("rea-kuna-single-kept-failed-", {
project: "exit-failure",
});
const client = open();
try {
expect(
await client.execute("procedure_assembly", { procedure: "main" }),
).toMatchObject({ ok: false, error: { retryable: false } });
for (let repeat = 0; repeat < 2; repeat += 1) {
const answer = await run(client, "procedure_pseudo_code", {
procedure: "main",
});
expect(answer.rawResult).toMatchObject({
source: { kind: "single-function" },
});
expect(answer.limitations).toContainEqual(
expect.stringContaining("The project export failed"),
);
}
expect(await decompilesOf(kuna)).toHaveLength(1);
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("does not keep a failed run, so a repeat runs again", async () => {
const { kuna, open } = await session(
"rea-kuna-single-failed-not-kept-",
{ project: "hang", decompile: "hang" },
{ REA_KUNA_TIMEOUT_MS: "200" },
);
const client = open();
try {
for (let repeat = 0; repeat < 2; repeat += 1)
expect(
await client.execute("procedure_pseudo_code", { procedure: "main" }),
).toMatchObject({ ok: false });
expect(await decompilesOf(kuna)).toHaveLength(2);
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("drops the least recently used text beyond the session's 16 MiB budget", async () => {
// Two 6 MiB texts fit the budget and a third does not. Reading main again
// makes parse the least recently used, so keeping broken drops parse.
const { kuna, open } = await session("rea-kuna-single-kept-budget-", {
project: "hang",
decompilePadding: 6 * 1024 * 1024,
});
const client = open();
try {
for (const procedure of [
"main",
"parse",
"main",
"broken",
"main",
"parse",
])
await run(client, "procedure_pseudo_code", { procedure });
expect((await decompilesOf(kuna)).map(({ argv }) => argv[2])).toEqual([
"0x1000",
"0x1040",
"0x1080",
"0x1040",
]);
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("names the setting that owns a single-function deadline", async () => {
const { open } = await session(
"rea-kuna-function-deadline-",
{ project: "hang", decompile: "hang" },
{ REA_KUNA_TIMEOUT_MS: "300" },
);
const client = open();
try {
const result = await client.execute("procedure_pseudo_code", {
procedure: "main",
});
if (result.ok) throw new Error("the hanging run answered");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "provider_timeout",
message:
"kuna decompile did not finish within REA_KUNA_TIMEOUT_MS (300 ms).",
remediation: {
action: expect.stringMatching(
/^Raise REA_KUNA_TIMEOUT_MS .* restart REA.*project export/u,
),
},
details: { timeout_ms: 300 },
});
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});