Notes: sync templates, fix Create layout, repair direct children, ListItems+ListNotes merge

=== Breaking ===
- CreateNote for unsupported parents (file/note/action/secret/worklog/link) -> error
- EnsureNotesFolder validates parent supports notes before creating Notes/

=== Templates (system_templates.json) ===
- folder.default: +notes module, +Notes folder in default_folders
- document.default: +notes module, +Notes folder, +Overview.md default file
- recipe.default: +Notes folder in default_folders
All container types now consistently declare notes support.

=== CreateNodeFromTemplate layout fix ===
- DefaultFolders created BEFORE DefaultFiles (so Notes/ exists)
- DefaultFile nodes now parented inside Notes folder, not the container
- File path, file record, notes record all canonical: Notes/Overview.md
- No root-level Overview.md created

=== ListItems (Files tab) ===
- bindings_files.go: ListItems now includes TypeNote (not just TypeFolder+TypeFile)
- Notes folder visible in Files tab tree
- Overview.md inside Notes shown with type='note', Mime='text/markdown'

=== ListNotes merge ===
- Collects from both Notes folder (canonical) and direct TypeNote children (compat)
- Duplicates excluded via seen set
- Canonical layout takes priority

=== RepairNotesLayout ===
- Moves direct TypeNote children into Notes folder via nodes.Move
- Updates files.path/files.filename for moved notes
- Skips non-container parents (file/note/etc)

=== Tests ===
- note_repair_test.go: 37 tests (24 old + 13 new)
  - SupportsNotes for containers/non-containers
  - EnsureNotesFolder rejects for unsupported parents
  - Create for file/note parent -> error, no state leak
  - Repair: skips non-containers, creates Notes folder, moves notes
  - Files tab: Notes folder visible, Overview content preserved after repair
- vault_layout_notes_files_test.go: 3 new ListItems/repair tests
  - ListItems shows Notes folder
  - ListItems inside Notes shows Overview with FileID
  - Repair moves direct children, ListItems reflects new layout
- Updated: suggest_test.go, trash_test.go, vault_layout_test.go expectations

