1package persist
2
3import (
4 "os"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/pkg/kv"
11)
12
13const legacyRecordWireVersion = 5
14
15func TestOpenOrCreateMigrationPreservesBackupAndIsIdempotent(t *testing.T) {
16 t.Parallel()
17 record := domain.CatalogueRecord{
18 Name: "work", IncarnationID: domain.IncarnationID{1}, Cwd: "/workspace",
19 CreatedAt: 11, UpdatedAt: 22, LastUsedSeq: 33,
20 TabNames: []string{"shell"}, TabRecords: []domain.CatalogueTabRecord{{StableID: "tab-1", Name: "shell"}},
21 Committed: &domain.CheckpointRef{Generation: 3, ManifestDigest: [32]byte{3}},
22 }
23 legacy, err := encodeRecordValueForFormat(record, protocolRecordVersion, legacyRecordWireVersion)
24 require.NoError(t, err)
25 dir := privateDir(t)
26 store, err := kv.Open(StorePath(dir))
27 require.NoError(t, err)
28 require.NoError(t, store.Set([]byte(record.Name), legacy))
29 require.NoError(t, store.Close())
30 original, err := os.ReadFile(StorePath(dir))
31 require.NoError(t, err)
32 require.NoError(t, os.WriteFile(StorePath(dir)+legacyCatalogueBackupSuffix+".tmp", []byte("stale"), 0o600))
33
34 opened, err := OpenOrCreate(dir)
35 require.NoError(t, err)
36 require.Equal(t, []domain.CatalogueRecord{record}, opened.Records)
37 require.True(t, opened.Migration.Performed)
38 require.Equal(t, []uint16{protocolRecordVersion}, opened.Migration.SourceFormats)
39 require.Equal(t, catalogueRecordVersion, opened.Migration.TargetFormat)
40 require.Equal(t, 1, opened.Migration.RecordCount)
41 require.NoError(t, opened.Catalogue.Close())
42
43 backup, err := os.ReadFile(StorePath(dir) + legacyCatalogueBackupSuffix)
44 require.NoError(t, err)
45 require.Equal(t, original, backup)
46 info, err := os.Stat(StorePath(dir) + legacyCatalogueBackupSuffix)
47 require.NoError(t, err)
48 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
49 migrated, err := os.ReadFile(StorePath(dir))
50 require.NoError(t, err)
51
52 reopened, err := OpenOrCreate(dir)
53 require.NoError(t, err)
54 require.False(t, reopened.Migration.Performed)
55 require.Equal(t, []domain.CatalogueRecord{record}, reopened.Records)
56 require.NoError(t, reopened.Catalogue.Close())
57 after, err := os.ReadFile(StorePath(dir))
58 require.NoError(t, err)
59 require.Equal(t, migrated, after)
60}
61
62func TestOpenOrCreateMigratesV3Records(t *testing.T) {
63 t.Parallel()
64 legacyRecord := domain.CatalogueRecord{
65 Name: "work", IncarnationID: domain.IncarnationID{1}, Cwd: "/workspace",
66 CreatedAt: 11, UpdatedAt: 22, LastUsedSeq: 33, TabNames: []string{"shell"},
67 Committed: &domain.CheckpointRef{Generation: 3, ManifestDigest: [32]byte{3}},
68 }
69 legacy, err := encodeRecordValueForFormat(legacyRecord, legacyCatalogueRecordVersion, 0)
70 require.NoError(t, err)
71 dir := privateDir(t)
72 store, err := kv.Open(StorePath(dir))
73 require.NoError(t, err)
74 require.NoError(t, store.Set([]byte(legacyRecord.Name), legacy))
75 require.NoError(t, store.Close())
76 original, err := os.ReadFile(StorePath(dir))
77 require.NoError(t, err)
78
79 opened, err := OpenOrCreate(dir)
80 require.NoError(t, err)
81 want := legacyRecord
82 want.TabRecords = []domain.CatalogueTabRecord{{Name: "shell"}}
83 require.Equal(t, []domain.CatalogueRecord{want}, opened.Records)
84 require.Equal(t, []uint16{legacyCatalogueRecordVersion}, opened.Migration.SourceFormats)
85 require.NoError(t, opened.Catalogue.Close())
86 backup, err := os.ReadFile(StorePath(dir) + legacyCatalogueBackupSuffix)
87 require.NoError(t, err)
88 require.Equal(t, original, backup)
89}
90
91func TestOpenOrCreateMigrationRejectsConflictingBackupWithoutChangingCatalogue(t *testing.T) {
92 t.Parallel()
93 record := domain.CatalogueRecord{Name: "work", IncarnationID: domain.IncarnationID{1}}
94 legacy, err := encodeRecordValueForFormat(record, protocolRecordVersion, legacyRecordWireVersion)
95 require.NoError(t, err)
96 dir := privateDir(t)
97 store, err := kv.Open(StorePath(dir))
98 require.NoError(t, err)
99 require.NoError(t, store.Set([]byte(record.Name), legacy))
100 require.NoError(t, store.Close())
101 original, err := os.ReadFile(StorePath(dir))
102 require.NoError(t, err)
103 require.NoError(t, os.WriteFile(StorePath(dir)+legacyCatalogueBackupSuffix, []byte("different"), 0o600))
104
105 _, err = OpenOrCreate(dir)
106 require.Error(t, err)
107 after, readErr := os.ReadFile(StorePath(dir))
108 require.NoError(t, readErr)
109 require.Equal(t, original, after)
110}
111
112func TestOpenOrCreateMigrationRejectsSymlinkBackupWithoutChangingCatalogue(t *testing.T) {
113 t.Parallel()
114 record := domain.CatalogueRecord{Name: "work", IncarnationID: domain.IncarnationID{1}}
115 legacy, err := encodeRecordValueForFormat(record, protocolRecordVersion, legacyRecordWireVersion)
116 require.NoError(t, err)
117 dir := privateDir(t)
118 store, err := kv.Open(StorePath(dir))
119 require.NoError(t, err)
120 require.NoError(t, store.Set([]byte(record.Name), legacy))
121 require.NoError(t, store.Close())
122 original, err := os.ReadFile(StorePath(dir))
123 require.NoError(t, err)
124 require.NoError(t, os.Symlink(StorePath(dir), StorePath(dir)+legacyCatalogueBackupSuffix))
125
126 _, err = OpenOrCreate(dir)
127 require.Error(t, err)
128 after, readErr := os.ReadFile(StorePath(dir))
129 require.NoError(t, readErr)
130 require.Equal(t, original, after)
131}