feat: 双链 link graph pure core — [[link]] parse + backlinks (F7)
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Pure bidirectional-link engine for the sticky-note/board system (a user-named differentiator). parseLinkTargets extracts trimmed, de-duped [[targets]]; LinkGraph builds forward + backlink indices (fromTexts parses, fromLinks takes explicit targets), ignores self-links, and danglingTargets() surfaces links to unknown nodes. Widget-free + storage-free so it is fully unit-tested; the board UI + persistence wrap it later. Built ahead of its phase deliberately as a zero-rework-risk pure data structure (not rendering/perf — the P0.5 device gate can't invalidate it). flutter analyze lib/editor clean; 139/139 tests (+10: parsing, backlinks, self-link, dangling, immutability). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
test/link_graph_test.dart
Normal file
81
test/link_graph_test.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
// Tests for the 双链 (bidirectional link) pure core (F7): [[link]] parsing +
|
||||
// forward/backlink indices + dangling-link detection.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/editor/link/link_graph.dart';
|
||||
|
||||
void main() {
|
||||
group('parseLinkTargets', () {
|
||||
test('extracts bracketed targets, trimmed', () {
|
||||
expect(parseLinkTargets('see [[Alpha]] and [[ Beta ]]'),
|
||||
['Alpha', 'Beta']);
|
||||
});
|
||||
|
||||
test('de-duplicates, keeping first-occurrence order', () {
|
||||
expect(parseLinkTargets('[[A]] [[B]] [[A]]'), ['A', 'B']);
|
||||
});
|
||||
|
||||
test('ignores empty / whitespace-only targets', () {
|
||||
expect(parseLinkTargets('x [[]] y [[ ]] z [[Real]]'), ['Real']);
|
||||
});
|
||||
|
||||
test('no links → empty', () {
|
||||
expect(parseLinkTargets('plain text, no links'), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('LinkGraph', () {
|
||||
test('backlinksOf collects every node linking to a target', () {
|
||||
final g = LinkGraph.fromTexts({
|
||||
'n1': 'links to [[n2]] and [[n3]]',
|
||||
'n2': 'links to [[n3]]',
|
||||
'n3': 'a leaf',
|
||||
});
|
||||
expect(g.linksFrom('n1'), {'n2', 'n3'});
|
||||
expect(g.linksFrom('n3'), isEmpty);
|
||||
expect(g.backlinksOf('n3'), {'n1', 'n2'});
|
||||
expect(g.backlinksOf('n2'), {'n1'});
|
||||
expect(g.backlinksOf('n1'), isEmpty);
|
||||
});
|
||||
|
||||
test('self-links are ignored', () {
|
||||
final g = LinkGraph.fromTexts({'n1': 'I reference [[n1]] myself'});
|
||||
expect(g.linksFrom('n1'), isEmpty);
|
||||
expect(g.backlinksOf('n1'), isEmpty);
|
||||
});
|
||||
|
||||
test('fromLinks builds the same indices from explicit targets', () {
|
||||
final g = LinkGraph.fromLinks({
|
||||
'a': ['b', 'c'],
|
||||
'b': ['c'],
|
||||
'c': [],
|
||||
});
|
||||
expect(g.backlinksOf('c'), {'a', 'b'});
|
||||
expect(g.linksFrom('a'), {'b', 'c'});
|
||||
});
|
||||
|
||||
test('danglingTargets surfaces links to unknown nodes', () {
|
||||
final g = LinkGraph.fromLinks({
|
||||
'a': ['b', 'ghost'],
|
||||
'b': [],
|
||||
});
|
||||
expect(g.danglingTargets(), {'ghost'});
|
||||
});
|
||||
|
||||
test('returned sets are unmodifiable', () {
|
||||
final g = LinkGraph.fromLinks({
|
||||
'a': ['b'],
|
||||
'b': [],
|
||||
});
|
||||
expect(() => g.linksFrom('a').add('x'), throwsUnsupportedError);
|
||||
expect(() => g.backlinksOf('b').add('x'), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('unknown node ids yield empty (no crash)', () {
|
||||
final g = LinkGraph.fromLinks({'a': []});
|
||||
expect(g.linksFrom('nope'), isEmpty);
|
||||
expect(g.backlinksOf('nope'), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user