568691542a
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)
484 lines
10 KiB
Go
484 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestExtractISO_SkipsExistingWAVs(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
called := false
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
called = true
|
|
return "", nil
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
isoOutDir := filepath.Join(tmp, sanitiseISOName(filepath.Base("/fake/test.iso")))
|
|
if err := os.MkdirAll(isoOutDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"kick.wav", "snare.wav", "hihat.wav"} {
|
|
fd, err := os.Create(filepath.Join(isoOutDir, name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = fd.Close() }()
|
|
}
|
|
|
|
result, err := extractISO("/fake/test.iso", tmp)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if called {
|
|
t.Fatal("runAkaiutil should not have been called for existing WAVs")
|
|
}
|
|
if result == "" {
|
|
t.Fatal("expected non-empty result")
|
|
}
|
|
}
|
|
|
|
func TestExtractISO_ProceedsWhenNoWAVs(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
calls := 0
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
calls++
|
|
if commands == "df\nq\n" {
|
|
return `
|
|
Disk /disk0
|
|
A - S1000 HD (3840 blocks)
|
|
`, nil
|
|
}
|
|
return ` exported 3 file(s)
|
|
`, nil
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
result, err := extractISO("/fake/test.iso", tmp)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if calls == 0 {
|
|
t.Fatal("runAkaiutil should have been called")
|
|
}
|
|
if result == "" {
|
|
t.Fatal("expected non-empty result")
|
|
}
|
|
}
|
|
|
|
func TestListPartitions(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
if commands != "df\nq\n" {
|
|
t.Fatalf("unexpected commands: %q", commands)
|
|
}
|
|
return `
|
|
Disk /disk0
|
|
Total blocks: 12288 (8 KB each) 98304 KB
|
|
Partition table:
|
|
A - S1000 HD (3840 blocks)
|
|
B - S1000 HD (5120 blocks)
|
|
C - S1000 HD (3328 blocks)
|
|
`, nil
|
|
}
|
|
|
|
parts, err := listPartitions("/fake/disk.iso")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(parts) != 3 {
|
|
t.Fatalf("expected 3 partitions, got %d: %v", len(parts), parts)
|
|
}
|
|
if parts[0] != "A" || parts[1] != "B" || parts[2] != "C" {
|
|
t.Fatalf("unexpected partitions: %v", parts)
|
|
}
|
|
}
|
|
|
|
func TestListPartitionsEmpty(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
return "No partitions found\n", nil
|
|
}
|
|
|
|
parts, err := listPartitions("/fake/disk.iso")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(parts) != 1 || parts[0] != "A" {
|
|
t.Fatalf("expected fallback to [A], got %v", parts)
|
|
}
|
|
}
|
|
|
|
func TestListVolumes(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
return `
|
|
/disk0/A > cd /disk0/A
|
|
/disk0/A > dir
|
|
1 DRUM KIT 1 - VOLUME (10 files)
|
|
2 BASS SAMPLES - VOLUME (5 files)
|
|
/disk0/A > cd /disk0/B
|
|
/disk0/B > dir
|
|
3 SYNTH PADS - VOLUME (20 files)
|
|
/disk0/B >
|
|
`, nil
|
|
}
|
|
|
|
vols, err := listVolumes("/fake/disk.iso", []string{"A", "B"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(vols) != 3 {
|
|
t.Fatalf("expected 3 volumes, got %d: %v", len(vols), vols)
|
|
}
|
|
if vols[0].Part != "A" || vols[0].Name != "DRUM KIT 1" {
|
|
t.Fatalf("unexpected vol 0: %+v", vols[0])
|
|
}
|
|
if vols[1].Part != "A" || vols[1].Name != "BASS SAMPLES" {
|
|
t.Fatalf("unexpected vol 1: %+v", vols[1])
|
|
}
|
|
if vols[2].Part != "B" || vols[2].Name != "SYNTH PADS" {
|
|
t.Fatalf("unexpected vol 2: %+v", vols[2])
|
|
}
|
|
}
|
|
|
|
func TestRunExtractionCount(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
calls := 0
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
calls++
|
|
return `
|
|
exported 10 file(s)
|
|
exported 5 file(s)
|
|
exported 0 file(s)
|
|
`, nil
|
|
}
|
|
|
|
vols := []volInfo{
|
|
{Part: "A", Name: "DRUMS"},
|
|
{Part: "A", Name: "BASS"},
|
|
}
|
|
total, err := runExtraction("/fake/disk.iso", "/tmp/out", vols)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 15 {
|
|
t.Fatalf("expected 15 total WAVs, got %d", total)
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("expected 1 akaiutil call, got %d", calls)
|
|
}
|
|
}
|
|
|
|
func TestExtractWAVs_Success(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
callCount := 0
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
callCount++
|
|
if callCount == 1 {
|
|
if commands != "df\nq\n" {
|
|
t.Fatalf("first call: expected df\\q\\n, got %q", commands)
|
|
}
|
|
return `
|
|
Disk /disk0
|
|
A - S1000 HD (3840 blocks)
|
|
`, nil
|
|
}
|
|
if callCount == 2 {
|
|
return `
|
|
/disk0/A > cd /disk0/A
|
|
/disk0/A > dir
|
|
1 DRUMS - VOLUME (10 files)
|
|
/disk0/A > q
|
|
`, nil
|
|
}
|
|
if callCount == 3 {
|
|
return ` exported 10 file(s)
|
|
`, nil
|
|
}
|
|
t.Fatalf("unexpected call #%d with commands: %q", callCount, commands)
|
|
return "", nil
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
total, err := extractWAVs("/fake/test.iso", tmp)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if total != 10 {
|
|
t.Fatalf("expected 10 WAVs, got %d", total)
|
|
}
|
|
if callCount != 3 {
|
|
t.Fatalf("expected 3 akaiutil calls, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestExtractWAVs_MultipleVolumes(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
calls := 0
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
calls++
|
|
if calls == 1 {
|
|
return `
|
|
Disk /disk0
|
|
A - S1000 HD (3840 blocks)
|
|
B - S1000 HD (5120 blocks)
|
|
`, nil
|
|
}
|
|
if calls == 2 {
|
|
return `
|
|
/disk0/A > cd /disk0/A
|
|
1 VOL_A - VOLUME (5 files)
|
|
/disk0/B > cd /disk0/B
|
|
2 VOL_B - VOLUME (8 files)
|
|
`, nil
|
|
}
|
|
return ` exported 5 file(s)
|
|
exported 8 file(s)
|
|
`, nil
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
total, err := extractWAVs("/fake/multivol.iso", tmp)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if total != 13 {
|
|
t.Fatalf("expected 13 WAVs, got %d", total)
|
|
}
|
|
if calls != 3 {
|
|
t.Fatalf("expected 3 calls, got %d", calls)
|
|
}
|
|
}
|
|
|
|
func TestExtractWAVs_NoVolumes(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
calls := 0
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
calls++
|
|
if calls == 1 {
|
|
return `Disk /disk0
|
|
A - S1000 HD (3840 blocks)
|
|
`, nil
|
|
}
|
|
if calls == 2 {
|
|
return `/disk0/A > cd /disk0/A
|
|
/disk0/A > q
|
|
`, nil
|
|
}
|
|
return ` exported 0 file(s)
|
|
`, nil
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
total, err := extractWAVs("/fake/empty.iso", tmp)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if total != 0 {
|
|
t.Fatalf("expected 0 WAVs, got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestExtractWAVs_ErrorInPartitionList(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
return "", fmt.Errorf("simulated partition error")
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
_, err := extractWAVs("/fake/error.iso", tmp)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestExtractWAVs_ErrorInVolumeList(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
calls := 0
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
calls++
|
|
if calls == 1 {
|
|
return `
|
|
Disk /disk0
|
|
A - S1000 HD (3840 blocks)
|
|
`, nil
|
|
}
|
|
return "", fmt.Errorf("simulated volume list error")
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
_, err := extractWAVs("/fake/error.iso", tmp)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestRunExtraction_CreatesVolumeDirs(t *testing.T) {
|
|
orig := runAkaiutil
|
|
defer func() { runAkaiutil = orig }()
|
|
|
|
created := map[string]bool{}
|
|
runAkaiutil = func(isoPath, commands string) (string, error) {
|
|
for _, line := range strings.Split(commands, "\n") {
|
|
if strings.HasPrefix(line, "lcd ") {
|
|
dir := strings.TrimPrefix(line, "lcd ")
|
|
created[dir] = true
|
|
}
|
|
}
|
|
return ` exported 3 file(s)
|
|
`, nil
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
vols := []volInfo{
|
|
{Part: "A", Name: "Test Volume One"},
|
|
}
|
|
baseDir := filepath.Join(tmp, "output")
|
|
_, err := runExtraction("/fake/test.iso", baseDir, vols)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if !created["/"] {
|
|
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)
|
|
}
|
|
}
|