fix(javascript): index repeated graph observations before final sorting

Retain stable live nodes and observation arrays while avoiding quadratic
collection rebuilds. Preserve duplicate handling and canonical output.

Fixes #1748

Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
This commit is contained in:
Ysopwnable
2026-10-11 16:16:21 +08:00
committed by GitHub
parent 713db4dc26
commit 6d5d71fab5
2 changed files with 162 additions and 26 deletions
@@ -0,0 +1,97 @@
import { expect, it } from "vitest";
import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import { createJavaScriptApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
import { artifactObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js";
const nodeInput = (value: string, sha256 = "a".repeat(64)) => ({
kind: "javascript-asset",
identity: {
strategy: "content-digest",
stability: "global-exact",
sha256,
},
observations: [
{
label: value,
properties: { value },
evidence: artifactObservationEvidence({
sha256,
path: "main.js",
operation: "accumulator-test",
coverage: completeApplicationCoverage(),
}),
},
],
});
it("retains live references, exact deduplication and canonical output across later additions", () => {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const inputs = ["雪😀", "alpha", "beta"].map((value) => nodeInput(value));
const firstInput = inputs[0];
if (firstInput === undefined) throw new Error("Missing first input");
const node = accumulator.addNode(firstInput);
const observations = node.observations;
const firstObservation = observations[0];
expect(accumulator.addNode(firstInput)).toBe(node);
expect(node.observations).toBe(observations);
expect(observations).toHaveLength(1);
expect(observations[0]).not.toBe(firstObservation);
for (const input of inputs.slice(1))
expect(accumulator.addNode(input)).toBe(node);
const secondNode = accumulator.addNode(nodeInput("other", "b".repeat(64)));
const firstSnapshot = accumulator.nodes();
const expectedNode = createJavaScriptApplicationNode({
...firstInput,
observations: inputs.flatMap((input) => input.observations),
});
expect(firstSnapshot).toEqual([expectedNode, secondNode]);
expect(firstSnapshot[0]).toBe(node);
expect(node.observations).toBe(observations);
const finalizedObservations = [...observations];
expect(accumulator.nodes()).not.toBe(firstSnapshot);
expect(accumulator.nodes()).toEqual(firstSnapshot);
for (const [index, observation] of finalizedObservations.entries())
expect(node.observations[index]).toBe(observation);
const later = nodeInput("later");
expect(accumulator.addNode(later)).toBe(node);
// Updating a duplicate after final sorting must use its new array index.
expect(accumulator.addNode(firstInput)).toBe(node);
expect(accumulator.nodes()).toEqual([
createJavaScriptApplicationNode({
...firstInput,
observations: [...inputs, later].flatMap((input) => input.observations),
}),
secondNode,
]);
expect(node.observations).toBe(observations);
});
it("preserves first-input duplicates and retains the last occurrence when later merged", () => {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const first = nodeInput("same");
const distinct = nodeInput("distinct");
const node = accumulator.addNode({
...first,
observations: [...first.observations, ...first.observations],
});
const observations = node.observations;
const lastInitial = observations[1];
expect(accumulator.nodes()[0]?.observations).toHaveLength(2);
expect(accumulator.addNode(distinct)).toBe(node);
expect(accumulator.nodes()).toEqual([
createJavaScriptApplicationNode({
...first,
observations: [...first.observations, ...distinct.observations],
}),
]);
expect(node.observations).toBe(observations);
expect(observations).toContain(lastInitial);
// A repeated observation inside an incoming batch also collapses to one.
accumulator.addNode({
...distinct,
observations: [...distinct.observations, ...distinct.observations],
});
expect(accumulator.nodes()[0]?.observations).toHaveLength(2);
});