Files tab: .md → note editor via CheckFileAction + frontend
Backend: - FindByFileID: notes+files JOIN query - LinkFile: INSERT OR IGNORE notes record - CheckFileAction binding: note/preview/external/auto-link Frontend (App.svelte): - Import isMarkdownFile from fileUtils - openPreview now calls CheckFileAction for .md files - .md+note → switch to Notes tab + note editor - .md outside Notes/ → inline preview - non-.md → unchanged Tests: 7 new (FindByFileID×3, CheckFileAction×4), all PASS
This commit is contained in:
parent
fec35f55b8
commit
bfe57ac0ac
|
|
@ -191,6 +191,14 @@ type FileTreeItemDTO struct {
|
||||||
HasKids bool `json:"hasKids"`
|
HasKids bool `json:"hasKids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreflightFileAction describes what should happen when opening a file from the Files tab.
|
||||||
|
type PreflightFileAction struct {
|
||||||
|
Action string `json:"action"` // "note" | "preview" | "external"
|
||||||
|
NoteID string `json:"noteId,omitempty"`
|
||||||
|
NoteTitle string `json:"noteTitle,omitempty"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
}
|
||||||
|
|
||||||
type ActionDTO struct {
|
type ActionDTO struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
NodeID string `json:"nodeId"`
|
NodeID string `json:"nodeId"`
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"verstak/internal/core/activity"
|
"verstak/internal/core/activity"
|
||||||
"verstak/internal/core/files"
|
"verstak/internal/core/files"
|
||||||
"verstak/internal/core/nodes"
|
"verstak/internal/core/nodes"
|
||||||
|
|
@ -168,6 +172,44 @@ func (a *App) ValidateName(name string) error {
|
||||||
return files.ValidateName(name)
|
return files.ValidateName(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) CheckFileAction(fileID string) (*PreflightFileAction, error) {
|
||||||
|
if err := a.requireVault(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fileRec, err := a.files.Get(fileID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get file: %w", err)
|
||||||
|
}
|
||||||
|
name := strings.ToLower(fileRec.Filename)
|
||||||
|
isMD := strings.HasSuffix(name, ".md") || strings.HasSuffix(name, ".markdown")
|
||||||
|
if !isMD {
|
||||||
|
return &PreflightFileAction{Action: "external", FileName: fileRec.Filename}, nil
|
||||||
|
}
|
||||||
|
// .md file — check for linked note
|
||||||
|
noteRec, err := a.notes.FindByFileID(fileID)
|
||||||
|
if err == nil && noteRec != nil {
|
||||||
|
noteNode, nodeErr := a.nodes.Get(noteRec.NodeID)
|
||||||
|
title := fileRec.Filename
|
||||||
|
if nodeErr == nil && noteNode != nil {
|
||||||
|
title = noteNode.Title
|
||||||
|
}
|
||||||
|
return &PreflightFileAction{Action: "note", NoteID: noteRec.NodeID, NoteTitle: title, FileName: fileRec.Filename}, nil
|
||||||
|
}
|
||||||
|
// .md inside Notes/ with no note record — auto-link
|
||||||
|
pathLower := strings.ToLower(fileRec.Path)
|
||||||
|
insideNotes := strings.Contains(pathLower, string(filepath.Separator)+"notes"+string(filepath.Separator)) ||
|
||||||
|
strings.HasPrefix(pathLower, "notes"+string(filepath.Separator))
|
||||||
|
if insideNotes {
|
||||||
|
noteNode, nodeErr := a.nodes.Get(fileRec.NodeID)
|
||||||
|
if nodeErr == nil && noteNode != nil {
|
||||||
|
_ = a.notes.LinkFile(noteNode.ID, fileID, "markdown")
|
||||||
|
return &PreflightFileAction{Action: "note", NoteID: noteNode.ID, NoteTitle: noteNode.Title, FileName: fileRec.Filename}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .md outside Notes/ — internal preview
|
||||||
|
return &PreflightFileAction{Action: "preview", FileName: fileRec.Filename}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) PreviewImport(sourcePath string) (*files.ImportSummary, error) {
|
func (a *App) PreviewImport(sourcePath string) (*files.ImportSummary, error) {
|
||||||
if err := a.requireVault(); err != nil {
|
if err := a.requireVault(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@
|
||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-DwDG7FeH.js"></script>
|
<script type="module" crossorigin src="/assets/main-CzfuqGWF.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -246,3 +246,217 @@ func TestRepairMovesDirectNoteChildrenToNotesFolder(t *testing.T) {
|
||||||
t.Errorf("file path should be %q, got %q", expectedRelPath, recs[0].Path)
|
t.Errorf("file path should be %q, got %q", expectedRelPath, recs[0].Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TestCheckFileAction_NoteLinked verifies that CheckFileAction returns
|
||||||
|
// Action="note" for a .md file linked to a note record.
|
||||||
|
func TestCheckFileAction_NoteLinked(t *testing.T) {
|
||||||
|
app, _ := setupTestApp(t)
|
||||||
|
proj, err := app.CreateNodeFromTemplate("", "TestProj", "project.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create project: %v", err)
|
||||||
|
}
|
||||||
|
// Find the Overview note — it should be inside Notes folder, linked via notes record
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
notesChildren, err := app.nodes.ListChildren(notesFolder.ID, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListChildren(Notes): %v", err)
|
||||||
|
}
|
||||||
|
if len(notesChildren) == 0 {
|
||||||
|
t.Fatal("expected at least one note inside Notes folder")
|
||||||
|
}
|
||||||
|
// Get file ID for the Overview note
|
||||||
|
items, err := app.ListItems(notesFolder.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListItems: %v", err)
|
||||||
|
}
|
||||||
|
var overviewFileID string
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Type == "note" && item.Name == "Overview" {
|
||||||
|
overviewFileID = item.FileID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if overviewFileID == "" {
|
||||||
|
t.Fatal("Overview note has no FileID")
|
||||||
|
}
|
||||||
|
// CheckFileAction should return Action="note"
|
||||||
|
action, err := app.CheckFileAction(overviewFileID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CheckFileAction: %v", err)
|
||||||
|
}
|
||||||
|
if action.Action != "note" {
|
||||||
|
t.Errorf("expected Action=note for linked .md, got %q", action.Action)
|
||||||
|
}
|
||||||
|
if action.NoteID == "" {
|
||||||
|
t.Error("expected non-empty NoteID for linked note")
|
||||||
|
}
|
||||||
|
if action.NoteTitle == "" {
|
||||||
|
t.Error("expected non-empty NoteTitle")
|
||||||
|
}
|
||||||
|
if action.FileName == "" {
|
||||||
|
t.Error("expected non-empty FileName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckFileAction_ExternalForNonMD verifies that non-.md files return
|
||||||
|
// Action="external" from CheckFileAction.
|
||||||
|
func TestCheckFileAction_ExternalForNonMD(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
proj, err := app.CreateNodeFromTemplate("", "TestProj", "project.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create project: %v", err)
|
||||||
|
}
|
||||||
|
// Create a file node and record for a non-.md file
|
||||||
|
fileNode, err := app.nodes.Create(&proj.ID, nodes.TypeFile, "image.png", 0, "", filepath.Join(proj.FsPath, "image.png"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create file node: %v", err)
|
||||||
|
}
|
||||||
|
absPath := filepath.Join(vault, proj.FsPath, "image.png")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(absPath), 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(absPath, []byte("fake-png"), 0o640); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
// Insert file record directly
|
||||||
|
_, err = app.db.Exec(
|
||||||
|
`INSERT INTO files (id,node_id,filename,path,storage_mode,size,sha256,mime,created_at,updated_at,missing)
|
||||||
|
VALUES (?,?,?,?,'vault',0,'','image/png','2024-01-01T00:00:00Z','2024-01-01T00:00:00Z',0)`,
|
||||||
|
"file-png-"+fileNode.ID, fileNode.ID, "image.png", filepath.Join(proj.FsPath, "image.png"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert file record: %v", err)
|
||||||
|
}
|
||||||
|
// CheckFileAction should return Action="external"
|
||||||
|
action, err := app.CheckFileAction("file-png-" + fileNode.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CheckFileAction: %v", err)
|
||||||
|
}
|
||||||
|
if action.Action != "external" {
|
||||||
|
t.Errorf("expected Action=external for .png, got %q", action.Action)
|
||||||
|
}
|
||||||
|
if action.FileName != "image.png" {
|
||||||
|
t.Errorf("expected FileName=image.png, got %q", action.FileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckFileAction_PreviewForMDOutsideNotes verifies that .md files
|
||||||
|
// outside Notes/ without a note record return Action="preview".
|
||||||
|
func TestCheckFileAction_PreviewForMDOutsideNotes(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
proj, err := app.CreateNodeFromTemplate("", "TestProj", "project.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create project: %v", err)
|
||||||
|
}
|
||||||
|
// Create a .md file directly under the project (not inside Notes/)
|
||||||
|
mdNode, err := app.nodes.Create(&proj.ID, nodes.TypeFile, "readme.md", 0, "", filepath.Join(proj.FsPath, "readme.md"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create md node: %v", err)
|
||||||
|
}
|
||||||
|
absPath := filepath.Join(vault, proj.FsPath, "readme.md")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(absPath), 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(absPath, []byte("# Readme\n"), 0o640); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
// Insert file record directly
|
||||||
|
_, err = 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/markdown','2024-01-01T00:00:00Z','2024-01-01T00:00:00Z',0)`,
|
||||||
|
"file-md-"+mdNode.ID, mdNode.ID, "readme.md", filepath.Join(proj.FsPath, "readme.md"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert file record: %v", err)
|
||||||
|
}
|
||||||
|
// Do NOT create a notes record — this .md is outside Notes/
|
||||||
|
action, err := app.CheckFileAction("file-md-" + mdNode.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CheckFileAction: %v", err)
|
||||||
|
}
|
||||||
|
if action.Action != "preview" {
|
||||||
|
t.Errorf("expected Action=preview for .md outside Notes/, got %q", action.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckFileAction_AutoLinkInNotes(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")
|
||||||
|
}
|
||||||
|
// Create a .md file INSIDE Notes/ but WITHOUT a notes record
|
||||||
|
mdNode, err := app.nodes.Create(¬esFolder.ID, nodes.TypeFile, "orphan.md", 0, "",
|
||||||
|
filepath.Join(proj.FsPath, notes.NotesFolder, "orphan.md"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create md node: %v", err)
|
||||||
|
}
|
||||||
|
notesDir := filepath.Join(vault, proj.FsPath, notes.NotesFolder)
|
||||||
|
if err := os.MkdirAll(notesDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
absPath := filepath.Join(notesDir, "orphan.md")
|
||||||
|
if err := os.WriteFile(absPath, []byte("# Orphan Note\n"), 0o640); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
// Insert file record
|
||||||
|
_, err = 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/markdown','2024-01-01T00:00:00Z','2024-01-01T00:00:00Z',0)`,
|
||||||
|
"file-orphan-"+mdNode.ID, mdNode.ID, "orphan.md",
|
||||||
|
filepath.Join(proj.FsPath, notes.NotesFolder, "orphan.md"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert file record: %v", err)
|
||||||
|
}
|
||||||
|
// No notes record yet — just file + node
|
||||||
|
// CheckFileAction should auto-link and return Action="note"
|
||||||
|
action, err := app.CheckFileAction("file-orphan-" + mdNode.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CheckFileAction: %v", err)
|
||||||
|
}
|
||||||
|
if action.Action != "note" {
|
||||||
|
t.Errorf("expected Action=note for .md inside Notes/, got %q", action.Action)
|
||||||
|
}
|
||||||
|
if action.NoteID == "" {
|
||||||
|
t.Error("expected auto-linked NoteID")
|
||||||
|
}
|
||||||
|
if action.NoteTitle == "" {
|
||||||
|
t.Error("expected NoteTitle for auto-linked note")
|
||||||
|
}
|
||||||
|
// Verify notes record was actually created
|
||||||
|
noteRec, err := app.notes.FindByFileID("file-orphan-" + mdNode.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindByFileID after auto-link: %v", err)
|
||||||
|
}
|
||||||
|
if noteRec == nil {
|
||||||
|
t.Fatal("expected note record after auto-link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
import AppHeader from './lib/AppHeader.svelte'
|
import AppHeader from './lib/AppHeader.svelte'
|
||||||
import { onMount, onDestroy } from 'svelte'
|
import { onMount, onDestroy } from 'svelte'
|
||||||
import { actionIcon } from './lib/actionIcons.js'
|
import { actionIcon } from './lib/actionIcons.js'
|
||||||
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
|
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from './lib/fileUtils.js'
|
||||||
import { t } from './lib/i18n'
|
import { t } from './lib/i18n'
|
||||||
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
||||||
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
|
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
|
||||||
|
|
@ -622,6 +622,24 @@
|
||||||
// ===== File preview =====
|
// ===== File preview =====
|
||||||
|
|
||||||
async function openPreview(item) {
|
async function openPreview(item) {
|
||||||
|
// For .md files: check if linked to a note, open note editor instead of preview modal
|
||||||
|
if (item && item.fileId && isMarkdownFile(item)) {
|
||||||
|
try {
|
||||||
|
const action = await wailsCall('CheckFileAction', item.fileId)
|
||||||
|
if (action.action === 'note') {
|
||||||
|
await openNote({ id: action.noteId, title: action.noteTitle })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (action.action === 'external') {
|
||||||
|
await wailsCall('OpenFile', item.fileId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 'preview' → fall through to normal preview
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('CheckFileAction failed, falling back to preview:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
previewItem = item
|
previewItem = item
|
||||||
previewContent = ''
|
previewContent = ''
|
||||||
previewError = ''
|
previewError = ''
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,11 @@ func NewService(db *storage.DB, vaultRoot string, nodeRepo *nodes.Repository, fi
|
||||||
return &Service{db: db, vaultRoot: vaultRoot, nodes: nodeRepo, files: fileSvc}
|
return &Service{db: db, vaultRoot: vaultRoot, nodes: nodeRepo, files: fileSvc}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DB returns the underlying storage.DB for direct queries (used in tests).
|
||||||
|
func (s *Service) DB() *storage.DB {
|
||||||
|
return s.db
|
||||||
|
}
|
||||||
|
|
||||||
// FindNotesFolder returns the TypeFolder "Notes" node under parentID, or nil.
|
// FindNotesFolder returns the TypeFolder "Notes" node under parentID, or nil.
|
||||||
func (s *Service) FindNotesFolder(parentID string) *nodes.Node {
|
func (s *Service) FindNotesFolder(parentID string) *nodes.Node {
|
||||||
children, err := s.nodes.ListChildren(parentID, false)
|
children, err := s.nodes.ListChildren(parentID, false)
|
||||||
|
|
@ -683,6 +688,37 @@ func (s *Service) repairNoteFilePath(noteID, caseID string, res *RepairResult) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// FindByFileID looks up the note record associated with the given file ID.
|
||||||
|
|
||||||
|
|
||||||
|
// LinkFile creates a notes record linking an existing node to an existing file record.
|
||||||
|
// It is a no-op if a record for node_id already exists (idempotent).
|
||||||
|
func (s *Service) LinkFile(nodeID, fileID, format string) error {
|
||||||
|
if format == "" {
|
||||||
|
format = "markdown"
|
||||||
|
}
|
||||||
|
_, err := s.db.Exec(
|
||||||
|
`INSERT OR IGNORE INTO notes (node_id, file_id, format) VALUES (?,?,?)`,
|
||||||
|
nodeID, fileID, format,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) FindByFileID(fileID string) (*Record, error) {
|
||||||
|
row := s.db.QueryRow(
|
||||||
|
`SELECT node_id, file_id, format, encrypted FROM notes WHERE file_id=?`, fileID,
|
||||||
|
)
|
||||||
|
var rec Record
|
||||||
|
var enc int
|
||||||
|
if err := row.Scan(&rec.NodeID, &rec.FileID, &rec.Format, &enc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rec.Encrypted = enc != 0
|
||||||
|
return &rec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// fileExists returns true if path refers to an existing file or directory.
|
// fileExists returns true if path refers to an existing file or directory.
|
||||||
func fileExists(path string) bool {
|
func fileExists(path string) bool {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
|
|
|
||||||
|
|
@ -1431,3 +1431,95 @@ func TestFilesTabManualMoveRepair(t *testing.T) {
|
||||||
t.Error("Files tab should show Overview note inside Notes folder after repair")
|
t.Error("Files tab should show Overview note inside Notes folder after repair")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestFindByFileID_Success(t *testing.T) {
|
||||||
|
svc, nodeRepo, _, vaultDir := setupRepairTest(t)
|
||||||
|
caseNode := createCaseNode(t, nodeRepo, "TestCase", "test_case")
|
||||||
|
// Create a note via the full Create path...
|
||||||
|
noteNode, fileRec, err := svc.Create(caseNode.ID, "TestNote", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create note: %v", err)
|
||||||
|
}
|
||||||
|
// Verify FindByFileID works
|
||||||
|
rec, err := svc.FindByFileID(fileRec.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindByFileID: %v", err)
|
||||||
|
}
|
||||||
|
if rec == nil {
|
||||||
|
t.Fatal("expected non-nil record")
|
||||||
|
}
|
||||||
|
if rec.NodeID != noteNode.ID {
|
||||||
|
t.Errorf("expected NodeID=%s, got %s", noteNode.ID, rec.NodeID)
|
||||||
|
}
|
||||||
|
if rec.FileID != fileRec.ID {
|
||||||
|
t.Errorf("expected FileID=%s, got %s", fileRec.ID, rec.FileID)
|
||||||
|
}
|
||||||
|
if rec.Format != "markdown" {
|
||||||
|
t.Errorf("expected Format=markdown, got %s", rec.Format)
|
||||||
|
}
|
||||||
|
_ = vaultDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindByFileID_NotFound(t *testing.T) {
|
||||||
|
svc, _, _, _ := setupRepairTest(t)
|
||||||
|
rec, err := svc.FindByFileID("nonexistent-file-id")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for nonexistent file ID")
|
||||||
|
}
|
||||||
|
if rec != nil {
|
||||||
|
t.Error("expected nil record")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindByFileID_AfterLinkFile(t *testing.T) {
|
||||||
|
svc, nodeRepo, _, vaultDir := setupRepairTest(t)
|
||||||
|
caseNode, err := nodeRepo.Create(nil, nodes.TypeCase, "TestCase", 0, "", "test_case_link")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create case node: %v", err)
|
||||||
|
}
|
||||||
|
// Create directory on disk
|
||||||
|
caseDir := filepath.Join(vaultDir, "test_case_link")
|
||||||
|
if err := os.MkdirAll(caseDir, 0o750); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
// Write a physical .md file
|
||||||
|
mdPath := filepath.Join(caseDir, "mynote.md")
|
||||||
|
if err := os.WriteFile(mdPath, []byte("# mynote"), 0o640); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
// Create a file node
|
||||||
|
fileNode, err := nodeRepo.Create(&caseNode.ID, nodes.TypeFile, "mynote.md", 0, "", "test_case_link/mynote.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create file node: %v", err)
|
||||||
|
}
|
||||||
|
// Insert file record directly (insertRecord is private, so we use DB())
|
||||||
|
db := svc.DB()
|
||||||
|
if db == nil {
|
||||||
|
t.Fatal("DB() returned nil")
|
||||||
|
}
|
||||||
|
_, err = db.Exec(
|
||||||
|
`INSERT INTO files (id,node_id,filename,path,storage_mode,size,sha256,mime,created_at,updated_at,missing)
|
||||||
|
VALUES (?,?,?,?,'vault',0,'','text/markdown','2024-01-01T00:00:00Z','2024-01-01T00:00:00Z',0)`,
|
||||||
|
"file-"+fileNode.ID, fileNode.ID, "mynote.md", "test_case_link/mynote.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert file record: %v", err)
|
||||||
|
}
|
||||||
|
// Link note record via LinkFile
|
||||||
|
if err := svc.LinkFile(fileNode.ID, "file-"+fileNode.ID, "markdown"); err != nil {
|
||||||
|
t.Fatalf("LinkFile: %v", err)
|
||||||
|
}
|
||||||
|
// Verify
|
||||||
|
rec, err := svc.FindByFileID("file-" + fileNode.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindByFileID: %v", err)
|
||||||
|
}
|
||||||
|
if rec == nil {
|
||||||
|
t.Fatal("expected non-nil record")
|
||||||
|
}
|
||||||
|
if rec.NodeID != fileNode.ID {
|
||||||
|
t.Errorf("expected NodeID=%s, got %s", fileNode.ID, rec.NodeID)
|
||||||
|
}
|
||||||
|
_ = vaultDir
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue