vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 1h ago
4b8c9e1d
| 1 | package index |
| 2 | |
| 3 | // An incremental refresh is only correct if it leaves the rows it did not touch |
| 4 | // alone. These tests hold that: a new issue must not delete the repository's |
| 5 | // rows, and a comment or label edit must not consume a number. |
| 6 | |
| 7 | import ( |
| 8 | "fmt" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "rodent.software/vole/core/git" |
| 16 | "rodent.software/vole/forge/repo" |
| 17 | "rodent.software/vole/metadata" |
| 18 | ) |
| 19 | |
| 20 | func incrementalRepo(t *testing.T) (*Store, *repo.Meta, *git.Repo) { |
| 21 | t.Helper() |
| 22 | dir := filepath.Join(t.TempDir(), "r.git") |
| 23 | if err := git.InitBare(dir, "main"); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | g := git.Open(dir) |
| 27 | tree, err := g.MkTree(nil) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | seed, err := g.CommitTree(tree, "", "seed") |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | if err := g.UpdateRef(metadata.Ref, seed); err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | |
| 39 | s, err := Open(filepath.Join(t.TempDir(), "index.db")) |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | t.Cleanup(func() { s.Close() }) |
| 44 | if _, err := s.db.Exec(`INSERT INTO repos(id, name, created) VALUES('r1','demo',0)`); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | return s, &repo.Meta{ID: "r1", Name: "demo", Dir: dir}, g |
| 48 | } |
| 49 | |
| 50 | func createIndexedIssue(t *testing.T, g *git.Repo, title string, at time.Time, labels, assignees []string) *metadata.Issue { |
| 51 | t.Helper() |
| 52 | iss, err := metadata.NewIssue(title, "body of "+title, "keenan", labels, assignees, at) |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | if err := metadata.CreateIssue(g, iss, "keenan"); err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | return iss |
| 60 | } |
| 61 | |
| 62 | func watchedDeletes(t *testing.T, s *Store) { |
| 63 | t.Helper() |
| 64 | if _, err := s.db.Exec(`CREATE TABLE index_deletes(table_name TEXT NOT NULL)`); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | for _, table := range []string{"issues", "issue_labels", "issue_assignees", "entity_numbers"} { |
| 68 | trigger := `CREATE TRIGGER ` + table + `_deleted AFTER DELETE ON ` + table + |
| 69 | ` BEGIN INSERT INTO index_deletes VALUES('` + table + `'); END;` |
| 70 | if _, err := s.db.Exec(trigger); err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | func deleteCount(t *testing.T, s *Store) int { |
| 77 | t.Helper() |
| 78 | var n int |
| 79 | if err := s.db.QueryRow(`SELECT COUNT(*) FROM index_deletes`).Scan(&n); err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | return n |
| 83 | } |
| 84 | |
| 85 | func TestEnsureAddsOneIssueWithoutDeletingTheIndex(t *testing.T) { |
| 86 | t.Parallel() |
| 87 | s, m, g := incrementalRepo(t) |
| 88 | now := time.Date(2026, 10, 30, 12, 0, 0, 0, time.UTC) |
| 89 | first := createIndexedIssue(t, g, "First issue", now, []string{"bug"}, []string{"keenan"}) |
| 90 | second := createIndexedIssue(t, g, "Second issue", now.Add(time.Minute), nil, nil) |
| 91 | if err := s.Ensure(m); err != nil { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | watchedDeletes(t, s) |
| 95 | |
| 96 | third := createIndexedIssue(t, g, "Third issue", now.Add(2*time.Minute), []string{"cli"}, []string{"wilbur"}) |
| 97 | if err := s.Ensure(m); err != nil { |
| 98 | t.Fatal(err) |
| 99 | } |
| 100 | if n := deleteCount(t, s); n != 0 { |
| 101 | t.Fatalf("adding one issue deleted %d index rows, want 0", n) |
| 102 | } |
| 103 | |
| 104 | rows, err := s.Issues(m.ID, Filter{}) |
| 105 | if err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | if len(rows) != 3 { |
| 109 | t.Fatalf("issues = %d, want 3", len(rows)) |
| 110 | } |
| 111 | wantNumbers := map[string]int{first.ID: 1, second.ID: 2, third.ID: 3} |
| 112 | for _, row := range rows { |
| 113 | if row.Number != wantNumbers[row.ID] { |
| 114 | t.Fatalf("issue %s number = %d, want %d", row.ID, row.Number, wantNumbers[row.ID]) |
| 115 | } |
| 116 | } |
| 117 | for _, row := range rows { |
| 118 | if row.ID != third.ID { |
| 119 | continue |
| 120 | } |
| 121 | if len(row.Labels) != 1 || row.Labels[0] != "cli" { |
| 122 | t.Fatalf("labels = %v, want [cli]", row.Labels) |
| 123 | } |
| 124 | if len(row.Assignees) != 1 || row.Assignees[0] != "wilbur" { |
| 125 | t.Fatalf("assignees = %v, want [wilbur]", row.Assignees) |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | func TestEnsureUpdatesOneIssueWithoutAddingANumber(t *testing.T) { |
| 131 | t.Parallel() |
| 132 | s, m, g := incrementalRepo(t) |
| 133 | now := time.Date(2026, 10, 30, 12, 0, 0, 0, time.UTC) |
| 134 | iss := createIndexedIssue(t, g, "Working issue", now, []string{"bug"}, nil) |
| 135 | other := createIndexedIssue(t, g, "Other issue", now.Add(time.Minute), nil, nil) |
| 136 | if err := s.Ensure(m); err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | |
| 140 | comment, err := metadata.NewIssueComment("A comment", "keenan", now.Add(2*time.Minute)) |
| 141 | if err != nil { |
| 142 | t.Fatal(err) |
| 143 | } |
| 144 | if err := metadata.AddIssueComment(g, iss.ID, comment, "keenan"); err != nil { |
| 145 | t.Fatal(err) |
| 146 | } |
| 147 | if _, err := metadata.UpdateIssue(g, iss.ID, func(iss *metadata.Issue) error { |
| 148 | iss.Labels = []string{"cli"} |
| 149 | return nil |
| 150 | }, "keenan"); err != nil { |
| 151 | t.Fatal(err) |
| 152 | } |
| 153 | if err := s.Ensure(m); err != nil { |
| 154 | t.Fatal(err) |
| 155 | } |
| 156 | |
| 157 | rows, err := s.Issues(m.ID, Filter{}) |
| 158 | if err != nil { |
| 159 | t.Fatal(err) |
| 160 | } |
| 161 | if len(rows) != 2 { |
| 162 | t.Fatalf("issues = %d, want 2", len(rows)) |
| 163 | } |
| 164 | for _, row := range rows { |
| 165 | switch row.ID { |
| 166 | case iss.ID: |
| 167 | if row.Comments != 1 { |
| 168 | t.Fatalf("comment count = %d, want 1", row.Comments) |
| 169 | } |
| 170 | if len(row.Labels) != 1 || row.Labels[0] != "cli" { |
| 171 | t.Fatalf("labels = %v, want [cli]", row.Labels) |
| 172 | } |
| 173 | if row.Number != 1 { |
| 174 | t.Fatalf("number = %d, want 1", row.Number) |
| 175 | } |
| 176 | case other.ID: |
| 177 | if row.Number != 2 { |
| 178 | t.Fatalf("other number = %d, want 2", row.Number) |
| 179 | } |
| 180 | } |
| 181 | } |
| 182 | var numbered int |
| 183 | if err := s.db.QueryRow(`SELECT COUNT(*) FROM entity_numbers WHERE repo_id=?`, m.ID).Scan(&numbered); err != nil { |
| 184 | t.Fatal(err) |
| 185 | } |
| 186 | if numbered != 2 { |
| 187 | t.Fatalf("entity numbers = %d, want 2 (an edit must not consume one)", numbered) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | func TestEnsureRebuildsWhenTheIndexIsMissing(t *testing.T) { |
| 192 | t.Parallel() |
| 193 | s, m, g := incrementalRepo(t) |
| 194 | iss := createIndexedIssue(t, g, "Keep me", time.Date(2026, 10, 30, 12, 0, 0, 0, time.UTC), nil, nil) |
| 195 | if err := s.Ensure(m); err != nil { |
| 196 | t.Fatal(err) |
| 197 | } |
| 198 | if _, err := s.db.Exec(` |
| 199 | DELETE FROM issues; |
| 200 | DELETE FROM issue_labels; |
| 201 | DELETE FROM issue_assignees; |
| 202 | DELETE FROM entity_numbers; |
| 203 | UPDATE repos SET issues_sha='';`); err != nil { |
| 204 | t.Fatal(err) |
| 205 | } |
| 206 | if err := s.Ensure(m); err != nil { |
| 207 | t.Fatal(err) |
| 208 | } |
| 209 | rows, err := s.Issues(m.ID, Filter{}) |
| 210 | if err != nil { |
| 211 | t.Fatal(err) |
| 212 | } |
| 213 | if len(rows) != 1 || rows[0].ID != iss.ID || rows[0].Number != 1 { |
| 214 | t.Fatalf("rebuilt rows = %+v, want the issue at #1", rows) |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | func TestEnsureKeepsANumberSpentInsideTheRange(t *testing.T) { |
| 219 | t.Parallel() |
| 220 | s, m, g := incrementalRepo(t) |
| 221 | now := time.Date(2026, 10, 30, 12, 0, 0, 0, time.UTC) |
| 222 | if err := s.Ensure(m); err != nil { |
| 223 | t.Fatal(err) |
| 224 | } |
| 225 | // An entity created and deleted again between two refreshes still consumed a |
| 226 | // number. The next issue must not take it, or the index would disagree with |
| 227 | // the full history the branch can always reproduce. |
| 228 | gone := createIndexedIssue(t, g, "Deleted again", now, nil, nil) |
| 229 | if _, err := git.CommitFiles(g, metadata.Ref, func(files map[string][]byte) error { |
| 230 | delete(files, "issues/"+gone.ID+"/issue.md") |
| 231 | return nil |
| 232 | }, "delete the issue again", "keenan"); err != nil { |
| 233 | t.Fatal(err) |
| 234 | } |
| 235 | kept := createIndexedIssue(t, g, "Stays", now.Add(time.Minute), nil, nil) |
| 236 | if err := s.Ensure(m); err != nil { |
| 237 | t.Fatal(err) |
| 238 | } |
| 239 | |
| 240 | rows, err := s.Issues(m.ID, Filter{}) |
| 241 | if err != nil { |
| 242 | t.Fatal(err) |
| 243 | } |
| 244 | if len(rows) != 1 || rows[0].ID != kept.ID || rows[0].Number != 2 { |
| 245 | t.Fatalf("rows = %+v, want %s at #2", rows, kept.ID) |
| 246 | } |
| 247 | var numbered int |
| 248 | if err := s.db.QueryRow(`SELECT COUNT(*) FROM entity_numbers WHERE repo_id=?`, m.ID).Scan(&numbered); err != nil { |
| 249 | t.Fatal(err) |
| 250 | } |
| 251 | if numbered != 2 { |
| 252 | t.Fatalf("entity numbers = %d, want 2 (the deleted creation keeps its number)", numbered) |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | // TestEnsureDerivesNewNumbersFromTheRangeOnly is the regression test for the |
| 257 | // cost a long branch used to add to every refresh: the numbering request names a |
| 258 | // commit range, not the whole tip. |
| 259 | // |
| 260 | // It is not parallel because it sets GIT_TRACE for the git subprocesses the |
| 261 | // fixture starts. |
| 262 | func TestEnsureDerivesNewNumbersFromTheRangeOnly(t *testing.T) { |
| 263 | s, m, g := incrementalRepo(t) |
| 264 | // History under a path the numbering does not care about. A full walk still |
| 265 | // has to traverse these commits; the range does not. |
| 266 | for i := 0; i < 10; i++ { |
| 267 | if _, err := git.CommitFiles(g, metadata.Ref, func(files map[string][]byte) error { |
| 268 | files[fmt.Sprintf("runs/old%d/1/record.md", i)] = []byte("record\n") |
| 269 | return nil |
| 270 | }, "old record", "keenan"); err != nil { |
| 271 | t.Fatal(err) |
| 272 | } |
| 273 | } |
| 274 | now := time.Date(2026, 10, 30, 12, 0, 0, 0, time.UTC) |
| 275 | createIndexedIssue(t, g, "First", now, nil, nil) |
| 276 | if err := s.Ensure(m); err != nil { |
| 277 | t.Fatal(err) |
| 278 | } |
| 279 | |
| 280 | trace := filepath.Join(t.TempDir(), "git.trace") |
| 281 | t.Setenv("GIT_TRACE", trace) |
| 282 | createIndexedIssue(t, g, "Second", now.Add(time.Minute), nil, nil) |
| 283 | if err := s.Ensure(m); err != nil { |
| 284 | t.Fatal(err) |
| 285 | } |
| 286 | data, err := os.ReadFile(trace) |
| 287 | if err != nil { |
| 288 | t.Fatal(err) |
| 289 | } |
| 290 | seen := 0 |
| 291 | for _, line := range strings.Split(string(data), "\n") { |
| 292 | if !strings.Contains(line, "--diff-filter=A") { |
| 293 | continue |
| 294 | } |
| 295 | seen++ |
| 296 | if !strings.Contains(line, "..") { |
| 297 | t.Fatalf("numbering walked the whole tip instead of the new range: %s", line) |
| 298 | } |
| 299 | } |
| 300 | if seen == 0 { |
| 301 | t.Fatal("no numbering git call was traced; the test did not observe the refresh") |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | // TestEnsureDatesAnIssueFromItsNewestChange pins what the issues list sorts by. |
| 306 | // The metadata branch has no updated field, so the date comes from the commits: |
| 307 | // a comment, an edit, or an edit reverted inside the refreshed range all move it, |
| 308 | // and a full rebuild derives the same value as the incremental refresh. |
| 309 | func TestEnsureDatesAnIssueFromItsNewestChange(t *testing.T) { |
| 310 | s, m, g := incrementalRepo(t) |
| 311 | base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| 312 | // The commit dates are the fixture; the branch's own commits would otherwise |
| 313 | // all land in the same second and the test could not tell them apart. |
| 314 | at := func(offset time.Duration) { |
| 315 | t.Helper() |
| 316 | date := fmt.Sprintf("%d +0000", base.Add(offset).Unix()) |
| 317 | t.Setenv("GIT_AUTHOR_DATE", date) |
| 318 | t.Setenv("GIT_COMMITTER_DATE", date) |
| 319 | } |
| 320 | rowOf := func(t *testing.T, id string) IssueRow { |
| 321 | t.Helper() |
| 322 | rows, err := s.Issues(m.ID, Filter{}) |
| 323 | if err != nil { |
| 324 | t.Fatal(err) |
| 325 | } |
| 326 | for _, row := range rows { |
| 327 | if row.ID == id { |
| 328 | return row |
| 329 | } |
| 330 | } |
| 331 | t.Fatalf("no indexed row for %s", id) |
| 332 | return IssueRow{} |
| 333 | } |
| 334 | |
| 335 | at(time.Hour) |
| 336 | first := createIndexedIssue(t, g, "First", base.Add(time.Hour), nil, nil) |
| 337 | at(2 * time.Hour) |
| 338 | second := createIndexedIssue(t, g, "Second", base.Add(2*time.Hour), nil, nil) |
| 339 | if err := s.Ensure(m); err != nil { |
| 340 | t.Fatal(err) |
| 341 | } |
| 342 | if got := rowOf(t, first.ID).Updated; !got.Equal(base.Add(time.Hour)) { |
| 343 | t.Fatalf("a fresh issue updated = %v, want its opening commit %v", got, base.Add(time.Hour)) |
| 344 | } |
| 345 | |
| 346 | // A comment is activity: the issue it is on becomes the most recently updated |
| 347 | // one, even though the other issue was filed later. |
| 348 | at(3 * time.Hour) |
| 349 | comment, err := metadata.NewIssueComment("A comment", "keenan", base.Add(3*time.Hour)) |
| 350 | if err != nil { |
| 351 | t.Fatal(err) |
| 352 | } |
| 353 | if err := metadata.AddIssueComment(g, first.ID, comment, "keenan"); err != nil { |
| 354 | t.Fatal(err) |
| 355 | } |
| 356 | if err := s.Ensure(m); err != nil { |
| 357 | t.Fatal(err) |
| 358 | } |
| 359 | if got := rowOf(t, first.ID).Updated; !got.Equal(base.Add(3 * time.Hour)) { |
| 360 | t.Fatalf("updated after a comment = %v, want %v", got, base.Add(3*time.Hour)) |
| 361 | } |
| 362 | if got := rowOf(t, second.ID).Updated; !got.Equal(base.Add(2 * time.Hour)) { |
| 363 | t.Fatalf("the untouched issue updated = %v, want %v", got, base.Add(2*time.Hour)) |
| 364 | } |
| 365 | rows, err := s.Issues(m.ID, Filter{}) |
| 366 | if err != nil { |
| 367 | t.Fatal(err) |
| 368 | } |
| 369 | if len(rows) != 2 || rows[0].ID != first.ID { |
| 370 | t.Fatalf("default order = %+v, want the commented issue first", rows) |
| 371 | } |
| 372 | |
| 373 | // Two edits that cancel out leave the tree where it started. The tree diff |
| 374 | // sees nothing; the commits still happened, and the list has to say so. |
| 375 | at(4 * time.Hour) |
| 376 | if _, err := metadata.UpdateIssue(g, first.ID, func(iss *metadata.Issue) error { |
| 377 | iss.Labels = []string{"cli"} |
| 378 | return nil |
| 379 | }, "keenan"); err != nil { |
| 380 | t.Fatal(err) |
| 381 | } |
| 382 | at(5 * time.Hour) |
| 383 | if _, err := metadata.UpdateIssue(g, first.ID, func(iss *metadata.Issue) error { |
| 384 | iss.Labels = nil |
| 385 | return nil |
| 386 | }, "keenan"); err != nil { |
| 387 | t.Fatal(err) |
| 388 | } |
| 389 | if err := s.Ensure(m); err != nil { |
| 390 | t.Fatal(err) |
| 391 | } |
| 392 | incremental := rowOf(t, first.ID).Updated |
| 393 | if !incremental.Equal(base.Add(5 * time.Hour)) { |
| 394 | t.Fatalf("updated after an edit and its revert = %v, want %v", incremental, base.Add(5*time.Hour)) |
| 395 | } |
| 396 | |
| 397 | // A rebuild walks the whole branch. It must land on the same date, or the two |
| 398 | // ways of filling the index would disagree about the same repository. |
| 399 | if err := s.IndexRepo(m); err != nil { |
| 400 | t.Fatal(err) |
| 401 | } |
| 402 | if rebuilt := rowOf(t, first.ID).Updated; !rebuilt.Equal(incremental) { |
| 403 | t.Fatalf("rebuilt updated = %v, incremental = %v", rebuilt, incremental) |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | // TestEnsureDatesARequestFromItsNewestChange is the request half of the list's |
| 408 | // activity sort. A comment or an approval moves the date; the incremental refresh |
| 409 | // and a full rebuild derive the same one. |
| 410 | func TestEnsureDatesARequestFromItsNewestChange(t *testing.T) { |
| 411 | s, m, g := incrementalRepo(t) |
| 412 | base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| 413 | at := func(offset time.Duration) { |
| 414 | t.Helper() |
| 415 | date := fmt.Sprintf("%d +0000", base.Add(offset).Unix()) |
| 416 | t.Setenv("GIT_AUTHOR_DATE", date) |
| 417 | t.Setenv("GIT_COMMITTER_DATE", date) |
| 418 | } |
| 419 | |
| 420 | at(time.Hour) |
| 421 | first := createIndexedRequest(t, g, "First change", base.Add(time.Hour), nil) |
| 422 | at(2 * time.Hour) |
| 423 | second := createIndexedRequest(t, g, "Second change", base.Add(2*time.Hour), nil) |
| 424 | if err := s.Ensure(m); err != nil { |
| 425 | t.Fatal(err) |
| 426 | } |
| 427 | rowOf := func(t *testing.T, id string) RequestRow { |
| 428 | t.Helper() |
| 429 | rows, err := s.Requests(m.ID, RequestFilter{}) |
| 430 | if err != nil { |
| 431 | t.Fatal(err) |
| 432 | } |
| 433 | for _, row := range rows { |
| 434 | if row.ID == id { |
| 435 | return row |
| 436 | } |
| 437 | } |
| 438 | t.Fatalf("no indexed row for %s", id) |
| 439 | return RequestRow{} |
| 440 | } |
| 441 | if got := rowOf(t, first.ID).Updated; !got.Equal(base.Add(time.Hour)) { |
| 442 | t.Fatalf("a fresh request updated = %v, want its opening commit %v", got, base.Add(time.Hour)) |
| 443 | } |
| 444 | |
| 445 | // A comment moves the date, and the commented change takes the first row. |
| 446 | at(3 * time.Hour) |
| 447 | comment, err := metadata.NewRequestComment("A review note", "wilbur", base.Add(3*time.Hour)) |
| 448 | if err != nil { |
| 449 | t.Fatal(err) |
| 450 | } |
| 451 | if err := metadata.AddRequestComment(g, first.ID, comment, "wilbur"); err != nil { |
| 452 | t.Fatal(err) |
| 453 | } |
| 454 | if err := s.Ensure(m); err != nil { |
| 455 | t.Fatal(err) |
| 456 | } |
| 457 | if got := rowOf(t, first.ID).Updated; !got.Equal(base.Add(3 * time.Hour)) { |
| 458 | t.Fatalf("updated after a comment = %v, want %v", got, base.Add(3*time.Hour)) |
| 459 | } |
| 460 | if got := rowOf(t, second.ID).Updated; !got.Equal(base.Add(2 * time.Hour)) { |
| 461 | t.Fatalf("the untouched request updated = %v, want %v", got, base.Add(2*time.Hour)) |
| 462 | } |
| 463 | rows, err := s.Requests(m.ID, RequestFilter{}) |
| 464 | if err != nil { |
| 465 | t.Fatal(err) |
| 466 | } |
| 467 | if len(rows) != 2 || rows[0].ID != first.ID { |
| 468 | t.Fatalf("default order = %+v, want the commented change first", rows) |
| 469 | } |
| 470 | |
| 471 | // An approval is activity too: it changes the request's directory. |
| 472 | at(4 * time.Hour) |
| 473 | if _, err := metadata.ApproveRequest(g, first.ID, "wilbur", "main", requestHead, base.Add(4*time.Hour), "wilbur"); err != nil { |
| 474 | t.Fatal(err) |
| 475 | } |
| 476 | if err := s.Ensure(m); err != nil { |
| 477 | t.Fatal(err) |
| 478 | } |
| 479 | incremental := rowOf(t, first.ID).Updated |
| 480 | if !incremental.Equal(base.Add(4 * time.Hour)) { |
| 481 | t.Fatalf("updated after an approval = %v, want %v", incremental, base.Add(4*time.Hour)) |
| 482 | } |
| 483 | |
| 484 | if err := s.IndexRepo(m); err != nil { |
| 485 | t.Fatal(err) |
| 486 | } |
| 487 | if rebuilt := rowOf(t, first.ID).Updated; !rebuilt.Equal(incremental) { |
| 488 | t.Fatalf("rebuilt updated = %v, incremental = %v", rebuilt, incremental) |
| 489 | } |
| 490 | } |
| 491 |