| 1 | // internal/appconfig/environments_test.go |
| 2 | package appconfig |
| 3 | |
| 4 | import ( |
| 5 | "context" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | ) |
| 11 | |
| 12 | func TestEnvironmentKeyIsAccountAndAppScoped(t *testing.T) { |
| 13 | got := EnvironmentKey("bab", "keymail", "demo") |
| 14 | want := "apps/bab/keymail/environments/demo.json" |
| 15 | if got != want { |
| 16 | t.Fatalf("EnvironmentKey(bab, keymail, demo) = %q, want %q", got, want) |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | func TestValidEnvironmentName(t *testing.T) { |
| 21 | valid := []string{"demo", "a", "connect-timeslots", "yste-demo", "0-9"} |
| 22 | invalid := []string{ |
| 23 | "", "default", // reserved / empty |
| 24 | "Demo", // uppercase |
| 25 | "has space", // space |
| 26 | "has_us", // underscore not allowed |
| 27 | "UP", |
| 28 | string(make([]byte, 33)), // too long (33 NUL bytes, also fails the char class) |
| 29 | } |
| 30 | for _, n := range valid { |
| 31 | if !ValidEnvironmentName(n) { |
| 32 | t.Errorf("ValidEnvironmentName(%q) = false, want true", n) |
| 33 | } |
| 34 | } |
| 35 | for _, n := range invalid { |
| 36 | if ValidEnvironmentName(n) { |
| 37 | t.Errorf("ValidEnvironmentName(%q) = true, want false", n) |
| 38 | } |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | func TestValidEnvironmentNameLengthBoundary(t *testing.T) { |
| 43 | ok32 := "01234567890123456789012345678901" // 32 chars |
| 44 | if len(ok32) != 32 { |
| 45 | t.Fatalf("test fixture is %d chars, want 32", len(ok32)) |
| 46 | } |
| 47 | if !ValidEnvironmentName(ok32) { |
| 48 | t.Errorf("ValidEnvironmentName(32 chars) = false, want true") |
| 49 | } |
| 50 | tooLong := ok32 + "x" |
| 51 | if ValidEnvironmentName(tooLong) { |
| 52 | t.Errorf("ValidEnvironmentName(33 chars) = true, want false") |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | func TestLoadEnvironmentOnMissingBundleReturnsEmpty(t *testing.T) { |
| 57 | s := release.NewDirStore(t.TempDir()) |
| 58 | m, err := LoadEnvironment(context.Background(), s, "bab", "keymail", "demo") |
| 59 | if err != nil { |
| 60 | t.Fatalf("LoadEnvironment: %v", err) |
| 61 | } |
| 62 | if len(m.Vars) != 0 { |
| 63 | t.Fatalf("Vars = %v, want empty for a never-configured environment", m.Vars) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | func TestLoadEnvironmentRejectsReservedName(t *testing.T) { |
| 68 | s := release.NewDirStore(t.TempDir()) |
| 69 | if _, err := LoadEnvironment(context.Background(), s, "bab", "keymail", "default"); err == nil { |
| 70 | t.Fatal("LoadEnvironment(..., \"default\") accepted, want a refusal") |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | func TestSaveEnvironmentThenLoad(t *testing.T) { |
| 75 | s := release.NewDirStore(t.TempDir()) |
| 76 | ctx := context.Background() |
| 77 | if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", |
| 78 | map[string]Var{"LOG_LEVEL": {Kind: "env", Value: "debug"}}, nil); err != nil { |
| 79 | t.Fatalf("SaveEnvironment: %v", err) |
| 80 | } |
| 81 | m, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo") |
| 82 | if err != nil { |
| 83 | t.Fatalf("LoadEnvironment: %v", err) |
| 84 | } |
| 85 | if v := m.Vars["LOG_LEVEL"]; v.Kind != "env" || v.Value != "debug" { |
| 86 | t.Fatalf("LOG_LEVEL = %+v, want kind env value debug", v) |
| 87 | } |
| 88 | // The default bundle (config.json) must be untouched by a named-bundle write. |
| 89 | def, err := Get(ctx, s, "bab", "keymail") |
| 90 | if err != nil { |
| 91 | t.Fatalf("Get (default): %v", err) |
| 92 | } |
| 93 | if len(def.Vars) != 0 { |
| 94 | t.Fatalf("default bundle vars = %v, want empty — SaveEnvironment must not touch config.json", def.Vars) |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func TestSaveEnvironmentRejectsReservedOrInvalidName(t *testing.T) { |
| 99 | s := release.NewDirStore(t.TempDir()) |
| 100 | ctx := context.Background() |
| 101 | for _, bad := range []string{"", "default", "Has-Upper", "has_underscore"} { |
| 102 | if err := SaveEnvironment(ctx, s, "bab", "keymail", bad, |
| 103 | map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err == nil { |
| 104 | t.Errorf("SaveEnvironment(..., %q, ...) accepted, want a refusal", bad) |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func TestSaveEnvironmentNewerSchemaRefused(t *testing.T) { |
| 110 | s := release.NewDirStore(t.TempDir()) |
| 111 | ctx := context.Background() |
| 112 | raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`) |
| 113 | if _, err := s.Put(ctx, EnvironmentKey("bab", "keymail", "demo"), raw, ""); err != nil { |
| 114 | t.Fatalf("Put: %v", err) |
| 115 | } |
| 116 | if _, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo"); err == nil { |
| 117 | t.Fatal("LoadEnvironment read a schema-2 bundle without complaint, want a refusal") |
| 118 | } |
| 119 | if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", |
| 120 | map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err == nil { |
| 121 | t.Fatal("SaveEnvironment wrote over a schema-2 bundle, want a refusal") |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestListEnvironments(t *testing.T) { |
| 126 | s := release.NewDirStore(t.TempDir()) |
| 127 | ctx := context.Background() |
| 128 | for _, name := range []string{"demo", "connect", "timeslots"} { |
| 129 | if err := SaveEnvironment(ctx, s, "bab", "tito", name, |
| 130 | map[string]Var{"X": {Kind: "env", Value: "1"}}, nil); err != nil { |
| 131 | t.Fatalf("SaveEnvironment(%s): %v", name, err) |
| 132 | } |
| 133 | } |
| 134 | // A different app's environment must not leak in. |
| 135 | if err := SaveEnvironment(ctx, s, "bab", "other-app", "demo", |
| 136 | map[string]Var{"X": {Kind: "env", Value: "1"}}, nil); err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | names, err := ListEnvironments(ctx, s, "bab", "tito") |
| 140 | if err != nil { |
| 141 | t.Fatalf("ListEnvironments: %v", err) |
| 142 | } |
| 143 | want := []string{"connect", "demo", "timeslots"} |
| 144 | if len(names) != len(want) { |
| 145 | t.Fatalf("ListEnvironments = %v, want %v", names, want) |
| 146 | } |
| 147 | for i := range want { |
| 148 | if names[i] != want[i] { |
| 149 | t.Fatalf("ListEnvironments = %v, want %v", names, want) |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | func TestListEnvironmentsEmptyWhenNoneConfigured(t *testing.T) { |
| 155 | s := release.NewDirStore(t.TempDir()) |
| 156 | names, err := ListEnvironments(context.Background(), s, "bab", "keymail") |
| 157 | if err != nil { |
| 158 | t.Fatalf("ListEnvironments: %v", err) |
| 159 | } |
| 160 | if len(names) != 0 { |
| 161 | t.Fatalf("ListEnvironments = %v, want empty", names) |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | // --- LoadMerged --------------------------------------------------------- |
| 166 | |
| 167 | func TestLoadMergedEmptyEnvNameIsDefaultOnly(t *testing.T) { |
| 168 | s := release.NewDirStore(t.TempDir()) |
| 169 | ctx := context.Background() |
| 170 | if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "default-a"}); err != nil { |
| 171 | t.Fatal(err) |
| 172 | } |
| 173 | m, err := LoadMerged(ctx, s, "bab", "keymail", "") |
| 174 | if err != nil { |
| 175 | t.Fatalf("LoadMerged: %v", err) |
| 176 | } |
| 177 | if len(m.Vars) != 1 || m.Vars["A"].Value != "default-a" { |
| 178 | t.Fatalf("LoadMerged(envName=\"\") = %+v, want just the default bundle", m.Vars) |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | // Matches Sync's current behaviour for an unconfigured app: no default |
| 183 | // object AND envName == "" resolves to an empty manifest, not an error. |
| 184 | func TestLoadMergedNoDefaultNoEnvNameIsEmptyNotError(t *testing.T) { |
| 185 | s := release.NewDirStore(t.TempDir()) |
| 186 | m, err := LoadMerged(context.Background(), s, "bab", "keymail", "") |
| 187 | if err != nil { |
| 188 | t.Fatalf("LoadMerged: %v, want no error for an unconfigured app", err) |
| 189 | } |
| 190 | if len(m.Vars) != 0 { |
| 191 | t.Fatalf("Vars = %v, want empty", m.Vars) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | func TestLoadMergedEnvironmentOverlaysDefaultWinningPerKey(t *testing.T) { |
| 196 | s := release.NewDirStore(t.TempDir()) |
| 197 | ctx := context.Background() |
| 198 | if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{ |
| 199 | "SHARED": "default-value", "DEFAULT_ONLY": "d", |
| 200 | }); err != nil { |
| 201 | t.Fatal(err) |
| 202 | } |
| 203 | if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ |
| 204 | "SHARED": {Kind: "env", Value: "demo-value"}, |
| 205 | "ENV_ONLY": {Kind: "env", Value: "e"}, |
| 206 | }, nil); err != nil { |
| 207 | t.Fatal(err) |
| 208 | } |
| 209 | m, err := LoadMerged(ctx, s, "bab", "keymail", "demo") |
| 210 | if err != nil { |
| 211 | t.Fatalf("LoadMerged: %v", err) |
| 212 | } |
| 213 | if len(m.Vars) != 3 { |
| 214 | t.Fatalf("Vars = %+v, want 3 keys (SHARED, DEFAULT_ONLY, ENV_ONLY)", m.Vars) |
| 215 | } |
| 216 | if m.Vars["SHARED"].Value != "demo-value" { |
| 217 | t.Fatalf("SHARED = %q, want the environment's overlay value to win", m.Vars["SHARED"].Value) |
| 218 | } |
| 219 | if m.Vars["DEFAULT_ONLY"].Value != "d" { |
| 220 | t.Fatalf("DEFAULT_ONLY = %q, want the default's value preserved", m.Vars["DEFAULT_ONLY"].Value) |
| 221 | } |
| 222 | if m.Vars["ENV_ONLY"].Value != "e" { |
| 223 | t.Fatalf("ENV_ONLY = %q, want the environment-only value preserved", m.Vars["ENV_ONLY"].Value) |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | // A MISSING default with a present, named environment resolves to the |
| 228 | // environment alone — not an error, and not folded into an empty manifest. |
| 229 | func TestLoadMergedEnvironmentOnlyNoDefault(t *testing.T) { |
| 230 | s := release.NewDirStore(t.TempDir()) |
| 231 | ctx := context.Background() |
| 232 | if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", |
| 233 | map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { |
| 234 | t.Fatal(err) |
| 235 | } |
| 236 | m, err := LoadMerged(ctx, s, "bab", "keymail", "demo") |
| 237 | if err != nil { |
| 238 | t.Fatalf("LoadMerged: %v", err) |
| 239 | } |
| 240 | if len(m.Vars) != 1 || m.Vars["A"].Value != "1" { |
| 241 | t.Fatalf("LoadMerged(env-only) = %+v, want just the environment's vars", m.Vars) |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | // A missing environment named EXPLICITLY is an error: the caller bound a |
| 246 | // route to it, so its absence is a defect, not an empty set — distinct from |
| 247 | // envName == "", which is a legitimate "no environment bound" state. |
| 248 | func TestLoadMergedMissingNamedEnvironmentErrors(t *testing.T) { |
| 249 | s := release.NewDirStore(t.TempDir()) |
| 250 | ctx := context.Background() |
| 251 | if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err != nil { |
| 252 | t.Fatal(err) |
| 253 | } |
| 254 | if _, err := LoadMerged(ctx, s, "bab", "keymail", "demo"); err == nil { |
| 255 | t.Fatal("LoadMerged with a named-but-absent environment: got nil error, want one") |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | func TestLoadMergedRejectsInvalidEnvironmentName(t *testing.T) { |
| 260 | s := release.NewDirStore(t.TempDir()) |
| 261 | if _, err := LoadMerged(context.Background(), s, "bab", "keymail", "default"); err == nil { |
| 262 | t.Fatal("LoadMerged(..., \"default\") accepted, want a refusal — \"default\" is not a named environment") |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | func TestLoadMergedSchemaTooNewOnEitherSideRefuses(t *testing.T) { |
| 267 | s := release.NewDirStore(t.TempDir()) |
| 268 | ctx := context.Background() |
| 269 | raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`) |
| 270 | if _, err := s.Put(ctx, EnvironmentKey("bab", "keymail", "demo"), raw, ""); err != nil { |
| 271 | t.Fatal(err) |
| 272 | } |
| 273 | if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err != nil { |
| 274 | t.Fatal(err) |
| 275 | } |
| 276 | if _, err := LoadMerged(ctx, s, "bab", "keymail", "demo"); err == nil { |
| 277 | t.Fatal("LoadMerged over a schema-2 environment bundle: got nil error, want one") |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | // Review CRITICAL 2, named-bundle half: an unset against a bundle that does |
| 282 | // not exist must not MINT it. A phantom empty bundle passes the route |
| 283 | // existence rail (`carlos route -environment`), which exists precisely to |
| 284 | // refuse binding a bundle that isn't there — and a route bound to it silently |
| 285 | // loses whatever environment it was on. |
| 286 | func TestUnsetEnvironmentOnMissingBundleErrorsAndMintsNothing(t *testing.T) { |
| 287 | s := release.NewDirStore(t.TempDir()) |
| 288 | ctx := context.Background() |
| 289 | removed, err := UnsetEnvironment(ctx, s, "bab", "keymail", "typoo", []string{"NOTHING"}) |
| 290 | if err == nil { |
| 291 | t.Fatalf("unset against a nonexistent bundle succeeded (removed %d), want a refusal", removed) |
| 292 | } |
| 293 | if !strings.Contains(err.Error(), "nothing to unset") { |
| 294 | t.Errorf("error = %v, want it to say there is nothing to unset", err) |
| 295 | } |
| 296 | names, err := ListEnvironments(ctx, s, "bab", "keymail") |
| 297 | if err != nil { |
| 298 | t.Fatalf("ListEnvironments: %v", err) |
| 299 | } |
| 300 | if len(names) != 0 { |
| 301 | t.Fatalf("environments = %v, want none — the unset minted a phantom bundle", names) |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | // The same command against a bundle that DOES exist works, and reports how |
| 306 | // many keys it actually removed (review MINOR 9), not how many it was given. |
| 307 | func TestUnsetEnvironmentReportsActualRemovals(t *testing.T) { |
| 308 | s := release.NewDirStore(t.TempDir()) |
| 309 | ctx := context.Background() |
| 310 | if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ |
| 311 | "A": {Kind: "env", Value: "1"}, |
| 312 | "B": {Kind: "env", Value: "2"}, |
| 313 | }, nil); err != nil { |
| 314 | t.Fatal(err) |
| 315 | } |
| 316 | removed, err := UnsetEnvironment(ctx, s, "bab", "keymail", "demo", []string{"A", "NEVER_SET"}) |
| 317 | if err != nil { |
| 318 | t.Fatalf("UnsetEnvironment: %v", err) |
| 319 | } |
| 320 | if removed != 1 { |
| 321 | t.Fatalf("removed = %d, want 1 (A removed, NEVER_SET was never there)", removed) |
| 322 | } |
| 323 | m, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo") |
| 324 | if err != nil { |
| 325 | t.Fatal(err) |
| 326 | } |
| 327 | if _, ok := m.Vars["A"]; ok { |
| 328 | t.Fatal("A survived the unset") |
| 329 | } |
| 330 | if m.Vars["B"].Value != "2" { |
| 331 | t.Fatalf("B = %+v, want it untouched", m.Vars["B"]) |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | // --- issue #195: exec_delivery at environment scope --- |
| 336 | |
| 337 | func TestSetExecDeliveryAtEnvironmentRoundTrips(t *testing.T) { |
| 338 | ctx := context.Background() |
| 339 | s := release.NewDirStore(t.TempDir()) |
| 340 | // A bundle must exist before a flag can be written into it, exactly as |
| 341 | // the default bundle must. |
| 342 | if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", |
| 343 | map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { |
| 344 | t.Fatal(err) |
| 345 | } |
| 346 | if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { |
| 347 | t.Fatalf("set: %v", err) |
| 348 | } |
| 349 | m, err := LoadEnvironment(ctx, s, "bab", "titogo", "relay") |
| 350 | if err != nil { |
| 351 | t.Fatal(err) |
| 352 | } |
| 353 | if m.ExecDelivery != ExecDeliveryFile { |
| 354 | t.Fatalf("environment bundle exec_delivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile) |
| 355 | } |
| 356 | // The vars already in the bundle survive the flag write — read-modify- |
| 357 | // write, not replace. |
| 358 | if m.Vars["A"].Value != "1" { |
| 359 | t.Fatalf("A = %q, want it preserved at \"1\"", m.Vars["A"].Value) |
| 360 | } |
| 361 | // And the DEFAULT bundle is untouched: an environment-scoped write that |
| 362 | // leaked into config.json would opt the whole fleet in, which is the |
| 363 | // bug this closes. |
| 364 | def, found, err := getIfExists(ctx, s, "bab", "titogo") |
| 365 | if err != nil { |
| 366 | t.Fatal(err) |
| 367 | } |
| 368 | if found && def.ExecDelivery != "" { |
| 369 | t.Fatalf("default bundle exec_delivery = %q, want it untouched", def.ExecDelivery) |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | func TestSetExecDeliveryAtEnvironmentClearsToInherit(t *testing.T) { |
| 374 | ctx := context.Background() |
| 375 | s := release.NewDirStore(t.TempDir()) |
| 376 | if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", |
| 377 | map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { |
| 378 | t.Fatal(err) |
| 379 | } |
| 380 | if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { |
| 381 | t.Fatal(err) |
| 382 | } |
| 383 | if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ""); err != nil { |
| 384 | t.Fatalf("clear: %v", err) |
| 385 | } |
| 386 | m, err := LoadEnvironment(ctx, s, "bab", "titogo", "relay") |
| 387 | if err != nil { |
| 388 | t.Fatal(err) |
| 389 | } |
| 390 | if m.ExecDelivery != "" { |
| 391 | t.Fatalf("exec_delivery = %q, want it cleared to inherit", m.ExecDelivery) |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | func TestSetExecDeliveryAtRefusesAbsentBundle(t *testing.T) { |
| 396 | ctx := context.Background() |
| 397 | s := release.NewDirStore(t.TempDir()) |
| 398 | err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", "") |
| 399 | if err == nil { |
| 400 | t.Fatal("clearing the flag on a bundle that does not exist must be refused, not silently create one") |
| 401 | } |
| 402 | if !strings.Contains(err.Error(), "relay") { |
| 403 | t.Fatalf("refusal %q must name the bundle it could not find", err) |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | func TestSetExecDeliveryAtRefusesInvalidValue(t *testing.T) { |
| 408 | ctx := context.Background() |
| 409 | s := release.NewDirStore(t.TempDir()) |
| 410 | if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", |
| 411 | map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { |
| 412 | t.Fatal(err) |
| 413 | } |
| 414 | if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", "socket"); err == nil { |
| 415 | t.Fatal("an unknown exec_delivery value must be refused before it reaches the bucket") |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | func TestSetExecDeliveryAtRefusesInvalidEnvironmentName(t *testing.T) { |
| 420 | ctx := context.Background() |
| 421 | s := release.NewDirStore(t.TempDir()) |
| 422 | if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "Relay", ExecDeliveryFile); err == nil { |
| 423 | t.Fatal("an invalid environment name must be refused before any bucket write") |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | func TestSetExecDeliveryAtBumpsGeneration(t *testing.T) { |
| 428 | ctx := context.Background() |
| 429 | s := release.NewDirStore(t.TempDir()) |
| 430 | if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", |
| 431 | map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { |
| 432 | t.Fatal(err) |
| 433 | } |
| 434 | _, before, err := s.Get(ctx, GenKey("bab", "titogo")) |
| 435 | if err != nil { |
| 436 | t.Fatal(err) |
| 437 | } |
| 438 | if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { |
| 439 | t.Fatal(err) |
| 440 | } |
| 441 | _, after, err := s.Get(ctx, GenKey("bab", "titogo")) |
| 442 | if err != nil { |
| 443 | t.Fatal(err) |
| 444 | } |
| 445 | if before == after { |
| 446 | t.Fatal("an environment-scoped exec_delivery write must bump the generation object — the fast loop watches nothing else for it") |
| 447 | } |
| 448 | } |
| 449 | |