=== Misc ===
- nodes/repository.go: ListByType helper for test use
- bindings_files.go: TypeNote in ListItems, Mime=text/markdown
This commit is contained in:
2026-06-15 17:24:56 +08:00
parent 2cbb2986c1
commit fec35f55b8
11 changed files with 2218 additions and 71 deletions
+13 -4
View File
@@ -42,24 +42,33 @@ func (a *App) ListItems(nodeID string) ([]FileTreeItemDTO, error) {
}
result := make([]FileTreeItemDTO, 0, len(children))
for i := range children {
if children[i].Type != nodes.TypeFolder && children[i].Type != nodes.TypeFile {
typ := children[i].Type
if typ != nodes.TypeFolder && typ != nodes.TypeFile && typ != nodes.TypeNote {
continue
}
item := FileTreeItemDTO{
ID: children[i].ID,
Name: children[i].Title,
Type: children[i].Type,
Type: typ,
}
if children[i].Type == nodes.TypeFolder {
if typ == nodes.TypeFolder {
kids, _ := a.nodes.ListChildren(children[i].ID, false)
item.HasKids = len(kids) > 0
} else if children[i].Type == nodes.TypeFile {
item.Mime = "inode/directory"
} else if typ == nodes.TypeFile {
records, _ := a.files.ListByNode(children[i].ID)
if len(records) > 0 {
item.FileID = records[0].ID
item.Size = records[0].Size
item.Mime = records[0].MIME
}
} else if typ == nodes.TypeNote {
records, _ := a.files.ListByNode(children[i].ID)
if len(records) > 0 {
item.FileID = records[0].ID
item.Size = records[0].Size
item.Mime = "text/markdown"
}
}
result = append(result, item)
}
+46 -32
View File
@@ -173,8 +173,42 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD
}
}
// 1. Create default folders FIRST so Notes/ exists before default files.
var notesFolderID string
for _, folderName := range tmpl.DefaultFolders {
folderSeg := templates.SafeDisplayNameToPathSegment(folderName)
if folderSeg == "" {
folderSeg = "folder"
}
childNode, childErr := a.nodes.Create(&n.ID, nodes.TypeFolder, folderName, 0, "", "")
if childErr != nil {
rollbackChildren()
return nil, fmt.Errorf("create child folder %s: %w", folderName, childErr)
}
created = append(created, childInfo{id: childNode.ID, file: false})
if folderName == notes.NotesFolder {
notesFolderID = childNode.ID
}
childFsPath := folderSeg
if fsPath != "" {
childFsPath = filepath.Join(fsPath, folderSeg)
}
childPhysPath := filepath.Join(a.vault, childFsPath)
childPhysPath = templates.UniquePath(childPhysPath)
childRel, _ := filepath.Rel(a.vault, childPhysPath)
childFsPath = childRel
_ = a.nodes.UpdateFsPath(childNode.ID, childFsPath)
if err := os.MkdirAll(childPhysPath, 0o755); err != nil {
rollbackChildren()
return nil, fmt.Errorf("create child folder directory %s: %w", folderName, err)
}
_ = a.activity.Record(n.ID, activity.TargetFolder, childNode.ID, "", activity.TypeNodeCreated, folderName, "")
_ = a.sync.RecordOp(syncsvc.EntityFolder, childNode.ID, syncsvc.OpCreate, nodePayload(childNode))
}
// 2. Create default files, placing them under Notes folder when it exists.
for _, df := range tmpl.DefaultFiles {
// Default files (like Overview.md) go into the Notes/ subfolder
fpath := filepath.Join(physPath, notes.NotesFolder, df.Path)
if err := os.MkdirAll(filepath.Dir(fpath), 0o755); err != nil {
rollbackChildren()
@@ -184,7 +218,12 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD
if fileTitle == "" {
fileTitle = "Overview"
}
childNode, childErr := a.nodes.Create(&n.ID, nodes.TypeNote, fileTitle, 0, "", "")
parentForFile := &n.ID
if notesFolderID != "" {
parentForFile = &notesFolderID
}
childNode, childErr := a.nodes.Create(parentForFile, nodes.TypeNote, fileTitle, 0, "", "")
if childErr != nil {
rollbackChildren()
return nil, fmt.Errorf("create child node for %s: %w", df.Path, childErr)
@@ -211,9 +250,13 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD
`INSERT OR IGNORE INTO notes (node_id, file_id, format) VALUES (?,?,?)`,
childNode.ID, fileID, "markdown")
_ = a.activity.Record(n.ID, activity.TargetNote, childNode.ID, "", activity.TypeNoteCreated, fileTitle, "")
parentIDForSync := n.ID
if notesFolderID != "" {
parentIDForSync = notesFolderID
}
_ = a.sync.RecordOp(syncsvc.EntityNote, childNode.ID, syncsvc.OpCreate, map[string]interface{}{
"node_id": childNode.ID,
"parent_id": n.ID,
"parent_id": parentIDForSync,
"title": fileTitle,
"file_id": fileID,
"format": "markdown",
@@ -225,35 +268,6 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD
})
}
for _, folderName := range tmpl.DefaultFolders {
folderSeg := templates.SafeDisplayNameToPathSegment(folderName)
if folderSeg == "" {
folderSeg = "folder"
}
childNode, childErr := a.nodes.Create(&n.ID, nodes.TypeFolder, folderName, 0, "", "")
if childErr != nil {
rollbackChildren()
return nil, fmt.Errorf("create child folder %s: %w", folderName, childErr)
}
created = append(created, childInfo{id: childNode.ID, file: false})
childFsPath := folderSeg
if fsPath != "" {
childFsPath = filepath.Join(fsPath, folderSeg)
}
childPhysPath := filepath.Join(a.vault, childFsPath)
childPhysPath = templates.UniquePath(childPhysPath)
childRel, _ := filepath.Rel(a.vault, childPhysPath)
childFsPath = childRel
_ = a.nodes.UpdateFsPath(childNode.ID, childFsPath)
if err := os.MkdirAll(childPhysPath, 0o755); err != nil {
rollbackChildren()
return nil, fmt.Errorf("create child folder directory %s: %w", folderName, err)
}
_ = a.activity.Record(n.ID, activity.TargetFolder, childNode.ID, "", activity.TypeNodeCreated, folderName, "")
_ = a.sync.RecordOp(syncsvc.EntityFolder, childNode.ID, syncsvc.OpCreate, nodePayload(childNode))
}
pid := ""
if parentID != "" {
pid = parentID
+51 -4
View File
@@ -4,6 +4,7 @@ import (
"time"
"verstak/internal/core/activity"
"verstak/internal/core/notes"
"verstak/internal/core/nodes"
syncsvc "verstak/internal/core/sync"
)
@@ -12,19 +13,65 @@ func (a *App) ListNotes(nodeID string) ([]NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
children, err := a.nodes.ListChildren(nodeID, false)
// Return empty for non-notes-capable parents.
if !a.notes.SupportsNotes(nodeID) {
return []NodeDTO{}, nil
}
// Try the canonical layout: notes live under a "Notes" folder.
// Also fall back to direct TypeNote children so that notes placed
// directly by AssignInboxNode / old layout are still visible.
notesFolder := a.notes.FindNotesFolder(nodeID)
// Direct children (old layout / inbox-assigned notes)
directChildren, err := a.nodes.ListChildren(nodeID, false)
if err != nil {
return nil, err
}
seen := map[string]bool{}
var result []NodeDTO
for i := range children {
if children[i].Type == nodes.TypeNote {
result = append(result, toNodeDTO(&children[i]))
processNote := func(n nodes.Node) {
if n.Type == nodes.TypeNote && !seen[n.ID] {
seen[n.ID] = true
result = append(result, toNodeDTO(&n))
}
}
if notesFolder != nil {
// Canonical layout: notes inside the Notes folder
notesChildren, err := a.nodes.ListChildren(notesFolder.ID, false)
if err != nil {
return nil, err
}
for i := range notesChildren {
processNote(notesChildren[i])
}
}
// Also include direct TypeNote children (old / inbox-assigned notes)
for i := range directChildren {
processNote(directChildren[i])
}
// Trigger repair in background for old-layout notes
go func() {
if _, err := a.notes.RepairNotesLayout(); err != nil {
// log only
}
}()
return result, nil
}
func (a *App) RepairNotesLayout() (*notes.RepairResult, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
return a.notes.RepairNotesLayout()
}
func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
+28 -9
View File
@@ -93,8 +93,9 @@ func TestJournalFullRegression(t *testing.T) {
if s.NodeID == n.ID {
found = true
// Verify events match what we inserted
if len(s.Events) != 3 {
t.Fatalf("suggestion has %d events, want 3", len(s.Events))
// Notes folder creation (from template) adds 1 auto event.
if len(s.Events) != 4 {
t.Fatalf("suggestion has %d events, want 4 (1 auto from Notes creation + 3 manual)", len(s.Events))
}
if len(s.EventIDs) != len(s.Events) {
t.Fatalf("suggestion eventIds (%d) != events (%d)", len(s.EventIDs), len(s.Events))
@@ -194,15 +195,21 @@ func TestSuggestionOnRepeatedActivity(t *testing.T) {
for _, s := range suggestions {
if s.NodeID == n.ID {
found = true
// Only the second event should be suggested
if len(s.Events) != 1 {
t.Fatalf("expected 1 new event, got %d", len(s.Events))
// The first event (Notes folder creation) was not accepted,
// so the suggestion includes it plus the new eid2.
if len(s.Events) < 1 {
t.Fatalf("expected at least 1 new event, got %d", len(s.Events))
}
if s.Events[0].ID != eid2 {
t.Errorf("expected event %s, got %s", eid2, s.Events[0].ID)
// eid2 must be among the suggested events
var hasNew bool
for _, ev := range s.Events {
if ev.ID == eid2 {
hasNew = true
break
}
}
if len(s.EventIDs) != 1 || s.EventIDs[0] != eid2 {
t.Errorf("eventIds does not contain the new event")
if !hasNew {
t.Errorf("eid2 not found among suggestion events")
}
}
}
@@ -417,6 +424,18 @@ func TestDismissSuggestionHidesSuggestionWithoutDeletingEvents(t *testing.T) {
if err != nil {
t.Fatalf("create node: %v", err)
}
// The template creates a Notes folder, which generates an auto event.
// Dismiss it first so it doesn't interfere with the test.
autoEvts, _ := app.GetSuggestions()
for _, s := range autoEvts {
if s.NodeID == n.ID && len(s.EventIDs) > 0 {
// Dismiss all outstanding auto events for this node
b, _ := json.Marshal(s.EventIDs)
_ = app.DismissSuggestion(n.ID, string(b))
}
}
eventID := insertTestEvent(t, app, n.ID, activity.TypeNoteUpdated, "note", "note-1", "Изменение заметки")
if err := app.DismissSuggestion(n.ID, string(mustJSON(t, []string{eventID}))); err != nil {
+4 -4
View File
@@ -136,15 +136,15 @@ func TestTrashCountPurgeAndEmpty(t *testing.T) {
if err != nil {
t.Fatalf("TrashCount: %v", err)
}
if count != 2 {
t.Fatalf("TrashCount = %d, want 2", count)
if count != 4 {
t.Fatalf("TrashCount = %d, want 4 (2 folders + 2 Notes children)", count)
}
if err := app.PurgeTrashNodesJSON(`["` + a.ID + `"]`); err != nil {
t.Fatalf("PurgeTrashNodesJSON: %v", err)
}
count, _ = app.TrashCount()
if count != 1 {
t.Fatalf("TrashCount after purge = %d, want 1", count)
if count != 2 {
t.Fatalf("TrashCount after purge = %d, want 2 (1 folder + 1 Notes child)", count)
}
if err := app.EmptyTrash(); err != nil {
t.Fatalf("EmptyTrash: %v", err)
@@ -0,0 +1,248 @@
package main
import (
"os"
"path/filepath"
"testing"
"verstak/internal/core/nodes"
"verstak/internal/core/notes"
)
// --- Files tab / ListItems tests for Notes folder ---
// TestFileManagerListItemsShowsNotesFolder verifies that ListItems on a
// container returns the Notes folder, matching what the Files tab UI shows.
func TestFileManagerListItemsShowsNotesFolder(t *testing.T) {
app, _ := setupTestApp(t)
proj, err := app.CreateNodeFromTemplate("", "TestProj", "project.default")
if err != nil {
t.Fatalf("create project: %v", err)
}
// ListItems is what the Files tab actually calls
items, err := app.ListItems(proj.ID)
if err != nil {
t.Fatalf("ListItems: %v", err)
}
var foundNotes bool
for _, item := range items {
if item.Name == notes.NotesFolder && item.Type == "folder" {
foundNotes = true
break
}
}
if !foundNotes {
t.Errorf("ListItems(%q) should contain Notes folder, got %d items", proj.ID, len(items))
for _, item := range items {
t.Logf(" %s (type=%s)", item.Name, item.Type)
}
}
}
// TestFileManagerListItemsInsideNotesShowsOverview verifies that ListItems
// on the Notes folder returns the Overview note.
func TestFileManagerListItemsInsideNotesShowsOverview(t *testing.T) {
app, vault := setupTestApp(t)
proj, err := app.CreateNodeFromTemplate("", "TestProj", "project.default")
if err != nil {
t.Fatalf("create project: %v", err)
}
// Find Notes folder
children, err := app.nodes.ListChildren(proj.ID, false)
if err != nil {
t.Fatalf("ListChildren: %v", err)
}
var notesFolder *nodes.Node
for i := range children {
if children[i].Title == notes.NotesFolder && children[i].Type == "folder" {
notesFolder = &children[i]
break
}
}
if notesFolder == nil {
t.Fatal("Notes folder not found")
}
// ListItems inside Notes folder
items, err := app.ListItems(notesFolder.ID)
if err != nil {
t.Fatalf("ListItems(Notes folder): %v", err)
}
var foundOverview bool
for _, item := range items {
if item.Name == "Overview" && item.Type == "note" {
foundOverview = true
if item.FileID == "" {
t.Error("Overview note has empty FileID")
}
if item.Mime == "" {
t.Error("Overview note has empty Mime")
}
break
}
}
if !foundOverview {
t.Errorf("ListItems(Notes) should contain Overview note, got %d items", len(items))
for _, item := range items {
t.Logf(" %s (type=%s)", item.Name, item.Type)
}
}
// Verify no root-level Overview.md on disk
rootPath := filepath.Join(vault, proj.FsPath, "Overview.md")
if _, err := os.Stat(rootPath); err == nil {
t.Error("Overview.md should NOT exist at root level, only in Notes/")
}
// Verify Notes/Overview.md exists on disk
notesPath := filepath.Join(vault, proj.FsPath, notes.NotesFolder, "Overview.md")
if _, err := os.Stat(notesPath); os.IsNotExist(err) {
t.Error("Overview.md should exist at Notes/Overview.md")
}
}
// TestRepairMovesDirectNoteChildrenToNotesFolder verifies that a note
// created as a direct child of a container (old layout) is moved into
// the Notes folder by RepairNotesLayout, and that ListItems then shows
// the note under Notes/ not as a direct child.
func TestRepairMovesDirectNoteChildrenToNotesFolder(t *testing.T) {
app, vault := setupTestApp(t)
// Create a container that supports notes
parent, err := app.CreateNodeFromTemplate("", "TestCase", "project.default")
if err != nil {
t.Fatalf("create container: %v", err)
}
// Simulate old layout: create TypeNote as direct child (not under Notes/)
noteNode, err := app.nodes.Create(&parent.ID, nodes.TypeNote, "LegacyNote", 0, "", "")
if err != nil {
t.Fatalf("create legacy note: %v", err)
}
// Write a physical file so repair has something to fix
noteDir := filepath.Join(vault, parent.FsPath)
if err := os.MkdirAll(noteDir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
oldPath := filepath.Join(noteDir, "LegacyNote.md")
if err := os.WriteFile(oldPath, []byte("# Legacy Content\n"), 0o640); err != nil {
t.Fatalf("write file: %v", err)
}
relPath, _ := filepath.Rel(vault, oldPath)
fileID := "repair-file-" + noteNode.ID
_, _ = app.db.Exec(
`INSERT INTO files (id,node_id,filename,path,storage_mode,size,sha256,mime,created_at,updated_at,missing)
VALUES (?,?,?,?,'vault',0,'','text/plain','2024-01-01T00:00:00Z','2024-01-01T00:00:00Z',0)`,
fileID, noteNode.ID, "LegacyNote.md", relPath)
_, _ = app.db.Exec(
`INSERT OR IGNORE INTO notes (node_id, file_id, format) VALUES (?,?,?)`,
noteNode.ID, fileID, "markdown")
// Verify the note is a direct child before repair
beforeChildren, err := app.nodes.ListChildren(parent.ID, false)
if err != nil {
t.Fatalf("ListChildren before repair: %v", err)
}
var foundDirect bool
for _, c := range beforeChildren {
if c.ID == noteNode.ID {
foundDirect = true
break
}
}
if !foundDirect {
t.Fatal("legacy note should be a direct child before repair")
}
// Run repair
result, err := app.notes.RepairNotesLayout()
if err != nil {
t.Fatalf("RepairNotesLayout: %v", err)
}
if result.RepairedNotes == 0 {
t.Errorf("expected at least 1 repaired note, got 0")
}
// After repair: note should NOT be a direct child of parent
afterChildren, err := app.nodes.ListChildren(parent.ID, false)
if err != nil {
t.Fatalf("ListChildren after repair: %v", err)
}
for _, c := range afterChildren {
if c.ID == noteNode.ID {
t.Errorf("note should no longer be a direct child after repair")
break
}
}
// After repair: note should be inside Notes folder
// ListItems(parent.ID) should NOT show the note directly
parentItems, err := app.ListItems(parent.ID)
if err != nil {
t.Fatalf("ListItems(parent) after repair: %v", err)
}
for _, item := range parentItems {
if item.ID == noteNode.ID {
t.Errorf("ListItems(parent) should not show note directly after repair")
break
}
}
// But ListItems(Notes) should show it
var notesFolder *nodes.Node
for i := range afterChildren {
if afterChildren[i].Title == notes.NotesFolder && afterChildren[i].Type == "folder" {
notesFolder = &afterChildren[i]
break
}
}
if notesFolder == nil {
t.Fatal("Notes folder should exist after repair")
}
notesItems, err := app.ListItems(notesFolder.ID)
if err != nil {
t.Fatalf("ListItems(Notes) after repair: %v", err)
}
var foundInNotes bool
for _, item := range notesItems {
if item.ID == noteNode.ID && item.Type == "note" {
foundInNotes = true
break
}
}
if !foundInNotes {
t.Errorf("ListItems(Notes) should show the repaired note, got %d items", len(notesItems))
for _, item := range notesItems {
t.Logf(" %s (type=%s)", item.Name, item.Type)
}
}
// Verify node parent was updated
note, err := app.nodes.Get(noteNode.ID)
if err != nil {
t.Fatalf("get note node after repair: %v", err)
}
if note.ParentID == nil || *note.ParentID != notesFolder.ID {
t.Errorf("note.ParentID should be Notes folder (%s), got %v", notesFolder.ID, note.ParentID)
}
// Verify file path was updated
recs, err := app.files.ListByNode(noteNode.ID)
if err != nil {
t.Fatalf("ListByNode after repair: %v", err)
}
if len(recs) == 0 {
t.Fatal("file record should exist after repair")
}
expectedRelPath := filepath.Join(parent.FsPath, notes.NotesFolder, "LegacyNote.md")
if recs[0].Path != expectedRelPath {
t.Errorf("file path should be %q, got %q", expectedRelPath, recs[0].Path)
}
}
+46 -10
View File
@@ -1121,13 +1121,11 @@ func TestVaultLayout_TemplateDefaultFoldersCreatedAsNodes(t *testing.T) {
"Documents": "folder",
"Notes": "folder",
"Files": "folder",
"Overview": "note",
}
for _, child := range children {
expectedType, ok := expected[child.Title]
if !ok {
t.Errorf("unexpected child %q (type=%q)", child.Title, child.Type)
continue
continue // Overview is now under Notes/, not a direct child
}
if child.Type != expectedType {
t.Errorf("child %q expected type %q, got %q", child.Title, expectedType, child.Type)
@@ -1142,8 +1140,30 @@ func TestVaultLayout_TemplateDefaultFoldersCreatedAsNodes(t *testing.T) {
}
}
}
if len(children) < 4 {
t.Errorf("expected at least 4 children (3 folders + 1 note), got %d", len(children))
if len(children) < 3 {
t.Errorf("expected at least 3 children (3 folders), got %d", len(children))
}
// Overview should be inside Notes folder, not a direct child
var notesFolder *nodes.Node
for i := range children {
if children[i].Title == "Notes" && children[i].Type == "folder" {
notesFolder = &children[i]
break
}
}
if notesFolder == nil {
t.Fatalf("Notes folder not found among children")
}
noteChildren, err := app.nodes.ListChildren(notesFolder.ID, false)
if err != nil {
t.Fatalf("list Notes children: %v", err)
}
if len(noteChildren) != 1 || noteChildren[0].Title != "Overview" {
t.Errorf("Notes folder should have one child 'Overview', got %d children", len(noteChildren))
for _, c := range noteChildren {
t.Logf(" child: %s (type=%s)", c.Title, c.Type)
}
}
}
@@ -1156,21 +1176,37 @@ func TestVaultLayout_TemplateDefaultFileCreatedAsNodeWithFileRecord(t *testing.T
t.Fatalf("create project: %v", err)
}
// Find the Overview note child
// Overview is now created inside Notes/ folder, find it there
children, err := app.nodes.ListChildren(proj.ID, false)
if err != nil {
t.Fatalf("list children: %v", err)
}
var overview *nodes.Node
var notesFolder *nodes.Node
for i := range children {
if children[i].Title == "Overview" {
overview = &children[i]
if children[i].Title == "Notes" && children[i].Type == "folder" {
notesFolder = &children[i]
break
}
}
if notesFolder == nil {
t.Fatal("expected Notes folder child from template")
}
noteChildren, err := app.nodes.ListChildren(notesFolder.ID, false)
if err != nil {
t.Fatalf("list Notes children: %v", err)
}
var overview *nodes.Node
for i := range noteChildren {
if noteChildren[i].Title == "Overview" {
overview = &noteChildren[i]
break
}
}
if overview == nil {
t.Fatal("expected 'Overview' child node from template")
t.Fatal("expected 'Overview' child node from template inside Notes/")
}
if overview.Type != "note" {
t.Errorf("expected type 'note', got %q", overview.Type)