1package oshost_test23import (4 "bytes"5 "context"6 "errors"7 "io"8 "os"9 "path/filepath"10 "testing"1112 "github.com/FelineStateMachine/atlas/format/bundle"13 "github.com/FelineStateMachine/atlas/internal/app/hostenv"14 "github.com/FelineStateMachine/atlas/internal/app/hostenv/oshost"15)1617// build writes a real bundle through the real writer, so the store under test18// meets an archive the format would accept and nothing opaque is checked in.19type build struct {20 slug string21 title string22 createdAt string23 revision int24 world string25}2627func (b build) write(t *testing.T, dir string) string {28 t.Helper()29 if b.world == "" {30 b.world = "overworld"31 }32 manifest := bundle.Manifest{33 Format: bundle.Format,34 FormatVersion: bundle.FormatVersion,35 Volume: bundle.Volume{Slug: b.slug, Title: b.title},36 Version: bundle.Version{37 Stamp: bundle.HashBytes([]byte(b.slug + b.createdAt + b.world)),38 CreatedAt: b.createdAt,39 Revision: b.revision,40 },41 TileGrid: bundle.TileGrid{SourceZoom: 13, FirstTile: 4064, TileSize: 256, Size: 8192},42 Worlds: []bundle.WorldEntry{{43 Slug: b.world, Title: b.title + " ground", Points: 1, UpdatedAt: b.createdAt,44 }},45 }46 path := filepath.Join(dir, bundle.VersionedFileName(manifest))47 file, err := os.Create(path)48 if err != nil {49 t.Fatal(err)50 }51 defer file.Close()52 writer, err := bundle.NewWriter(file, manifest)53 if err != nil {54 t.Fatal(err)55 }56 detail := `{"lenses":[{"tiles":"` + b.world + `","minZoom":0,"maxZoom":0,"formats":["jpg"]}],` +57 `"collections":[{"id":1,"title":"Marker","kind":"point","visible":true}]}`58 if err := writer.AddDeflated(bundle.WorldEntryName(b.world, bundle.WorldSuffix), []byte(detail)); err != nil {59 t.Fatal(err)60 }61 packed := bundle.PackLocations([]bundle.Location{{ID: 1, Title: "Origin"}})62 if err := writer.AddStored(bundle.WorldEntryName(b.world, bundle.PackedSuffix), bytes.NewReader(packed)); err != nil {63 t.Fatal(err)64 }65 if err := writer.AddDeflated(bundle.WorldEntryName(b.world, bundle.TextSuffix), []byte(`{}`)); err != nil {66 t.Fatal(err)67 }68 if err := writer.AddStored(bundle.TilesPrefix+b.world+"/0/0/0.jpg", bytes.NewReader([]byte("raster"))); err != nil {69 t.Fatal(err)70 }71 if err := writer.Close(); err != nil {72 t.Fatal(err)73 }74 return path75}7677// The fold is format/bundle's; what this checks is that the walk hands it78// every build and serves the one it picks.79func TestVolumesServesTheFoldsWinner(t *testing.T) {80 dir := t.TempDir()81 build{slug: "tunic", title: "TUNIC", createdAt: "2026-01-01T00:00:00Z"}.write(t, dir)82 newer := build{slug: "tunic", title: "TUNIC", createdAt: "2026-02-01T00:00:00Z"}83 newer.write(t, dir)84 build{slug: "mars", title: "Mars", createdAt: "2026-01-15T00:00:00Z", world: "global"}.write(t, dir)8586 store, err := oshost.NewVolumes(dir)87 if err != nil {88 t.Fatal(err)89 }90 t.Cleanup(func() { store.Close() })9192 volumes := store.Volumes()93 if len(volumes) != 2 {94 t.Fatalf("%d volumes serving, want 2", len(volumes))95 }96 if got := volumes[0].Manifest().Volume.Slug; got != "mars" {97 t.Errorf("volumes are listed %q first, want them sorted by slug", got)98 }99 tunic := volumes[1].Manifest()100 if tunic.Version.CreatedAt != "2026-02-01T00:00:00Z" {101 t.Errorf("serving the build of %s, want the newest capture", tunic.Version.CreatedAt)102 }103104 entry, size, err := volumes[1].Open(bundle.WorldEntryName("overworld", bundle.WorldSuffix))105 if err != nil {106 t.Fatal(err)107 }108 defer entry.Close()109 payload, err := io.ReadAll(entry)110 if err != nil {111 t.Fatal(err)112 }113 if int64(len(payload)) != size {114 t.Errorf("entry announced %d bytes and read %d", size, len(payload))115 }116 if _, _, err := volumes[1].Open("worlds/not-a-world.json"); err == nil {117 t.Error("an entry the bundle does not hold opened")118 }119}120121func TestVolumesRescanReportsWhatMoved(t *testing.T) {122 dir := t.TempDir()123 build{slug: "tunic", title: "TUNIC", createdAt: "2026-01-01T00:00:00Z"}.write(t, dir)124 store, err := oshost.NewVolumes(dir)125 if err != nil {126 t.Fatal(err)127 }128 t.Cleanup(func() { store.Close() })129130 if changed, err := store.Rescan(); err != nil || len(changed) != 0 {131 t.Fatalf("a rescan over an unchanged library reported %v, %v", changed, err)132 }133134 build{slug: "mars", title: "Mars", createdAt: "2026-01-15T00:00:00Z", world: "global"}.write(t, dir)135 changed, err := store.Rescan()136 if err != nil {137 t.Fatal(err)138 }139 if len(changed) != 1 || changed[0] != "mars" {140 t.Fatalf("rescan reported %v, want just the arrival", changed)141 }142}143144func TestVolumesInstall(t *testing.T) {145 library := t.TempDir()146 elsewhere := t.TempDir()147 source := build{slug: "tunic", title: "TUNIC", createdAt: "2026-01-01T00:00:00Z"}.write(t, elsewhere)148149 store, err := oshost.NewVolumes(library)150 if err != nil {151 t.Fatal(err)152 }153 t.Cleanup(func() { store.Close() })154 if len(store.Volumes()) != 0 {155 t.Fatal("an empty library is not empty")156 }157158 content, err := os.Open(source)159 if err != nil {160 t.Fatal(err)161 }162 installed, err := store.Install(filepath.Base(source), content)163 content.Close()164 if err != nil {165 t.Fatal(err)166 }167 if installed.Slug != "tunic" || installed.Already {168 t.Errorf("install reported %+v, want a first arrival of tunic", installed)169 }170 if len(installed.Changed) != 1 || installed.Changed[0] != "tunic" {171 t.Errorf("install reported %v changed, want tunic", installed.Changed)172 }173 if len(store.Volumes()) != 1 {174 t.Error("the import did not rescan")175 }176177 // The same build again: a successful import that copies nothing and178 // changes nothing, because the file name carries the stamp.179 content, err = os.Open(source)180 if err != nil {181 t.Fatal(err)182 }183 again, err := store.Install(filepath.Base(source), content)184 content.Close()185 if err != nil {186 t.Fatal(err)187 }188 if !again.Already || len(again.Changed) != 0 {189 t.Errorf("reinstalling the same build reported %+v, want a no-op", again)190 }191192 if _, err := store.Install("junk.atlas", bytes.NewReader([]byte("not a zip"))); err == nil {193 t.Error("a file that is not a bundle was let into the library")194 }195 if entries, _ := os.ReadDir(library); len(entries) != 1 {196 t.Errorf("the library holds %d files after a refused import, want 1", len(entries))197 }198}199200func TestSessionsRoundTrip(t *testing.T) {201 dir := filepath.Join(t.TempDir(), "sessions")202 store, err := oshost.NewSessions(dir)203 if err != nil {204 t.Fatal(err)205 }206207 if _, err := store.Load("volume.tunic.json"); !errors.Is(err, hostenv.ErrNoSession) {208 t.Fatalf("loading a record nobody wrote = %v, want ErrNoSession", err)209 }210 if err := store.Save("volume.tunic.json", []byte(`{"world":"world"}`)); err != nil {211 t.Fatal(err)212 }213 if err := store.Save("volume.tunic.json", []byte(`{"world":"other"}`)); err != nil {214 t.Fatal(err)215 }216 held, err := store.Load("volume.tunic.json")217 if err != nil || string(held) != `{"world":"other"}` {218 t.Fatalf("Load = %q, %v, want the second write whole", held, err)219 }220221 names, err := store.Names()222 if err != nil {223 t.Fatal(err)224 }225 if len(names) != 1 || names[0] != "volume.tunic.json" {226 t.Fatalf("Names = %v", names)227 }228 if err := store.Save("../escape.json", nil); err == nil {229 t.Error("a name that climbs out of the directory was accepted")230 }231}232233// Deleting a record, which is the whole of the blunt reset: the arrangement a234// volume opens with is synthesized from the world itself, so a record that is235// gone is a volume that comes back fresh.236//237// The half of the contract easiest to get wrong is the second one. A record238// that is not there is already what the caller asked for, so a missing file is239// success -- otherwise a reader who pressed reset twice would meet an error240// about a file they were right to want gone.241func TestSessionsDelete(t *testing.T) {242 dir := filepath.Join(t.TempDir(), "sessions")243 store, err := oshost.NewSessions(dir)244 if err != nil {245 t.Fatal(err)246 }247248 if err := store.Delete("volume.tunic.json"); err != nil {249 t.Errorf("deleting a record nobody wrote = %v, want a quiet success", err)250 }251 if err := store.Save("volume.tunic.json", []byte(`{"world":"world"}`)); err != nil {252 t.Fatal(err)253 }254 if err := store.Save("app.json", []byte(`{"volume":"tunic"}`)); err != nil {255 t.Fatal(err)256 }257 if err := store.Delete("volume.tunic.json"); err != nil {258 t.Fatal(err)259 }260 if _, err := store.Load("volume.tunic.json"); !errors.Is(err, hostenv.ErrNoSession) {261 t.Errorf("the record was still readable after a delete: %v", err)262 }263 if _, err := os.Stat(filepath.Join(dir, "volume.tunic.json")); !errors.Is(err, os.ErrNotExist) {264 t.Errorf("the record's file is still on disk: %v", err)265 }266 // One record went, not the directory: which volume the reader was last in267 // is a different fact, and it is nobody's to take away here.268 names, err := store.Names()269 if err != nil {270 t.Fatal(err)271 }272 if len(names) != 1 || names[0] != "app.json" {273 t.Errorf("Names = %v, want the pointer left standing", names)274 }275 if err := store.Delete("volume.tunic.json"); err != nil {276 t.Errorf("deleting a record twice = %v, want a quiet success", err)277 }278 if err := store.Delete("../escape.json"); err == nil {279 t.Error("a name that climbs out of the directory was accepted")280 }281}282283func TestHeadlessHostCannotPickAFile(t *testing.T) {284 host, err := oshost.New(oshost.Options{BundlesDir: t.TempDir()})285 if err != nil {286 t.Fatal(err)287 }288 if _, _, err := host.PickFile(context.Background()); !errors.Is(err, hostenv.ErrNotAvailable) {289 t.Fatalf("PickFile on a host with no window = %v, want ErrNotAvailable", err)290 }291 if _, err := host.Sessions().Names(); err != nil {292 t.Fatalf("a host with no session directory has no sessions, not an error: %v", err)293 }294}