Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
796a394
⚡ Bolt: [NetworkGraph 배열 슬라이싱 최적화]
seonghobae Sep 7, 2026
9b89f64
⚡ Bolt: [NetworkGraph 배열 슬라이싱 최적화]
seonghobae Sep 7, 2026
939c805
chore(bolt): remove completed task-specific repository guidance
seonghobae Sep 7, 2026
7c36562
test(network): cover bounded graph option materialization
seonghobae Sep 7, 2026
e20685f
⚡ Bolt: [NetworkGraph 배열 슬라이싱 최적화]
seonghobae Sep 8, 2026
8df8332
⚡ Bolt: [NetworkGraph 배열 슬라이싱 최적화]
seonghobae Sep 8, 2026
b3ef18a
test(network-graph): restore bounded option regression
seonghobae Sep 8, 2026
79e2bbc
⚡ Bolt: [NetworkGraph 배열 슬라이싱 최적화]
seonghobae Sep 8, 2026
8770667
chore(bolt): clean up unverified retro doctrines from changelog and j…
seonghobae Sep 8, 2026
08129db
test(network-graph): prove bounded materialization with iterable inst…
seonghobae Sep 8, 2026
816d614
test(network-graph): prove bounded materialization with iterable inst…
seonghobae Sep 8, 2026
06abf77
test(network-graph): preserve bounded option semantics during instrum…
seonghobae Sep 8, 2026
f25d440
test(network-graph): prove bounded materialization with iterable inst…
seonghobae Sep 8, 2026
bb4d04b
test(network-graph): restore isolated bounded-option regression
seonghobae Sep 8, 2026
2638e4f
test(network-graph): assure strict map iteration limits with global r…
seonghobae Sep 8, 2026
f25978c
test(network-graph): combine bounded semantics with failure-safe cleanup
seonghobae Sep 8, 2026
27a5acf
test(network-graph): assure strict map iteration limits with global r…
seonghobae Sep 8, 2026
419d3d7
test(network-graph): restore bounded option semantics
seonghobae Sep 8, 2026
ae7e5ce
test(network-graph): assure strict map iteration limits with global r…
seonghobae Sep 8, 2026
6ed655a
test(network-graph): restore semantic bounds after concurrent rewrite
seonghobae Sep 8, 2026
4a9980b
test(network-graph): bound reads per iterator
seonghobae Sep 8, 2026
cafd2f8
chore(bolt): remove completed task-specific repository guidance
seonghobae Sep 9, 2026
c66d3fb
fix(deps): bump next and sharp to resolve security vulnerabilities (C…
seonghobae Sep 9, 2026
c5c6d23
fix(deps): stack NetworkGraph behind patched frontend floor
seonghobae Sep 9, 2026
fdaaf05
test(ui): make NetworkGraph inspection deterministic
seonghobae Sep 9, 2026
fb1929b
docs(agent): serialize Next artifact verification
seonghobae Sep 9, 2026
f88343d
fix(scope): return Search browser evidence to copy owner
seonghobae Sep 9, 2026
8d18e79
fix(deps): restack NetworkGraph on current security owner
seonghobae Sep 9, 2026
156c16e
chore(network-graph): return governance and release-note ownership
seonghobae Sep 9, 2026
ba857c4
test(network-graph): restore per-iterator read bounds
seonghobae Sep 9, 2026
d01a536
chore(stack): adopt current frontend security contract
seonghobae Sep 9, 2026
9a299c0
chore(stack): adopt repaired frontend security owner
seonghobae Sep 9, 2026
3f5c655
test(network-graph): reject generator-branded source narration
seonghobae Sep 13, 2026
da82377
fix(network-graph): remove generator-branded optimization comments
seonghobae Sep 13, 2026
3379e4f
fix(network-graph): drop unrelated contrast drift
seonghobae Sep 13, 2026
99cd965
test(network-graph): assure strict map iteration limits with global r…
seonghobae Sep 15, 2026
08fcb92
test(network-graph): assure strict map iteration limits with precise …
seonghobae Sep 15, 2026
804401e
merge(network-graph): adopt current dependency-security parent
seonghobae Sep 15, 2026
8f98789
test(network-graph): bound each option iterator independently
seonghobae Sep 15, 2026
cf7092d
chore(stack): adopt current dependency-security parent
seonghobae Sep 16, 2026
f72fdae
chore(stack): adopt final PostCSS security-contract parent
seonghobae Sep 16, 2026
9723520
test(network): restore Map.values on setup failure
seonghobae Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions frontend/src/components/NetworkGraph.bounded-options.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/* @vitest-environment jsdom */
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";

const { apiGetMock } = vi.hoisted(() => ({
apiGetMock: vi.fn(),
}));

const destroyMock = vi.fn();

vi.mock("@/lib/api-client", () => ({
apiClient: {
get: apiGetMock,
},
}));

vi.mock("vis-network", () => ({
Network: vi.fn(function MockNetwork() {
return {
destroy: destroyMock,
fit: vi.fn(),
moveTo: vi.fn(),
off: vi.fn(),
on: vi.fn(),
selectEdges: vi.fn(),
selectNodes: vi.fn(),
};
}),
}));

import NetworkGraph from "./NetworkGraph";

async function flushAsyncWork() {
for (let index = 0; index < 5; index += 1) {
await act(async () => {
await Promise.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
}

describe("NetworkGraph bounded option materialization", () => {
const originalMapValues = Map.prototype.values;
let root: Root | null = null;
let container: HTMLDivElement | null = null;

afterEach(() => {
Map.prototype.values = originalMapValues;
expect(Map.prototype.values).toBe(originalMapValues);
if (root) {
act(() => root?.unmount());
}
root = null;
container?.remove();
container = null;
vi.clearAllMocks();
});

it("stops each option iterator at its limit and preserves insertion order", async () => {
const nodes = Array.from({ length: 15 }, (_, index) => ({
id: `node-${index}`,
label: `노드 ${index}`,
}));
const edges = Array.from({ length: 10 }, (_, index) => ({
id: `edge-${index}`,
from: `node-${index}`,
to: `node-${index + 1}`,
title: `관계 ${index}`,
}));

apiGetMock.mockResolvedValue({ nodes, edges });

const edgeIteratorReadCounts: number[] = [];
const nodeIteratorReadCounts: number[] = [];

try {
// Track each iterator independently so repeated renders cannot hide an
// unbounded iterator behind an aggregate read-count assertion.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Map.prototype.values = function(this: Map<any, any>) {
const iterator = originalMapValues.call(this);
const readCounts = this.has("edge-0")
? edgeIteratorReadCounts
: this.has("node-0")
? nodeIteratorReadCounts
: null;
const readCountIndex = readCounts?.push(0);

return {
next: () => {
if (readCounts && readCountIndex !== undefined) {
readCounts[readCountIndex - 1] += 1;
}
return iterator.next();
},
[Symbol.iterator]() {
return this;
},
};
} as any; // eslint-disable-line @typescript-eslint/no-explicit-any

container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);

await act(async () => {
root?.render(<NetworkGraph />);
});
await flushAsyncWork();

const relationshipSelect = container.querySelector(
'select[aria-label="관계 선택"]',
) as HTMLSelectElement | null;
const nodeSelect = container.querySelector(
'select[aria-label="노드 선택"]',
) as HTMLSelectElement | null;

expect(relationshipSelect?.options.length).toBe(6);
expect(nodeSelect?.options.length).toBe(9);

const actualEdgeOptions = Array.from(relationshipSelect?.options ?? [])
.map((option) => option.value)
.slice(1);
expect(actualEdgeOptions).toEqual([
"edge-0",
"edge-1",
"edge-2",
"edge-3",
"edge-4",
]);

const actualNodeOptions = Array.from(nodeSelect?.options ?? [])
.map((option) => option.value)
.slice(1);
expect(actualNodeOptions).toEqual([
"node-0",
"node-1",
"node-2",
"node-3",
"node-4",
"node-5",
"node-6",
"node-7",
]);

expect(edgeIteratorReadCounts.length).toBeGreaterThan(0);
expect(nodeIteratorReadCounts.length).toBeGreaterThan(0);
for (const readCount of edgeIteratorReadCounts) {
expect(readCount).toBeLessThanOrEqual(6);
}
for (const readCount of nodeIteratorReadCounts) {
expect(readCount).toBeLessThanOrEqual(9);
}
} finally {
Map.prototype.values = originalMapValues;
expect(Map.prototype.values).toBe(originalMapValues);
}
});
});
32 changes: 22 additions & 10 deletions frontend/src/components/NetworkGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,19 +286,31 @@ export default function NetworkGraph() {

const firstEdge = edges[0] ?? null;
const relationshipOptions = useMemo(() => {
return Array.from(edgeMap.values()).slice(0, 5).map((edge, index) => ({
edge,
id: String(edge.id),
label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`,
}));
const options = [];
let index = 0;
for (const edge of edgeMap.values()) {
if (options.length >= 5) break;
options.push({
edge,
id: String(edge.id),
label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`,
});
index++;
}
return options;
}, [edgeMap, nodeMap]);

const nodeOptions = useMemo(() => {
return Array.from(nodeInstanceMap.values()).slice(0, 8).map((node) => ({
id: String(node.id),
label: `노드: ${String(node.label ?? node.id)}`,
node,
}));
const options = [];
for (const node of nodeInstanceMap.values()) {
if (options.length >= 8) break;
options.push({
id: String(node.id),
label: `노드: ${String(node.label ?? node.id)}`,
node,
});
}
return options;
}, [nodeInstanceMap]);

const selectRelationship = (edge: Edge, status: string) => {
Expand Down