Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions internal/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,15 @@ func Load(root string) (*Bundle, error) {
relPath = filepath.ToSlash(relPath)

// Reserved filenames are loaded separately; they may lack frontmatter.
// ParseReserved reads the file once and tolerates a missing frontmatter
// block (e.g. generated index.md), but still surfaces malformed
// frontmatter and other errors rather than silently dropping the file.
if concept.ReservedNames[strings.ToLower(d.Name())] {
c, perr := concept.Parse(path, relPath)
if perr == nil {
b.Reserved = append(b.Reserved, c)
c, perr := concept.ParseReserved(path, relPath)
if perr != nil {
return fmt.Errorf("parse reserved %s: %w", relPath, perr)
}
b.Reserved = append(b.Reserved, c)
return nil
}

Expand Down
111 changes: 111 additions & 0 deletions internal/bundle/bundle_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package bundle

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/okfcli/okf/internal/concept"
"github.com/okfcli/okf/internal/index"
)

func TestLoad_ValidBundle(t *testing.T) {
Expand Down Expand Up @@ -64,3 +70,108 @@ func TestLoad_FrontmatterParsed(t *testing.T) {
t.Errorf("tags len = %d, want 3", len(c.Frontmatter.Tags))
}
}

// TestLoad_ReservedIndexNoFrontmatter verifies that a reserved index.md file
// without YAML frontmatter (as generated by `okf index`) is still loaded into
// Reserved with a correct ID, Path, and raw Body content.
func TestLoad_ReservedIndexNoFrontmatter(t *testing.T) {
tmp := t.TempDir()

// Write a concept file with frontmatter so the bundle is non-empty.
conceptDir := filepath.Join(tmp, "tables")
if err := os.MkdirAll(conceptDir, 0o755); err != nil {
t.Fatalf("mkdir tables: %v", err)
}
conceptContent := "---\ntype: BigQuery Table\ntitle: Events\n---\n\nEvent data.\n"
if err := os.WriteFile(filepath.Join(conceptDir, "events_.md"), []byte(conceptContent), 0o644); err != nil {
t.Fatalf("write concept: %v", err)
}

// Write an index.md WITHOUT frontmatter (as `okf index` generates).
indexPath := filepath.Join(conceptDir, "index.md")
indexContent := "# Index\n\n- [events_](events_.md)\n"
if err := os.WriteFile(indexPath, []byte(indexContent), 0o644); err != nil {
t.Fatalf("write index: %v", err)
}

b, err := Load(tmp)
if err != nil {
t.Fatalf("Load failed: %v", err)
}

var found *concept.Concept
for _, r := range b.Reserved {
if r.ID == "tables/index" {
found = r
break
}
}
if found == nil {
t.Fatalf("expected Reserved to contain tables/index, got %d entries", len(b.Reserved))
}

// Path should be the absolute path to index.md.
absIndex, _ := filepath.Abs(indexPath)
if found.Path != absIndex {
t.Errorf("Path = %q, want %q", found.Path, absIndex)
}

// Body should contain the raw file content.
if found.Body != indexContent {
t.Errorf("Body = %q, want %q", found.Body, indexContent)
}
}

// TestLoad_GeneratedIndexIsDiscoverable is an end-to-end regression test for
// the user-visible symptom: after `okf index` generates index.md files (which
// have no frontmatter), reloading the bundle must expose them in b.Reserved so
// runIndex reports a non-empty indexes_written. Previously these files were
// silently dropped and the index command reported zero indexes.
func TestLoad_GeneratedIndexIsDiscoverable(t *testing.T) {
tmp := t.TempDir()

conceptDir := filepath.Join(tmp, "tables")
if err := os.MkdirAll(conceptDir, 0o755); err != nil {
t.Fatalf("mkdir tables: %v", err)
}
conceptContent := "---\ntype: BigQuery Table\ntitle: Events\n---\n\nEvent data.\n"
if err := os.WriteFile(filepath.Join(conceptDir, "events_.md"), []byte(conceptContent), 0o644); err != nil {
t.Fatalf("write concept: %v", err)
}

// Generate index.md files the same way `okf index` does.
if err := index.Generate(tmp); err != nil {
t.Fatalf("index.Generate: %v", err)
}

b, err := Load(tmp)
if err != nil {
t.Fatalf("Load failed: %v", err)
}

// Mirror runIndex's collection logic.
var indexFiles []string
for _, r := range b.Reserved {
if r.ID == "index" || strings.HasSuffix(r.ID, "/index") {
indexFiles = append(indexFiles, r.Path)
}
}
if len(indexFiles) == 0 {
t.Fatalf("expected at least one generated index in Reserved, got %d reserved entries", len(b.Reserved))
}

// The tables/ subdirectory index must be among them and carry its body.
var tablesIndex *concept.Concept
for _, r := range b.Reserved {
if r.ID == "tables/index" {
tablesIndex = r
break
}
}
if tablesIndex == nil {
t.Fatal("expected Reserved to contain tables/index")
}
if tablesIndex.Body == "" {
t.Error("generated index Body is empty, want raw index content")
}
}
25 changes: 25 additions & 0 deletions internal/concept/concept.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,31 @@ func ParseBytes(raw []byte, relPath, absPath string) (*Concept, error) {
}, nil
}

// ParseReserved reads and parses a reserved document (index.md, log.md) from
// path. Unlike Parse, a missing frontmatter block is not an error: such files
// (e.g. those generated by `okf index`) are returned as a Concept with empty
// Frontmatter and the raw file content as Body, so callers can still discover
// them. The file is read exactly once. Malformed-but-present frontmatter still
// returns an error rather than being silently dropped.
func ParseReserved(path, relPath string) (*Concept, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read %s: %w", path, err)
}
c, err := ParseBytes(raw, relPath, path)
if err != nil {
if errors.Is(err, ErrNoFrontmatter) {
return &Concept{
ID: conceptID(relPath),
Path: path,
Body: string(raw),
}, nil
}
return nil, err
}
return c, nil
}

// ConceptID converts a relative file path to a concept ID by stripping the .md
// suffix and normalizing separators to forward slashes.
// e.g. "tables/users.md" -> "tables/users"
Expand Down
Loading