feat(workspace): tabula-rasa outliner architecture, SQLite blocks/block_tags schema & echo-protected live room sync
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m12s

This commit is contained in:
sandrodach committed 2026-10-26 21:14:06 +01:00
1 parent f3b3f07e04
commit 86867b0d6f
17 files changed
+3409 -7178

No files matched your search

+50 -155
View File
@@ -3,7 +3,6 @@ package api
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -842,9 +841,9 @@ func TestWebhookTriageBadgeAndIngestion(t *testing.T) {
}
}
func TestDocumentCRDTEndpoints(t *testing.T) {
func TestDocRoomLiveStateAndDebouncedFlush(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "crdt_api_test.db")
dbPath := filepath.Join(tmpDir, "docroom_test.db")
store, err := storage.NewStore(dbPath)
if err != nil {
@@ -860,59 +859,76 @@ func TestDocumentCRDTEndpoints(t *testing.T) {
srv := NewServer(cfg, store, hub, nil)
router := srv.Router()
user, err := store.CreateUser("crdt_api@example.com", "hash", "CRDT API User", "member")
user, err := store.CreateUser("outliner_ws@example.com", "hash", "WS Outliner User", "member")
if err != nil {
t.Fatalf("CreateUser failed: %v", err)
}
token, _ := auth.GenerateToken(cfg.JWTSecret, user.ID, user.Email, user.Role)
doc, err := store.CreateDocument(user.ID, "", "API CRDT Doc", "📄", "", nil, nil, false)
doc, err := store.CreateDocument(user.ID, "", "Initial Title", "📄", "", nil, nil, false)
if err != nil {
t.Fatalf("CreateDocument failed: %v", err)
}
// 1. Initial GET /api/v1/documents/{id}/crdt
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/"+doc.ID+"/crdt", nil)
// 1. Mutate in-memory DocRoom before 1.5s flush fires
room := hub.GetOrCreateDocRoom(doc.ID)
newContent := "Live RAM Block #ramtag"
newLevel := 1
room.HandleEvent(ws.DocSyncEvent{
Type: "title_updated",
ClientID: "cli_1",
Title: "RAM Updated Title",
}, nil)
room.HandleEvent(ws.DocSyncEvent{
Type: "block_created",
ClientID: "cli_1",
AfterBlockID: doc.Blocks[0].ID,
Block: &domain.DocumentBlock{
ID: "blk_ram_2",
Type: "text",
Content: newContent,
Level: newLevel,
},
}, nil)
// 2. Verify GET /api/v1/documents/{id} immediately returns the un-flushed in-memory state
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/"+doc.ID, nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from GET crdt, got %d: %s", rec.Code, rec.Body.String())
t.Fatalf("expected 200 OK from GET document, got %d: %s", rec.Code, rec.Body.String())
}
var liveDoc domain.Document
_ = json.Unmarshal(rec.Body.Bytes(), &liveDoc)
if liveDoc.Title != "RAM Updated Title" {
t.Fatalf("expected live RAM title 'RAM Updated Title', got '%s'", liveDoc.Title)
}
if len(liveDoc.Blocks) != 2 || liveDoc.Blocks[1].Content != newContent {
t.Fatalf("expected 2 live blocks with RAM content, got %+v", liveDoc.Blocks)
}
// 2. PUT /api/v1/documents/{id}/crdt with base64 state
savePayload := []byte(`{"state":"AQIDBA=="}`)
req = httptest.NewRequest(http.MethodPut, "/api/v1/documents/"+doc.ID+"/crdt", bytes.NewReader(savePayload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
rec = httptest.NewRecorder()
router.ServeHTTP(rec, req)
// 3. Flush room to SQLite and verify persistence + tag extraction + markdown_cache
room.Flush()
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from PUT crdt, got %d: %s", rec.Code, rec.Body.String())
persistedDoc, err := store.GetDocumentByID(user.ID, doc.ID)
if err != nil {
t.Fatalf("GetDocumentByID failed: %v", err)
}
// 3. Verify state persisted
req = httptest.NewRequest(http.MethodGet, "/api/v1/documents/"+doc.ID+"/crdt", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec = httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from GET crdt, got %d", rec.Code)
if persistedDoc.Title != "RAM Updated Title" {
t.Fatalf("expected persisted title 'RAM Updated Title', got '%s'", persistedDoc.Title)
}
var crdtResp struct {
DocID string `json:"docId"`
State string `json:"state"`
if !strings.Contains(persistedDoc.MarkdownCache, " - Live RAM Block #ramtag") {
t.Fatalf("expected compiled markdown_cache to contain indented bullet, got:\n%s", persistedDoc.MarkdownCache)
}
_ = json.Unmarshal(rec.Body.Bytes(), &crdtResp)
if crdtResp.State != "AQIDBA==" {
t.Fatalf("expected state 'AQIDBA==', got '%s'", crdtResp.State)
tags, _ := store.GetWorkspaceTags(user.ID)
if len(tags) != 1 || tags[0].Tag != "ramtag" {
t.Fatalf("expected extracted #ramtag in block_tags, got %+v", tags)
}
}
func TestSSEAndAwarenessEndpoints(t *testing.T) {
func TestSSEGlobalEndpoint(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "test_sse.db")
store, err := storage.NewStore(dbPath)
if err != nil {
@@ -921,7 +937,6 @@ func TestSSEAndAwarenessEndpoints(t *testing.T) {
defer store.Close()
hub := ws.NewHub()
hub.SetCRDTStore(store)
cfg := &config.Config{
JWTSecret: "test-jwt-secret-key",
AllowRegistration: false,
@@ -929,7 +944,6 @@ func TestSSEAndAwarenessEndpoints(t *testing.T) {
srv := NewServer(cfg, store, hub, nil)
router := srv.Router()
// 1. Setup user
setupBody := []byte(`{
"email": "sse_user@example.com",
"password": "secretpassword",
@@ -952,51 +966,13 @@ func TestSSEAndAwarenessEndpoints(t *testing.T) {
_ = json.Unmarshal(rec.Body.Bytes(), &authResp)
token := authResp.Token
// 2. Create Document
doc, err := store.CreateDocument(authResp.User.ID, "", "SSE Collaborative Note", "📄", "", nil, nil, false)
if err != nil {
t.Fatalf("failed to create document: %v", err)
}
// 3. Test Awareness POST and DELETE
awarenessBody := []byte(`{"userId":"` + authResp.User.ID + `","activeBlockId":"blk_123"}`)
req = httptest.NewRequest(http.MethodPost, "/api/v1/documents/"+doc.ID+"/awareness", bytes.NewReader(awarenessBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
rec = httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from POST awareness, got %d: %s", rec.Code, rec.Body.String())
}
req = httptest.NewRequest(http.MethodDelete, "/api/v1/documents/"+doc.ID+"/awareness", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec = httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from DELETE awareness, got %d", rec.Code)
}
// 4. Test Delta CRDT Update via PUT /api/v1/documents/{id}/crdt
updateBody := []byte(`{"update":"AQIDBA=="}`)
req = httptest.NewRequest(http.MethodPut, "/api/v1/documents/"+doc.ID+"/crdt", bytes.NewReader(updateBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
rec = httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from PUT crdt update, got %d: %s", rec.Code, rec.Body.String())
}
// 5. Test SSE Connection GET /api/v1/events
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
req = httptest.NewRequest(http.MethodGet, "/api/v1/events?token="+token+"&docId="+doc.ID, nil).WithContext(ctx)
req = httptest.NewRequest(http.MethodGet, "/api/v1/events?token="+token, nil).WithContext(ctx)
rec = httptest.NewRecorder()
go func() {
// Cancel context after short moment to terminate stream cleanly
time.Sleep(50 * time.Millisecond)
cancel()
}()
@@ -1007,92 +983,11 @@ func TestSSEAndAwarenessEndpoints(t *testing.T) {
if !strings.Contains(contentType, "text/event-stream") {
t.Fatalf("expected Content-Type text/event-stream, got %s", contentType)
}
if rec.Header().Get("X-Accel-Buffering") != "no" {
t.Fatalf("expected X-Accel-Buffering: no, got %s", rec.Header().Get("X-Accel-Buffering"))
}
bodyStr := rec.Body.String()
if !strings.Contains(bodyStr, "SSE_CONNECTED") {
t.Fatalf("expected SSE_CONNECTED event in body, got: %s", bodyStr)
}
}
func TestDocRoomDualTransportAndDebouncedPersistence(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "test_dual_transport.db")
store, err := storage.NewStore(dbPath)
if err != nil {
t.Fatalf("failed to init store: %v", err)
}
defer store.Close()
cfg := &config.Config{
JWTSecret: "test-secret-dual-transport",
AllowRegistration: true,
}
hub := ws.NewHub()
srv := NewServer(cfg, store, hub, nil)
router := srv.Router()
user, _ := store.CreateUser("dual@test.local", "hash", "Dual User", "member")
token, _ := auth.GenerateToken(cfg.JWTSecret, user.ID, user.Email, user.Role)
doc, _ := store.CreateDocument(user.ID, "", "Dual Transport Doc", "📄", "", nil, nil, false)
// 1. Test POST /api/v1/sync/docs/{id}/update with JSON base64
upd1 := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03})
req := httptest.NewRequest(http.MethodPost, "/api/v1/sync/docs/"+doc.ID+"/update", bytes.NewReader([]byte(`{"update":"`+upd1+`"}`)))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from sync update, got %d: %s", rec.Code, rec.Body.String())
}
// 2. Test POST /api/v1/sync/docs/{id}/update with octet-stream binary
binaryUpd := []byte{0x04, 0x05, 0x06}
req = httptest.NewRequest(http.MethodPost, "/api/v1/sync/docs/"+doc.ID+"/update", bytes.NewReader(binaryUpd))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/octet-stream")
rec = httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from binary sync update, got %d: %s", rec.Code, rec.Body.String())
}
// 3. Test GET /api/v1/sync/docs/{id}/stream SSE connection
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
req = httptest.NewRequest(http.MethodGet, "/api/v1/sync/docs/"+doc.ID+"/stream?token="+token, nil).WithContext(ctx)
rec = httptest.NewRecorder()
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
router.ServeHTTP(rec, req)
if !strings.Contains(rec.Header().Get("Content-Type"), "text/event-stream") {
t.Fatalf("expected text/event-stream, got %s", rec.Header().Get("Content-Type"))
}
bodyStr := rec.Body.String()
if !strings.Contains(bodyStr, "CRDT_INIT") {
t.Fatalf("expected CRDT_INIT in SSE stream, got %s", bodyStr)
}
// 4. Test Lock Neutrality: document update and block patch work without 423 Locked
updateReq := httptest.NewRequest(http.MethodPut, "/api/v1/documents/"+doc.ID, bytes.NewReader([]byte(`{"title":"Updated Without Locks"}`)))
updateReq.Header.Set("Authorization", "Bearer "+token)
updateReq.Header.Set("Content-Type", "application/json")
updateRec := httptest.NewRecorder()
router.ServeHTTP(updateRec, updateReq)
if updateRec.Code != http.StatusOK {
t.Fatalf("expected 200 OK from document update, got %d: %s", updateRec.Code, updateRec.Body.String())
}
}