@@ -0,0 +1,411 @@
+package restic
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "git.secluded.site/keld/internal/config"
+)
+
+// fixtureLsJSON is real output from:
+//
+// keld --preset music-final@b2 ls --json --recursive 8bd729b1 /home/amolith/Music/Final/3nd
+//
+// 24 lines: 1 snapshot header + 4 directories + 19 .flac files across 3 album
+// directories. The snapshot header line has message_type "snapshot" and must be
+// skipped; all remaining lines have message_type "node".
+const fixtureLsJSON = `{"time":"2026-03-24T00:20:53.867687549-06:00","parent":"2a1eb755327d446b938ef5a6e991f83f86abf12911f3bc87602fc696a7d1d4af","tree":"011fe38da2e96cb6ea9b2bb8bbc710d5b92ab9f4ea2d91be089bba532d329aae","paths":["/home/amolith/Music/Final"],"hostname":"angmar","username":"amolith","uid":1000,"gid":1000,"program_version":"restic 0.18.1","summary":{"backup_start":"2026-03-24T00:20:53.867687549-06:00","backup_end":"2026-03-24T00:20:58.420948969-06:00","files_new":0,"files_changed":4,"files_unmodified":6010,"dirs_new":0,"dirs_changed":4,"dirs_unmodified":862,"data_blobs":1,"tree_blobs":5,"data_added":89361,"data_added_packed":13072,"total_files_processed":6014,"total_bytes_processed":121289853644},"id":"8bd729b16043cef621423ca2f6100832e8add471374a3a54c555d624b011988c","short_id":"8bd729b1","message_type":"snapshot","struct_type":"snapshot"}
+{"name":"3nd","type":"dir","path":"/home/amolith/Music/Final/3nd","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2021-04-17T18:01:57.376612-06:00","atime":"2021-04-17T18:01:57.376612-06:00","ctime":"2024-09-26T19:41:45.573886986-06:00","inode":42207275,"message_type":"node","struct_type":"node"}
+{"name":"World Tour","type":"dir","path":"/home/amolith/Music/Final/3nd/World Tour","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.86776-06:00","atime":"2024-06-10T09:00:35.86776-06:00","ctime":"2024-09-26T19:41:45.563886805-06:00","inode":42207421,"message_type":"node","struct_type":"node"}
+{"name":"01 - monsoon.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac","uid":1000,"gid":1000,"size":30037820,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:20.840315-07:00","atime":"2021-11-25T17:31:20.840315-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213020,"message_type":"node","struct_type":"node"}
+{"name":"02 - china.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/02 - china.flac","uid":1000,"gid":1000,"size":17705986,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:22.00033-07:00","atime":"2021-11-25T17:31:22.00033-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213321,"message_type":"node","struct_type":"node"}
+{"name":"03 - SSK.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/03 - SSK.flac","uid":1000,"gid":1000,"size":32263802,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:22.753673-07:00","atime":"2021-11-25T17:31:22.753673-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213685,"message_type":"node","struct_type":"node"}
+{"name":"04 - midroll.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/04 - midroll.flac","uid":1000,"gid":1000,"size":37431281,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:23.643684-07:00","atime":"2021-11-25T17:31:23.643684-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213686,"message_type":"node","struct_type":"node"}
+{"name":"05 - LOTUS.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/05 - LOTUS.flac","uid":1000,"gid":1000,"size":33509064,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.157024-07:00","atime":"2021-11-25T17:31:24.157024-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213687,"message_type":"node","struct_type":"node"}
+{"name":"06 - 眠る.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/06 - 眠る.flac","uid":1000,"gid":1000,"size":21319060,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.383693-07:00","atime":"2021-11-25T17:31:24.383693-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213249,"message_type":"node","struct_type":"node"}
+{"name":"07 - filter in dust.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/07 - filter in dust.flac","uid":1000,"gid":1000,"size":21849635,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.613696-07:00","atime":"2021-11-25T17:31:24.613696-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213250,"message_type":"node","struct_type":"node"}
+{"name":"08 - 夏終る.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/08 - 夏終る.flac","uid":1000,"gid":1000,"size":28061640,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.817032-07:00","atime":"2021-11-25T17:31:24.817032-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212532,"message_type":"node","struct_type":"node"}
+{"name":"view from here","type":"dir","path":"/home/amolith/Music/Final/3nd/view from here","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.874427-06:00","atime":"2024-06-10T09:00:35.874427-06:00","ctime":"2024-09-26T19:41:45.570553593-06:00","inode":42207422,"message_type":"node","struct_type":"node"}
+{"name":"01 - clockworker.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/01 - clockworker.flac","uid":1000,"gid":1000,"size":38342556,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:12.526878-07:00","atime":"2021-11-25T17:31:12.526878-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42214320,"message_type":"node","struct_type":"node"}
+{"name":"02 - augustline.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/02 - augustline.flac","uid":1000,"gid":1000,"size":27357365,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:13.413555-07:00","atime":"2021-11-25T17:31:13.413555-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212371,"message_type":"node","struct_type":"node"}
+{"name":"03 - season.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/03 - season.flac","uid":1000,"gid":1000,"size":26918886,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:14.256899-07:00","atime":"2021-11-25T17:31:14.256899-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212245,"message_type":"node","struct_type":"node"}
+{"name":"04 - ハルオト.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/04 - ハルオト.flac","uid":1000,"gid":1000,"size":45722382,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:15.253578-07:00","atime":"2021-11-25T17:31:15.253578-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212533,"message_type":"node","struct_type":"node"}
+{"name":"05 - walk in the brown.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/05 - walk in the brown.flac","uid":1000,"gid":1000,"size":38267170,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:16.300258-07:00","atime":"2021-11-25T17:31:16.300258-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42214321,"message_type":"node","struct_type":"node"}
+{"name":"we dance × avec toi","type":"dir","path":"/home/amolith/Music/Final/3nd/we dance × avec toi","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.87776-06:00","atime":"2024-06-10T09:00:35.87776-06:00","ctime":"2024-09-26T19:41:45.573886986-06:00","inode":42207423,"message_type":"node","struct_type":"node"}
+{"name":"01 - Bender.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/01 - Bender.flac","uid":1000,"gid":1000,"size":24044464,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:17.563608-07:00","atime":"2021-11-25T17:31:17.563608-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211884,"message_type":"node","struct_type":"node"}
+{"name":"02 - テレプシコラー.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/02 - テレプシコラー.flac","uid":1000,"gid":1000,"size":29003735,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:18.106948-07:00","atime":"2021-11-25T17:31:18.106948-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211885,"message_type":"node","struct_type":"node"}
+{"name":"03 - コロコロ転がる.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/03 - コロコロ転がる.flac","uid":1000,"gid":1000,"size":27127530,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:18.643621-07:00","atime":"2021-11-25T17:31:18.643621-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212285,"message_type":"node","struct_type":"node"}
+{"name":"04 - algorhythm.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/04 - algorhythm.flac","uid":1000,"gid":1000,"size":36728278,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.070293-07:00","atime":"2021-11-25T17:31:19.070293-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212286,"message_type":"node","struct_type":"node"}
+{"name":"05 - Rain Song.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/05 - Rain Song.flac","uid":1000,"gid":1000,"size":27059422,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.540299-07:00","atime":"2021-11-25T17:31:19.540299-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212287,"message_type":"node","struct_type":"node"}
+{"name":"06 - untroubled terror.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/06 - untroubled terror.flac","uid":1000,"gid":1000,"size":6965876,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.853636-07:00","atime":"2021-11-25T17:31:19.853636-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211886,"message_type":"node","struct_type":"node"}
+`
+
+func TestParseLsNodes(t *testing.T) {
+ t.Parallel()
+
+ nodes, err := ParseLsNodes([]byte(fixtureLsJSON))
+ if err != nil {
+ t.Fatalf("ParseLsNodes(): unexpected error: %v", err)
+ }
+
+ // 23 node lines (4 dirs + 19 files), snapshot header skipped.
+ if got := len(nodes); got != 23 {
+ t.Fatalf("len(nodes): got %d, want 23", got)
+ }
+
+ // First node should be the root dir of the listing.
+ first := nodes[0]
+ if first.Name != "3nd" {
+ t.Errorf("first node name: got %q, want %q", first.Name, "3nd")
+ }
+ if first.Type != "dir" {
+ t.Errorf("first node type: got %q, want %q", first.Type, "dir")
+ }
+ if first.Path != "/home/amolith/Music/Final/3nd" {
+ t.Errorf("first node path: got %q, want %q", first.Path, "/home/amolith/Music/Final/3nd")
+ }
+ if first.Size != 0 {
+ t.Errorf("first node size: got %d, want 0 (dirs have no size)", first.Size)
+ }
+ if first.Mode != 2147484157 {
+ t.Errorf("first node mode: got %d, want %d", first.Mode, 2147484157)
+ }
+ if first.Permissions != "drwxrwxr-x" {
+ t.Errorf("first node permissions: got %q, want %q", first.Permissions, "drwxrwxr-x")
+ }
+ wantMtime, _ := time.Parse(time.RFC3339Nano, "2021-04-17T18:01:57.376612-06:00")
+ if !first.Mtime.Equal(wantMtime) {
+ t.Errorf("first node mtime: got %v, want %v", first.Mtime, wantMtime)
+ }
+
+ // Spot-check a file node: first .flac in "World Tour".
+ monsoon := nodes[2]
+ if monsoon.Name != "01 - monsoon.flac" {
+ t.Errorf("monsoon name: got %q, want %q", monsoon.Name, "01 - monsoon.flac")
+ }
+ if monsoon.Type != "file" {
+ t.Errorf("monsoon type: got %q, want %q", monsoon.Type, "file")
+ }
+ if monsoon.Path != "/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac" {
+ t.Errorf("monsoon path: got %q, want %q",
+ monsoon.Path, "/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac")
+ }
+ if monsoon.Size != 30037820 {
+ t.Errorf("monsoon size: got %d, want %d", monsoon.Size, 30037820)
+ }
+ if monsoon.Mode != 509 {
+ t.Errorf("monsoon mode: got %d, want %d", monsoon.Mode, 509)
+ }
+ if monsoon.Permissions != "-rwxrwxr-x" {
+ t.Errorf("monsoon permissions: got %q, want %q", monsoon.Permissions, "-rwxrwxr-x")
+ }
+
+ // Spot-check a file with Unicode in the name.
+ nemuru := nodes[7]
+ if nemuru.Name != "06 - 眠る.flac" {
+ t.Errorf("nemuru name: got %q, want %q", nemuru.Name, "06 - 眠る.flac")
+ }
+ if nemuru.Size != 21319060 {
+ t.Errorf("nemuru size: got %d, want %d", nemuru.Size, 21319060)
+ }
+
+ // Count dirs vs files.
+ var dirs, files int
+ for _, n := range nodes {
+ switch n.Type {
+ case "dir":
+ dirs++
+ case "file":
+ files++
+ default:
+ t.Errorf("unexpected node type %q for %q", n.Type, n.Path)
+ }
+ }
+ if dirs != 4 {
+ t.Errorf("dir count: got %d, want 4", dirs)
+ }
+ if files != 19 {
+ t.Errorf("file count: got %d, want 19", files)
+ }
+}
+
+func TestParseLsNodesEmpty(t *testing.T) {
+ t.Parallel()
+
+ nodes, err := ParseLsNodes([]byte(""))
+ if err != nil {
+ t.Fatalf("ParseLsNodes(empty): unexpected error: %v", err)
+ }
+ if len(nodes) != 0 {
+ t.Errorf("expected 0 nodes from empty input, got %d", len(nodes))
+ }
+}
+
+func TestParseLsNodesSnapshotOnly(t *testing.T) {
+ t.Parallel()
+
+ // A snapshot header with no node lines — e.g. an empty snapshot or a
+ // path filter that matched nothing.
+ input := `{"time":"2026-03-24T00:20:53.867687549-06:00","paths":["/home/amolith/Music/Final"],"hostname":"angmar","id":"8bd729b1","short_id":"8bd729b1","message_type":"snapshot","struct_type":"snapshot"}
+`
+
+ nodes, err := ParseLsNodes([]byte(input))
+ if err != nil {
+ t.Fatalf("ParseLsNodes(snapshot-only): unexpected error: %v", err)
+ }
+ if len(nodes) != 0 {
+ t.Errorf("expected 0 nodes from snapshot-only input, got %d", len(nodes))
+ }
+}
+
+func TestBuildLsCmd(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ cfg *config.ResolvedConfig
+ snapshot string
+ wantArgv []string
+ wantErr bool
+ }{
+ {
+ name: "basic repo flag",
+ cfg: &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--repo", Value: "/srv/backup"},
+ {Name: "--target", Value: "/tmp/restore"},
+ },
+ },
+ snapshot: "8bd729b1",
+ wantArgv: []string{"restic", "ls", "--json", "--repo", "/srv/backup", "8bd729b1"},
+ },
+ {
+ name: "repo with password-file and non-global flags stripped",
+ cfg: &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--repo", Value: "rclone:remote:/backup"},
+ {Name: "--password-file", Value: "/etc/restic/pw"},
+ {Name: "--target", Value: "/tmp/restore"},
+ {Name: "--overwrite", Value: "if-changed"},
+ },
+ },
+ snapshot: "latest",
+ wantArgv: []string{
+ "restic", "ls", "--json",
+ "--repo", "rclone:remote:/backup",
+ "--password-file", "/etc/restic/pw",
+ "latest",
+ },
+ },
+ {
+ name: "cache-dir and no-lock forwarded",
+ cfg: &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--repo", Value: "/srv/backup"},
+ {Name: "--cache-dir", Value: "/tmp/cache"},
+ {Name: "--no-lock"},
+ {Name: "--target", Value: "/tmp/restore"},
+ },
+ },
+ snapshot: "abc123",
+ wantArgv: []string{
+ "restic", "ls", "--json",
+ "--repo", "/srv/backup",
+ "--cache-dir", "/tmp/cache",
+ "--no-lock",
+ "abc123",
+ },
+ },
+ {
+ name: "snapshot selectors forwarded for latest",
+ cfg: &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--repo", Value: "/srv/backup"},
+ {Name: "--host", Value: "angmar"},
+ {Name: "--path", Value: "/home/amolith/Music"},
+ {Name: "--tag", Value: "daily"},
+ {Name: "--target", Value: "/tmp/restore"},
+ },
+ },
+ snapshot: "latest",
+ wantArgv: []string{
+ "restic", "ls", "--json",
+ "--repo", "/srv/backup",
+ "--host", "angmar",
+ "--path", "/home/amolith/Music",
+ "--tag", "daily",
+ "latest",
+ },
+ },
+ {
+ name: "short host flag forwarded",
+ cfg: &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--repo", Value: "/srv/backup"},
+ {Name: "-H", Value: "angmar"},
+ },
+ },
+ snapshot: "latest",
+ wantArgv: []string{
+ "restic", "ls", "--json",
+ "--repo", "/srv/backup",
+ "-H", "angmar",
+ "latest",
+ },
+ },
+ {
+ name: "repo via environ only",
+ cfg: &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--target", Value: "/tmp/restore"},
+ },
+ Environ: map[string]string{
+ "RESTIC_REPOSITORY": "/srv/backup",
+ },
+ },
+ snapshot: "8bd729b1",
+ wantArgv: []string{"restic", "ls", "--json", "8bd729b1"},
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ argv, err := buildLsCmd(tt.cfg, tt.snapshot)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("buildLsCmd(): err=%v, wantErr=%v", err, tt.wantErr)
+ }
+ if err != nil {
+ return
+ }
+
+ if len(argv) != len(tt.wantArgv) {
+ t.Fatalf("argv length: got %d %v, want %d %v",
+ len(argv), argv, len(tt.wantArgv), tt.wantArgv)
+ }
+ for i := range argv {
+ if argv[i] != tt.wantArgv[i] {
+ t.Errorf("argv[%d]: got %q, want %q", i, argv[i], tt.wantArgv[i])
+ }
+ }
+ })
+ }
+}
+
+func TestBuildLsCmdNoRepo(t *testing.T) {
+ t.Setenv("RESTIC_REPOSITORY", "")
+ t.Setenv("RESTIC_REPOSITORY_FILE", "")
+
+ cfg := &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--target", Value: "/tmp/restore"},
+ },
+ }
+
+ _, err := buildLsCmd(cfg, "8bd729b1")
+ if !errors.Is(err, ErrNoRepo) {
+ t.Errorf("expected ErrNoRepo, got %v", err)
+ }
+}
+
+// testdataRepoPath returns the absolute path to the committed test
+// restic repository. Tests using this fixture require restic to be
+// installed.
+func testdataRepoPath(t *testing.T) string {
+ t.Helper()
+ // Discover the repo relative to this test file's package dir.
+ path, err := filepath.Abs("testdata/repo")
+ if err != nil {
+ t.Fatalf("resolving testdata/repo: %v", err)
+ }
+ if _, err := os.Stat(path); err != nil {
+ t.Fatalf("testdata/repo not found at %s: %v", path, err)
+ }
+ return path
+}
+
+func TestRunLs(t *testing.T) {
+ t.Parallel()
+
+ repoPath := testdataRepoPath(t)
+ cfg := &config.ResolvedConfig{
+ Command: "restore",
+ Flags: []config.Flag{
+ {Name: "--repo", Value: repoPath},
+ },
+ Environ: map[string]string{
+ "RESTIC_PASSWORD": "test",
+ },
+ }
+
+ nodes, err := RunLs(cfg, "03b061a6")
+ if err != nil {
+ t.Fatalf("RunLs(): unexpected error: %v", err)
+ }
+
+ // The test repo has 29 files + 26 dirs = 55 nodes.
+ if got := len(nodes); got != 55 {
+ t.Fatalf("len(nodes): got %d, want 55", got)
+ }
+
+ // Count types.
+ var dirs, files int
+ for _, n := range nodes {
+ switch n.Type {
+ case "dir":
+ dirs++
+ case "file":
+ files++
+ default:
+ t.Errorf("unexpected node type %q for %q", n.Type, n.Path)
+ }
+ }
+ if dirs != 26 {
+ t.Errorf("dir count: got %d, want 26", dirs)
+ }
+ if files != 29 {
+ t.Errorf("file count: got %d, want 29", files)
+ }
+
+ // Spot-check a Unicode path.
+ var found bool
+ for _, n := range nodes {
+ if n.Name == "01 - 夜明け.flac" {
+ found = true
+ if n.Type != "file" {
+ t.Errorf("夜明け type: got %q, want %q", n.Type, "file")
+ }
+ break
+ }
+ }
+ if !found {
+ t.Error("expected to find node '01 - 夜明け.flac'")
+ }
+
+ // Spot-check a space-in-path entry.
+ found = false
+ for _, n := range nodes {
+ if n.Name == "Jazz Café" {
+ found = true
+ if n.Type != "dir" {
+ t.Errorf("Jazz Café type: got %q, want %q", n.Type, "dir")
+ }
+ break
+ }
+ }
+ if !found {
+ t.Error("expected to find node 'Jazz Café'")
+ }
+}