This commit is contained in:
2026-07-12 21:26:08 +08:00
commit 9dd41afd48
502 changed files with 129901 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
#!/bin/bash
# Regression: graph analysis helper should compute weights, source omission, and insights deterministically
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HELPER="$REPO_ROOT/scripts/graph-analysis.js"
fail() {
echo "FAIL: $1" >&2
exit 1
}
assert_text_contains() {
local text="$1"
local expected="$2"
if ! printf '%s' "$text" | grep -F -- "$expected" > /dev/null; then
fail "Expected output to contain: $expected"
fi
}
test_helper_computes_weights_and_source_omission() {
local tmp_dir output
tmp_dir="$(mktemp -d)"
mkdir -p "$tmp_dir/wiki/entities"
cat > "$tmp_dir/wiki/entities/A.md" <<'EOF'
---
sources: ["same.pdf"]
---
# A
[[B]]
EOF
cat > "$tmp_dir/wiki/entities/B.md" <<'EOF'
---
sources: ["same.pdf"]
---
# B
[[A]]
EOF
cat > "$tmp_dir/wiki/entities/C.md" <<'EOF'
# C
[[A]]
EOF
cat > "$tmp_dir/nodes.json" <<EOF
[
{"id":"A","label":"A","type":"entity","source_path":"$tmp_dir/wiki/entities/A.md"},
{"id":"B","label":"B","type":"entity","source_path":"$tmp_dir/wiki/entities/B.md"},
{"id":"C","label":"C","type":"entity","source_path":"$tmp_dir/wiki/entities/C.md"}
]
EOF
cat > "$tmp_dir/edges.json" <<'EOF'
[
{"id":"e1","from":"A","to":"B","type":"EXTRACTED"},
{"id":"e2","from":"B","to":"A","type":"EXTRACTED"},
{"id":"e3","from":"C","to":"A","type":"EXTRACTED"}
]
EOF
node "$HELPER" "$tmp_dir/nodes.json" "$tmp_dir/edges.json" "$tmp_dir/out.json" 0 500 250 1000 > /dev/null
output="$(jq -r '.edges[] | select(.from == "A" and .to == "B") | [.weight, .source_signal_available, .signals.source_overlap, .signals.type_affinity] | @tsv' "$tmp_dir/out.json")"
[ "$output" = $'0.667 true 1 1' ] || fail "Unexpected A-B edge metrics: $output"
output="$(jq -r '.edges[] | select(.from == "C" and .to == "A") | [.weight, .source_signal_available, (.signals.source_overlap // "null"), .signals.type_affinity] | @tsv' "$tmp_dir/out.json")"
[ "$output" = $'0.5 false null 1' ] || fail "Expected missing sources to be omitted from denominator, got: $output"
rm -rf "$tmp_dir"
}
test_single_node_graph_louvain_returns_safely() {
local output
output="$(node - <<'NODE' "$HELPER"
const helper = require(process.argv[2]);
const result = helper.runLouvain(["Solo"], new Map());
console.log(JSON.stringify(Object.fromEntries(result)));
NODE
)"
[ "$output" = '{"Solo":"Solo"}' ] || fail "Expected single-node Louvain to return solo assignment, got: $output"
}
test_parse_sources_yaml_multiline() {
local output
output="$(node - <<'NODE' "$HELPER"
const helper = require(process.argv[2]);
const fm = [
"title: Test",
"sources:",
" - paper_a.pdf",
" - paper_b.pdf",
" - \"quoted.pdf\""
].join("\n");
const result = helper.parseSourcesFrontmatter(fm);
console.log(JSON.stringify(result));
NODE
)"
local has_field parsed signal sources
has_field="$(printf '%s' "$output" | jq -r '.hasField')"
parsed="$(printf '%s' "$output" | jq -r '.parsed')"
signal="$(printf '%s' "$output" | jq -r '.signalAvailable')"
sources="$(printf '%s' "$output" | jq -r '.sources | join(",")')"
[ "$has_field" = "true" ] || fail "Expected hasField=true for YAML multiline, got: $has_field"
[ "$parsed" = "true" ] || fail "Expected parsed=true for YAML multiline, got: $parsed"
[ "$signal" = "true" ] || fail "Expected signalAvailable=true, got: $signal"
[ "$sources" = "paper_a.pdf,paper_b.pdf,quoted.pdf" ] || fail "Unexpected sources: $sources"
}
test_node_helper_bad_json_exits_with_error() {
local tmp_dir output
tmp_dir="$(mktemp -d)"
printf 'not json' > "$tmp_dir/bad.json"
printf '[]' > "$tmp_dir/edges.json"
if output="$(node "$HELPER" "$tmp_dir/bad.json" "$tmp_dir/edges.json" "$tmp_dir/out.json" 0 500 250 1000 2>&1)"; then
fail "graph-analysis.js should fail on invalid JSON input"
fi
assert_text_contains "$output" "Invalid JSON"
rm -rf "$tmp_dir"
}
test_helper_preserves_node_source_path() {
local tmp_dir output
tmp_dir="$(mktemp -d)"
mkdir -p "$tmp_dir/wiki/synthesis/sessions"
cat > "$tmp_dir/wiki/synthesis/sessions/A.md" <<'EOF'
# A
正文。
EOF
cat > "$tmp_dir/nodes.json" <<EOF
[
{"id":"A","label":"A","type":"synthesis","source_path":"$tmp_dir/wiki/synthesis/sessions/A.md"}
]
EOF
cat > "$tmp_dir/edges.json" <<'EOF'
[]
EOF
node "$HELPER" "$tmp_dir/nodes.json" "$tmp_dir/edges.json" "$tmp_dir/out.json" 0 500 250 1000 > /dev/null
output="$(jq -r '.nodes[] | select(.id == "A") | .source_path // ""' "$tmp_dir/out.json")"
[ "$output" = "$tmp_dir/wiki/synthesis/sessions/A.md" ] || fail "Expected source_path to survive graph analysis, got: $output"
rm -rf "$tmp_dir"
}
test_helper_reports_sparse_and_bridge_insights() {
local output
output="$(node - <<'NODE' "$HELPER"
const helper = require(process.argv[2]);
const nodesById = {
's-topic': { id: 's-topic', label: 'Sparse Topic' },
's1': { id: 's1', label: 'S1' },
's2': { id: 's2', label: 'S2' },
's3': { id: 's3', label: 'S3' },
's4': { id: 's4', label: 'S4' },
's5': { id: 's5', label: 'S5' },
'bridge': { id: 'bridge', label: 'Bridge' },
't2': { id: 't2', label: 'T2' },
'b1': { id: 'b1', label: 'B1' },
't3': { id: 't3', label: 'T3' },
'c1': { id: 'c1', label: 'C1' }
};
const edges = [
{ from: 's-topic', to: 's1', type: 'EXTRACTED' },
{ from: 's-topic', to: 's2', type: 'EXTRACTED' },
{ from: 'bridge', to: 's-topic', type: 'EXTRACTED' },
{ from: 'bridge', to: 't2', type: 'EXTRACTED' },
{ from: 'bridge', to: 't3', type: 'EXTRACTED' },
{ from: 't2', to: 'b1', type: 'EXTRACTED' },
{ from: 't3', to: 'c1', type: 'EXTRACTED' }
];
const pairMetrics = new Map([
['bridge\ts-topic', { weight: 0.8 }],
['bridge\tt2', { weight: 0.8 }],
['bridge\tt3', { weight: 0.8 }]
]);
const communityAssignments = new Map([
['s-topic', 's-topic'],
['s1', 's-topic'],
['s2', 's-topic'],
['s3', 's-topic'],
['s4', 's-topic'],
['s5', 's-topic'],
['bridge', 'bridge'],
['t2', 't2'],
['b1', 't2'],
['t3', 't3'],
['c1', 't3']
]);
const insights = helper.buildInsights(nodesById, edges, pairMetrics, communityAssignments, {
nodeCount: Object.keys(nodesById).length,
edgeCount: edges.length,
maxInsightNodes: 250,
maxInsightEdges: 1000
});
console.log(JSON.stringify(insights));
NODE
)"
local bridge sparse
bridge="$(printf '%s' "$output" | jq -r '.bridge_nodes[]?.id')"
assert_text_contains "$bridge" "bridge"
sparse="$(printf '%s' "$output" | jq -r '.sparse_communities[]?.id')"
assert_text_contains "$sparse" "s-topic"
}
main() {
test_helper_computes_weights_and_source_omission
test_single_node_graph_louvain_returns_safely
test_parse_sources_yaml_multiline
test_node_helper_bad_json_exits_with_error
test_helper_preserves_node_source_path
test_helper_reports_sparse_and_bridge_insights
echo "PASS: graph analysis helper regression coverage"
}
main "$@"