fix(javascript): retain runtime candidate ambiguity
Keep referenced ambiguous candidates unknown instead of classifying them as absent. Refs: #3192
This commit is contained in:
1 parent
44c9d2924d
commit
5e79d468e5
3 files changed
+133
No files matched your search
@@ -46,6 +46,7 @@ const classifyLayer = (
|
||||
): { readonly states: JavaScriptStaticLoadState[] } => {
|
||||
const nodes = layer.graph.nodes.filter(isLoadStateNode);
|
||||
const direct = new Map<string, string[]>();
|
||||
const ambiguous = new Set<string>();
|
||||
for (const match of matches)
|
||||
if (
|
||||
match.status === "matched" &&
|
||||
@@ -53,6 +54,10 @@ const classifyLayer = (
|
||||
match.static_node_id !== null
|
||||
)
|
||||
append(direct, match.static_node_id, match.runtime_node_id);
|
||||
else if (match.status === "ambiguous")
|
||||
for (const candidate of match.candidate_static_nodes)
|
||||
if (candidate.static_layer_id === layer.layerId)
|
||||
ambiguous.add(candidate.static_node_id);
|
||||
const resident = residentNodes(layer, new Set(direct.keys()));
|
||||
const runtimeScope = createStaticRuntimeScope(layer, runtimeEntities);
|
||||
const scopedEntities = runtimeEntities.filter(
|
||||
@@ -77,6 +82,12 @@ const classifyLayer = (
|
||||
runtimeNodeIds: [],
|
||||
reason: "containing-asset-was-loaded",
|
||||
});
|
||||
if (ambiguous.has(node.node_id))
|
||||
return state(layer, node, {
|
||||
status: "unknown",
|
||||
runtimeNodeIds: [],
|
||||
reason: "ambiguous-runtime-candidate",
|
||||
});
|
||||
const nodeWithinScope = staticNodeWithinRuntimeScope(node, runtimeScope);
|
||||
if (layerComplete && nodeWithinScope)
|
||||
return state(layer, node, {
|
||||
|
||||
@@ -228,6 +228,7 @@ const staticLoadStateSchema = z.strictObject({
|
||||
"not-observed-in-bounded-capture",
|
||||
"layer-outside-runtime-scope",
|
||||
"static-or-runtime-coverage-incomplete",
|
||||
"ambiguous-runtime-candidate",
|
||||
]),
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
type FakeCdpBrowser,
|
||||
} from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { writeElectronBoundaryFixture } from "../../fixtures/electronBoundaryApplication.js";
|
||||
import {
|
||||
SOURCE,
|
||||
applicationFixture,
|
||||
analyzeFixture,
|
||||
electronRuntimeEvidence,
|
||||
} from "../../fixtures/javascriptRuntimeReconciliation.js";
|
||||
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
@@ -219,8 +225,123 @@ describe("Electron CLI runtime reconciliation", () => {
|
||||
},
|
||||
INTEGRATION_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it(
|
||||
"keeps ambiguous runtime candidates unknown while unrelated scoped assets remain absent",
|
||||
async () => {
|
||||
const application = await applicationFixture();
|
||||
const assets = await createTestTempDirectory("rea-runtime-cli-assets-");
|
||||
const runtime = await createTestTempDirectory("rea-runtime-cli-live-");
|
||||
const inputPath = join(assets, "reconciliation.json");
|
||||
temporary.push(application, assets, runtime);
|
||||
await Promise.all([
|
||||
writeFile(join(assets, "app.js"), SOURCE),
|
||||
writeFile(join(assets, "absent.js"), "export const absent = true;\n"),
|
||||
writeFile(join(runtime, "app.js"), SOURCE),
|
||||
]);
|
||||
const rootMapping = [{ kind: "file-root", root: runtime }] as const;
|
||||
const input = {
|
||||
static_layers: [
|
||||
{
|
||||
role: "application",
|
||||
analysis: await analyzeFixture(application),
|
||||
runtime_mappings: rootMapping,
|
||||
},
|
||||
{
|
||||
role: "assets",
|
||||
analysis: await analyzeFixture(assets),
|
||||
runtime_mappings: rootMapping,
|
||||
},
|
||||
],
|
||||
runtime_observations: [
|
||||
electronRuntimeEvidence(runtime, SOURCE, { includeWorker: false }),
|
||||
],
|
||||
};
|
||||
await writeFile(inputPath, JSON.stringify(input));
|
||||
const reconciled = await runCli(
|
||||
["reconcile-javascript-runtime", inputPath, "--json"],
|
||||
process.env,
|
||||
);
|
||||
|
||||
const normalized =
|
||||
isRecord(reconciled) && isRecord(reconciled.normalized_result)
|
||||
? reconciled.normalized_result
|
||||
: undefined;
|
||||
const reconciliations = normalized?.reconciliations;
|
||||
const loadStates = normalized?.static_load_states;
|
||||
if (!Array.isArray(reconciliations) || !Array.isArray(loadStates))
|
||||
throw new Error("Missing runtime reconciliation output");
|
||||
const ambiguous = reconciliations.find(
|
||||
(item) =>
|
||||
isRecord(item) &&
|
||||
item.status === "ambiguous" &&
|
||||
item.entity_kind === "script",
|
||||
);
|
||||
if (
|
||||
!isRecord(ambiguous) ||
|
||||
!Array.isArray(ambiguous.candidate_static_nodes)
|
||||
)
|
||||
throw new Error("Missing ambiguous runtime script");
|
||||
const candidateIds = ambiguous.candidate_static_nodes.flatMap((item) =>
|
||||
isRecord(item) && typeof item.static_node_id === "string"
|
||||
? [item.static_node_id]
|
||||
: [],
|
||||
);
|
||||
expect(candidateIds).toHaveLength(2);
|
||||
const candidateStates = loadStates.filter(
|
||||
(item) =>
|
||||
isRecord(item) &&
|
||||
typeof item.static_node_id === "string" &&
|
||||
candidateIds.includes(item.static_node_id),
|
||||
);
|
||||
expect(candidateStates).toHaveLength(2);
|
||||
expect(
|
||||
candidateStates.every(
|
||||
(item) =>
|
||||
isRecord(item) &&
|
||||
item.status === "unknown" &&
|
||||
item.reason === "ambiguous-runtime-candidate",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(loadStates).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: "not-observed-in-capture",
|
||||
reason: "not-observed-in-bounded-capture",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
const singleLayerPath = join(application, "single-reconciliation.json");
|
||||
await writeFile(
|
||||
singleLayerPath,
|
||||
JSON.stringify({
|
||||
static_layers: [input.static_layers[0]],
|
||||
runtime_observations: input.runtime_observations,
|
||||
}),
|
||||
);
|
||||
const uniquelyMatched = await runCli(
|
||||
["reconcile-javascript-runtime", singleLayerPath, "--json"],
|
||||
process.env,
|
||||
);
|
||||
expect(uniquelyMatched).toMatchObject({
|
||||
normalized_result: {
|
||||
static_load_states: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: "loaded",
|
||||
reason: "runtime-script-correspondence",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
},
|
||||
INTEGRATION_TEST_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const runCli = async (
|
||||
arguments_: readonly string[],
|
||||
environment: NodeJS.ProcessEnv,
|
||||
|
||||
Reference in new issue
Block a user