// ingestion/internal/api/handler_test.go package api_test import ( "bytes" "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mathiasbq/hyperguild/ingestion/internal/api" "github.com/mathiasbq/hyperguild/ingestion/internal/pipeline" ) // stubComplete returns a fixed JSON page so tests never call a real LLM. func stubComplete(_ context.Context, _, _ string) (string, error) { return `[{"path":"wiki/sources/test-source.md","content":"# Test Source\n\nSome content here.\n"}]`, nil } func stubPipelineCfg() pipeline.Config { return pipeline.Config{ Complete: stubComplete, ChunkSize: 0, Schema: "# Test Schema\nwiki/sources/, wiki/concepts/, wiki/entities/", } } func setup(t *testing.T) (string, *api.Handler) { t.Helper() dir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(dir, "knowledge"), 0o755)) require.NoError(t, os.WriteFile( filepath.Join(dir, "knowledge", "tdd.md"), []byte("---\ntitle: TDD\ndomain: software\n---\n\nTest-driven development is a discipline.\n"), 0o644, )) logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) return dir, api.NewHandler(dir, logger, stubPipelineCfg()) } // --------------------------------------------------------------------------- // Existing tests (Write / Query) // --------------------------------------------------------------------------- func TestQuery_ReturnsResults(t *testing.T) { _, h := setup(t) body, _ := json.Marshal(map[string]any{"query": "test driven", "limit": 5}) req := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Query(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) results := resp["results"].([]any) assert.NotEmpty(t, results) } func TestWrite_CreatesKnowledgeFile(t *testing.T) { dir, h := setup(t) body, _ := json.Marshal(map[string]any{ "content": "# Test note\n\nSome content.", "filename": "test-note.md", }) req := httptest.NewRequest(http.MethodPost, "/write", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Write(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var resp map[string]string require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) assert.NotEmpty(t, resp["path"]) content, err := os.ReadFile(filepath.Join(dir, "knowledge", "test-note.md")) require.NoError(t, err) assert.Contains(t, string(content), "Some content.") } func TestQuery_RequiresQuery(t *testing.T) { _, h := setup(t) body, _ := json.Marshal(map[string]any{"limit": 5}) req := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Query(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestWrite_IncludesFrontmatterWhenTypeProvided(t *testing.T) { dir, h := setup(t) body, _ := json.Marshal(map[string]any{ "content": "Some learning.", "filename": "typed-note.md", "type": "concept", "domain": "software", }) req := httptest.NewRequest(http.MethodPost, "/write", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Write(rec, req) assert.Equal(t, http.StatusOK, rec.Code) content, err := os.ReadFile(filepath.Join(dir, "knowledge", "typed-note.md")) require.NoError(t, err) assert.Contains(t, string(content), "type: concept") assert.Contains(t, string(content), "domain: software") assert.Contains(t, string(content), "Some learning.") } func TestWrite_GeneratesFilenameIfAbsent(t *testing.T) { dir, h := setup(t) body, _ := json.Marshal(map[string]any{"content": "auto name"}) req := httptest.NewRequest(http.MethodPost, "/write", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Write(rec, req) assert.Equal(t, http.StatusOK, rec.Code) entries, _ := os.ReadDir(filepath.Join(dir, "knowledge")) // +1 because setup already wrote tdd.md assert.Len(t, entries, 2) assert.True(t, strings.HasSuffix(entries[1].Name(), ".md")) } // --------------------------------------------------------------------------- // POST /ingest // --------------------------------------------------------------------------- func TestIngest_MissingContent(t *testing.T) { _, h := setup(t) body, _ := json.Marshal(map[string]any{"source": "test-source"}) req := httptest.NewRequest(http.MethodPost, "/ingest", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Ingest(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestIngest_MissingSource(t *testing.T) { _, h := setup(t) body, _ := json.Marshal(map[string]any{"content": "some content"}) req := httptest.NewRequest(http.MethodPost, "/ingest", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Ingest(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestIngest_Success(t *testing.T) { _, h := setup(t) body, _ := json.Marshal(map[string]any{ "content": "some content about shape-up methodology", "source": "shape-up-book", "dry_run": true, }) req := httptest.NewRequest(http.MethodPost, "/ingest", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Ingest(rec, req) require.Equal(t, http.StatusOK, rec.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) pages, ok := resp["pages"] require.True(t, ok, "response must have pages field") pagesSlice, ok := pages.([]any) require.True(t, ok, "pages must be an array") assert.NotEmpty(t, pagesSlice) } // --------------------------------------------------------------------------- // POST /ingest-path // --------------------------------------------------------------------------- func TestIngestPath_MissingPath(t *testing.T) { _, h := setup(t) body, _ := json.Marshal(map[string]any{"source": "test-source"}) req := httptest.NewRequest(http.MethodPost, "/ingest-path", bytes.NewReader(body)) rec := httptest.NewRecorder() h.IngestPath(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestIngestPath_File(t *testing.T) { _, h := setup(t) // Create a temp file with content dir := t.TempDir() f := filepath.Join(dir, "doc.md") require.NoError(t, os.WriteFile(f, []byte("# Hello\nThis is markdown content."), 0o644)) body, _ := json.Marshal(map[string]any{ "path": f, "source": "test-doc", "dry_run": true, }) req := httptest.NewRequest(http.MethodPost, "/ingest-path", bytes.NewReader(body)) rec := httptest.NewRecorder() h.IngestPath(rec, req) require.Equal(t, http.StatusOK, rec.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) pages, ok := resp["pages"] require.True(t, ok, "response must have pages field") pagesSlice, ok := pages.([]any) require.True(t, ok, "pages must be an array") assert.NotEmpty(t, pagesSlice) } func TestIngestPath_Directory(t *testing.T) { _, h := setup(t) // Create a temp dir with one .md file dir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dir, "notes.md"), []byte("# Notes\nSome notes."), 0o644)) body, _ := json.Marshal(map[string]any{ "path": dir, "dry_run": true, }) req := httptest.NewRequest(http.MethodPost, "/ingest-path", bytes.NewReader(body)) rec := httptest.NewRecorder() h.IngestPath(rec, req) require.Equal(t, http.StatusOK, rec.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) pages, ok := resp["pages"] require.True(t, ok, "response must have pages field") pagesSlice, ok := pages.([]any) require.True(t, ok, "pages must be an array") assert.NotEmpty(t, pagesSlice) }