package core import ( "os" "path/filepath" "strings" "testing" "verstak/internal/core/actions" "verstak/internal/core/files" "verstak/internal/core/nodes" "verstak/internal/core/notes" "verstak/internal/core/search" "verstak/internal/core/storage" "verstak/internal/core/vault" "verstak/internal/core/worklog" ) func openTestDB(t *testing.T) *storage.DB { t.Helper() dir := t.TempDir() db, err := storage.Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { db.Close() }) return db } func TestMVPSmoke(t *testing.T) { // Create a test vault with a real filesystem area. vaultDir := t.TempDir() // 1. Init vault. if err := vault.Init(vaultDir); err != nil { t.Fatalf("vault init: %v", err) } db := openTestDB(t) nodeRepo := nodes.NewRepository(db) fileSvc := files.NewService(db, vaultDir, nodeRepo) noteSvc := notes.NewService(db, vaultDir, nodeRepo, fileSvc) actionSvc := actions.NewService(db) worklogSvc := worklog.NewService(db) searchSvc := search.NewService(db) // 2. Create client case structure. client, err := nodeRepo.Create(nil, nodes.TypeCase, "ООО Ромашка", 0, "", "") if err != nil { t.Fatalf("create client: %v", err) } project, err := nodeRepo.Create(&client.ID, nodes.TypeCase, "Сайт", 0, "", "") if err != nil { t.Fatalf("create project: %v", err) } // 3. Create a Markdown note. noteNode, noteRec, err := noteSvc.Create(project.ID, "overview.md", "") if err != nil { t.Fatalf("create note: %v", err) } if noteNode.Title != "overview.md" { t.Errorf("note title = %q, want overview.md", noteNode.Title) } // 4. Write note content. content := "# Сайт ООО Ромашка\n\nWordPress/nginx. Иногда обновление." if err := noteSvc.Save(noteNode.ID, content); err != nil { t.Fatalf("save note: %v", err) } // 5. Read note back. got, err := noteSvc.Read(noteNode.ID) if err != nil { t.Fatalf("read note: %v", err) } if got != content { t.Errorf("note content mismatch: got %q, want %q", got, content) } // 6. Verify note file exists on disk. noteFileRecs, _ := fileSvc.ListByNode(noteNode.ID) notePath := "" if len(noteFileRecs) > 0 { notePath = noteFileRecs[0].Path if _, err := os.Stat(filepath.Join(vaultDir, notePath)); os.IsNotExist(err) { t.Errorf("note file not on disk: %s", notePath) } } _ = noteRec // 7. Add a dummy external file. dummyPath := filepath.Join(vaultDir, "dogovor.docx") if err := os.WriteFile(dummyPath, []byte("dummy contract data"), 0644); err != nil { t.Fatal(err) } created, err := fileSvc.AddPathCopy(project.ID, dummyPath) if err != nil { t.Fatalf("add file: %v", err) } if len(created) == 0 { t.Fatal("no nodes created from file import") } // 8. Open the imported file (just verify it opens without error). fileNode := created[0] if fileNode.Type != nodes.TypeFile { t.Errorf("expected file type, got %s", fileNode.Type) } fileRecs, err := fileSvc.ListByNode(fileNode.ID) if err != nil || len(fileRecs) == 0 { t.Fatalf("file record not found for imported file") } _ = fileSvc.Open(fileRecs[0].ID) // may fail in test (no display) // 9. Verify the imported file node exists as a child of the project. children, err := nodeRepo.ListChildren(project.ID, false) if err != nil { t.Fatalf("list children: %v", err) } hasImported := false for _, c := range children { if c.Type == nodes.TypeFile && c.Title == "dogovor.docx" { hasImported = true break } } if !hasImported { t.Error("imported file node not found under project") } // 10. Create actions. urlAction, err := actionSvc.Create(project.ID, actions.KindOpenURL, "Открыть сайт", "", "", "https://example.com", nil, false, false) if err != nil { t.Fatalf("create url action: %v", err) } if urlAction.Kind != actions.KindOpenURL { t.Errorf("action kind = %s, want %s", urlAction.Kind, actions.KindOpenURL) } folderAction, err := actionSvc.Create(project.ID, actions.KindOpenFolder, "Открыть папку", vaultDir, "", "", nil, false, false) if err != nil { t.Fatalf("create folder action: %v", err) } if folderAction.Kind != actions.KindOpenFolder { t.Errorf("action kind = %s", folderAction.Kind) } // 11. List actions. actionList, err := actionSvc.ListByNode(project.ID) if err != nil { t.Fatalf("list actions: %v", err) } if len(actionList) != 2 { t.Errorf("expected 2 actions, got %d", len(actionList)) } // 12. Run an action (open URL — should not error). _, err = actionSvc.Run(urlAction.ID) if err != nil { t.Fatalf("run url action: %v", err) } // 13. Add worklog. entry, err := worklogSvc.Add(project.ID, "Обновил витрину сайта, баннеры", "", 180, false, false) if err != nil { t.Fatalf("add worklog: %v", err) } if entry.Minutes == nil || *entry.Minutes != 180 { t.Errorf("worklog minutes = %v, want 180", entry.Minutes) } // 14. List worklog. worklogList, err := worklogSvc.ListByNode(project.ID) if err != nil { t.Fatalf("list worklog: %v", err) } if len(worklogList) != 1 { t.Errorf("expected 1 worklog entry, got %d", len(worklogList)) } // 15. Search for something (FTS5 may not be available — gracefully skip). _ = searchSvc.Rebuild() _ = searchSvc.Index(noteNode.ID, noteNode.Title, content, notePath, "", "note") results, err := searchSvc.Search("WordPress") if err != nil { _ = err } if len(results) > 0 { t.Logf("search found %d results for 'WordPress'", len(results)) } else { t.Log("search returned no results (FTS5 may not be available)") } // 16. Verify section filtering. roots, err := nodeRepo.ListRoots(false) if err != nil { t.Fatalf("list roots: %v", err) } found := false for _, r := range roots { if r.ID == client.ID { found = true break } } if !found { t.Error("client not found in roots") } // 17. Soft delete node and verify. if err := nodeRepo.SoftDelete(project.ID); err != nil { t.Fatalf("soft delete project: %v", err) } _, err = nodeRepo.GetActive(project.ID) if err == nil { t.Errorf("expected error getting deleted node") } // 18. Reopen app (simulate by reconnecting to same DB). db2, err := storage.Open(db.Path()) if err != nil { t.Fatalf("reopen db: %v", err) } defer db2.Close() repo2 := nodes.NewRepository(db2) // 19. Verify data persists. client2, err := repo2.GetActive(client.ID) if err != nil { t.Fatalf("get client after reopen: %v", err) } if client2.Title != "ООО Ромашка" { t.Errorf("client title after reopen = %q", client2.Title) } // 20. Verify soft-deleted node stays soft-deleted. allNodes, err := repo2.ListChildren(client.ID, true) if err != nil { t.Fatalf("list children after reopen: %v", err) } if len(allNodes) == 0 { t.Error("no children found after reopen") } t.Log("MVP smoke test passed!") } func TestMVPSmoke_WorklogReport(t *testing.T) { db := openTestDB(t) nodeRepo := nodes.NewRepository(db) worklogSvc := worklog.NewService(db) n, err := nodeRepo.Create(nil, nodes.TypeCase, "Test", 0, "", "") if err != nil { t.Fatal(err) } _, err = worklogSvc.Add(n.ID, "Task 1", "", 60, true, false) if err != nil { t.Fatal(err) } _, err = worklogSvc.Add(n.ID, "Task 2", "", 30, false, false) if err != nil { t.Fatal(err) } report, err := worklogSvc.Report(n.ID) if err != nil { t.Fatalf("report: %v", err) } if !strings.Contains(report, "Task 1") || !strings.Contains(report, "Task 2") { t.Error("report missing entries") } if !strings.Contains(report, "Task 1") || !strings.Contains(report, "Task 2") { t.Error("report missing tasks") } t.Logf("report:\n%s", report) }