feat: Sample Tag Viewer — read-only tag browser for AKAI disk images

Backend:
- extract.go: listTags() navigates to volume, runs akaiutil lstags
- extract.go: parseTags() parses tag name/index from lstags output
- serve.go: GET /api/disk/partitions — list partitions on an ISO
- serve.go: GET /api/disk/volumes — list volumes on a partition
- serve.go: GET /api/volume/tags — list tags on a volume

Web UI:
- Tag Viewer section with ISO path input and Browse button
- Partition selector buttons
- Volume grid with click-to-select
- Tag chips with color-coded dots (20-color palette)

Tests:
- 6 parseTags tests (basic, spaces, defaults, no tags, empty, no-type)
- 1 listTags integration test
- 4 API endpoint tests (partitions, missing-ISO, missing-params, tags)

Karpathy: read-only view first — no write operations yet (settagi, clrtagi)
This commit is contained in:
2026-06-22 03:21:54 -07:00
parent f804ad8a41
commit 568691542a
7 changed files with 565 additions and 2 deletions
+113
View File
@@ -368,3 +368,116 @@ func TestRunExtraction_CreatesVolumeDirs(t *testing.T) {
t.Error("expected lcd / to be in commands")
}
}
func TestParseTags(t *testing.T) {
t.Run("parses basic tag list", func(t *testing.T) {
out := `
tag name
-----------------
01 Drums
02 Bass
03 FX
04 Vox
05 Percussion
-----------------
`
tags := parseTags(out)
if len(tags) != 5 {
t.Fatalf("expected 5 tags, got %d", len(tags))
}
if tags[0].Index != 1 || tags[0].Name != "Drums" {
t.Errorf("tag 0: got {%d %s}, want {1 Drums}", tags[0].Index, tags[0].Name)
}
if tags[4].Index != 5 || tags[4].Name != "Percussion" {
t.Errorf("tag 4: got {%d %s}, want {5 Percussion}", tags[4].Index, tags[4].Name)
}
})
t.Run("parses tags with spaces in names", func(t *testing.T) {
out := `
tag name
-----------------
01 Kick Drum
02 Hi Hat
03 Snare Drum
-----------------
`
tags := parseTags(out)
if len(tags) != 3 {
t.Fatalf("expected 3 tags, got %d", len(tags))
}
if tags[0].Name != "Kick Drum" {
t.Errorf("got %q, want 'Kick Drum'", tags[0].Name)
}
})
t.Run("handles default tag names from akaiutil", func(t *testing.T) {
out := `
tag name
-----------------
01 TAG A
02 TAG B
03 MyTag
-----------------
`
tags := parseTags(out)
if len(tags) != 3 {
t.Fatalf("expected 3 tags, got %d", len(tags))
}
})
t.Run("no tags present message returns empty", func(t *testing.T) {
out := "no tags present\n"
tags := parseTags(out)
if len(tags) != 0 {
t.Errorf("expected 0 tags, got %d", len(tags))
}
})
t.Run("handles empty output", func(t *testing.T) {
tags := parseTags("")
if len(tags) != 0 {
t.Errorf("expected 0 tags, got %d", len(tags))
}
})
t.Run("handles no tags in partition type", func(t *testing.T) {
out := "no tags in this partition type\n"
tags := parseTags(out)
if len(tags) != 0 {
t.Errorf("expected 0 tags, got %d", len(tags))
}
})
}
func TestListTags_Integration(t *testing.T) {
origRun := runAkaiutil
defer func() { runAkaiutil = origRun }()
runAkaiutil = func(isoPath, commands string) (string, error) {
return `
/disk0/A > lstags
tag name
-----------------
01 Drums
02 Bass
03 FX
-----------------
`, nil
}
tags, err := listTags("fake.iso", "A", "SomeVolume")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(tags) != 3 {
t.Errorf("expected 3 tags, got %d", len(tags))
}
if tags[0].Name != "Drums" {
t.Errorf("expected Drums, got %s", tags[0].Name)
}
if tags[2].Index != 3 || tags[2].Name != "FX" {
t.Errorf("tag 2: got {%d %s}, want {3 FX}", tags[2].Index, tags[2].Name)
}
}