mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
9be493880c
* 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>
315 lines
9.9 KiB
TypeScript
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 });
|
|
}
|
|
});
|