carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package caps
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7 "testing"
8)
9
10// TestUnknownCapabilityNameIsRefused pins the fail-closed half of spec §3b:
11// a capability name this binary does not know must refuse the whole block,
12// naming the capability and the fix — never silently drop it.
13func TestUnknownCapabilityNameIsRefused(t *testing.T) {
14 var s Set
15 err := json.Unmarshal([]byte(`{"v":1,"daemon":true,"gpu":true}`), &s)
16 if err == nil || !strings.Contains(err.Error(), "gpu") {
17 t.Fatalf("err = %v, want a refusal naming gpu", err)
18 }
19 if !strings.Contains(err.Error(), "update the platform binary") {
20 t.Fatalf("refusal %q does not name the fix", err)
21 }
22}
23
24// TestUnknownFieldInsideAKnownCapabilityIsIgnoredAndNoted pins the tolerant
25// half of spec §3b: room to grow a known capability additively without
26// breaking an older binary that already understands its other fields.
27func TestUnknownFieldInsideAKnownCapabilityIsIgnoredAndNoted(t *testing.T) {
28 var s Set
29 if err := json.Unmarshal([]byte(`{"v":1,"data":{"replicate":"platform","dbs":["console.db"],"future":7}}`), &s); err != nil {
30 t.Fatalf("unknown field inside a known capability must NOT fail: %v", err)
31 }
32 if s.Data == nil || s.Data.Replicate != DataReplicatePlatform || len(s.Data.DBs) != 1 {
33 t.Fatalf("known fields lost: %+v", s.Data)
34 }
35 if n := s.Notes(); len(n) != 1 || !strings.Contains(n[0], "future") {
36 t.Fatalf("notes = %v, want one line naming future", n)
37 }
38}
39
40// TestVersionBumpIsRefused pins fail-closed on "v": the box keeps serving
41// what it has rather than materialize a unit missing something new.
42func TestVersionBumpIsRefused(t *testing.T) {
43 var s Set
44 if err := json.Unmarshal([]byte(`{"v":2,"daemon":true}`), &s); err == nil {
45 t.Fatal("v:2 must be refused — fail closed, the box keeps what it serves")
46 }
47}
48
49// TestVMissingKeyIsRefusedHonestly pins review finding I3: an absent "v"
50// key — whether the object is entirely empty or merely misspells the key
51// ("V" instead of "v") — is refused with a message naming the missing key,
52// never the misleading "v 0" (which would suggest the block explicitly
53// asked for a version number that happens to be zero).
54func TestVMissingKeyIsRefusedHonestly(t *testing.T) {
55 for _, raw := range []string{`{}`, `{"V":1}`} {
56 var s Set
57 err := json.Unmarshal([]byte(raw), &s)
58 if err == nil {
59 t.Errorf("%s: accepted, want a refusal", raw)
60 continue
61 }
62 if !strings.Contains(err.Error(), "missing") {
63 t.Errorf("%s: err = %v, want an honest \"missing v\" message", raw, err)
64 }
65 if strings.Contains(err.Error(), "v 0") {
66 t.Errorf("%s: err = %v, must not say \"v 0\" — the key was never there to say 0", raw, err)
67 }
68 }
69}
70
71// TestVRangeAcceptedUpToVersionRefusedOutsideIt pins review finding I3's
72// range, generically against the Version constant rather than a hardcoded
73// number: every v in 1..Version decodes, and anything outside it (0,
74// negative, or Version+1) is refused. The day Version becomes 2, an
75// immutable v1 manifest already in a production bucket must still decode —
76// rollback depends on it.
77func TestVRangeAcceptedUpToVersionRefusedOutsideIt(t *testing.T) {
78 for v := 1; v <= Version; v++ {
79 var s Set
80 if err := json.Unmarshal([]byte(fmt.Sprintf(`{"v":%d}`, v)), &s); err != nil {
81 t.Errorf("v:%d (within 1..Version=%d) refused: %v", v, Version, err)
82 }
83 }
84 for _, v := range []int{0, -1, Version + 1} {
85 var s Set
86 if err := json.Unmarshal([]byte(fmt.Sprintf(`{"v":%d}`, v)), &s); err == nil {
87 t.Errorf("v:%d (outside 1..Version=%d) accepted", v, Version)
88 }
89 }
90}
91
92// TestValidCapabilitiesRefusesVOutsideRange pins review finding I6(b): the
93// same range check inside ValidCapabilities, exercised against Set literals
94// that never passed through UnmarshalJSON at all.
95func TestValidCapabilitiesRefusesVOutsideRange(t *testing.T) {
96 if err := ValidCapabilities("keymail", &Set{V: 0}); err == nil {
97 t.Fatal("Set{V:0} accepted by ValidCapabilities")
98 }
99 if err := ValidCapabilities("keymail", &Set{V: 2}); err == nil {
100 t.Fatal("Set{V:2} accepted by ValidCapabilities (Version is 1)")
101 }
102}
103
104// TestBareVBlockIsValidAndCommandEmptyMeansServe pins review finding I6:
105// {"v":1} alone (every other field absent) decodes and validates cleanly,
106// and an empty Command means the template's own "serve" — not an invalid
107// or a refused value.
108func TestBareVBlockIsValidAndCommandEmptyMeansServe(t *testing.T) {
109 var s Set
110 if err := json.Unmarshal([]byte(`{"v":1}`), &s); err != nil {
111 t.Fatalf(`bare {"v":1} refused: %v`, err)
112 }
113 if s.Command != "" {
114 t.Fatalf(`Command = %q, want "" (the template's own "serve")`, s.Command)
115 }
116 if err := ValidCapabilities("console", &s); err != nil {
117 t.Fatalf("bare v:1 block refused by ValidCapabilities: %v", err)
118 }
119}
120
121// TestKeymailSpecBlockRoundTripsAndValidates pins review finding I6(a):
122// decode the exact keymail capabilities block from
123// docs/superpowers/specs/2026-08-03-app-capabilities-design.md §3a, assert
124// every field, and confirm ValidCapabilities accepts it whole.
125func TestKeymailSpecBlockRoundTripsAndValidates(t *testing.T) {
126 const keymailBlock = `{
127 "v": 1,
128 "command": "relay",
129 "daemon": true,
130 "children": true,
131 "runtime_dir": "keymail",
132 "ports": [ { "port": 25, "name": "mx" } ],
133 "data": { "replicate": "app" },
134 "host_role": true
135 }`
136 var s Set
137 if err := json.Unmarshal([]byte(keymailBlock), &s); err != nil {
138 t.Fatalf("spec §3a keymail block failed to decode: %v", err)
139 }
140 if s.V != 1 {
141 t.Errorf("V = %d, want 1", s.V)
142 }
143 if s.Command != "relay" {
144 t.Errorf("Command = %q, want relay", s.Command)
145 }
146 if !s.Daemon {
147 t.Error("Daemon = false, want true")
148 }
149 if !s.Children {
150 t.Error("Children = false, want true")
151 }
152 if s.RuntimeDir != "keymail" {
153 t.Errorf("RuntimeDir = %q, want keymail", s.RuntimeDir)
154 }
155 if len(s.Ports) != 1 || s.Ports[0].Port != 25 || s.Ports[0].Name != "mx" {
156 t.Errorf("Ports = %+v, want [{25 mx}]", s.Ports)
157 }
158 if s.Data == nil || s.Data.Replicate != DataReplicateApp || len(s.Data.DBs) != 0 {
159 t.Errorf("Data = %+v, want {app []}", s.Data)
160 }
161 if !s.HostRole {
162 t.Error("HostRole = false, want true")
163 }
164 if n := s.Notes(); len(n) != 0 {
165 t.Errorf("Notes = %v, want none", n)
166 }
167 if err := ValidCapabilities("keymail", &s); err != nil {
168 t.Fatalf("ValidCapabilities(keymail, spec block) = %v, want nil", err)
169 }
170}
171
172// TestDocEngineDecodesAsAKnownCapability pins the wire shape of the new
173// capability: "doc_engine" is one of the closed vocabulary's names, so it
174// decodes to Set.DocEngine and is NOT refused as unknown and NOT recorded as an
175// ignored field. An absent doc_engine leaves it false (not declared).
176func TestDocEngineDecodesAsAKnownCapability(t *testing.T) {
177 var s Set
178 if err := json.Unmarshal([]byte(`{"v":1,"doc_engine":true}`), &s); err != nil {
179 t.Fatalf("doc_engine refused as unknown: %v", err)
180 }
181 if !s.DocEngine {
182 t.Error("DocEngine = false, want true")
183 }
184 if n := s.Notes(); len(n) != 0 {
185 t.Errorf("doc_engine recorded as an ignored field: %v", n)
186 }
187 if err := ValidCapabilities("gotenberg", &s); err != nil {
188 t.Fatalf("ValidCapabilities(doc_engine) = %v, want nil", err)
189 }
190
191 var absent Set
192 if err := json.Unmarshal([]byte(`{"v":1}`), &absent); err != nil {
193 t.Fatalf("decode: %v", err)
194 }
195 if absent.DocEngine {
196 t.Error("an absent doc_engine decoded to true, want false (not declared)")
197 }
198}
199
200// TestNilSetNotesDoesNotPanic pins review finding I1: every production
201// manifest today decodes to a nil Set, and the first ranged Notes() call in
202// Task 6's converge pass must not crash RunAdopt fleet-wide.
203func TestNilSetNotesDoesNotPanic(t *testing.T) {
204 var s *Set
205 if n := s.Notes(); n != nil {
206 t.Fatalf("nil Set.Notes() = %v, want nil", n)
207 }
208}
209
210// TestPortFieldCaseInsensitiveMatchIsNotApplied pins review finding I2: a
211// whole-object json.Unmarshal into Port would let Go's decoder case-fold an
212// unrecognised "PORT" onto the Port field via its default case-insensitive
213// tag matching, applying exactly the value the notes scan reports as
214// ignored. Decoding field-by-field from the exact-key fields map must
215// refuse to do that: "PORT" stays ignored and noted, and Port stays 0.
216func TestPortFieldCaseInsensitiveMatchIsNotApplied(t *testing.T) {
217 var s Set
218 if err := json.Unmarshal([]byte(`{"v":1,"ports":[{"PORT":25}]}`), &s); err != nil {
219 t.Fatalf("an unrecognised-cased field must not fail decode: %v", err)
220 }
221 if len(s.Ports) != 1 || s.Ports[0].Port != 0 || s.Ports[0].Name != "" {
222 t.Fatalf(`Ports = %+v, want [{0 ""}] — "PORT" must be ignored, not case-folded onto Port`, s.Ports)
223 }
224 if n := s.Notes(); len(n) != 1 || !strings.Contains(n[0], "PORT") {
225 t.Fatalf("notes = %v, want one line naming PORT", n)
226 }
227}
228
229// TestDataFieldCaseInsensitiveMatchIsNotApplied is TestPortFieldCase...'s
230// sibling for Data: "REPLICATE"/"DBS" must stay ignored-and-noted rather
231// than being case-folded onto Replicate/DBs (review finding I2).
232func TestDataFieldCaseInsensitiveMatchIsNotApplied(t *testing.T) {
233 var s Set
234 if err := json.Unmarshal([]byte(`{"v":1,"data":{"REPLICATE":"platform","DBS":["x.db"]}}`), &s); err != nil {
235 t.Fatalf("unrecognised-cased fields must not fail decode: %v", err)
236 }
237 if s.Data == nil || s.Data.Replicate != "" || len(s.Data.DBs) != 0 {
238 t.Fatalf("Data = %+v, want the zero value — REPLICATE/DBS must be ignored, not case-folded", s.Data)
239 }
240 if n := s.Notes(); len(n) != 2 {
241 t.Fatalf("notes = %v, want two lines naming REPLICATE and DBS", n)
242 }
243}
244
245// TestDataExplicitNullDecodesButIsRefused pins the Data doc comment's
246// asymmetry (review MINOR item): an explicit "data": null still allocates a
247// non-nil *Data (the zero value), which ValidCapabilities then refuses for
248// naming neither accepted replicate value — unlike an absent "data" key,
249// which leaves Data nil and valid.
250func TestDataExplicitNullDecodesButIsRefused(t *testing.T) {
251 var s Set
252 if err := json.Unmarshal([]byte(`{"v":1,"data":null}`), &s); err != nil {
253 t.Fatalf(`{"v":1,"data":null} failed to decode: %v`, err)
254 }
255 if s.Data == nil {
256 t.Fatal(`explicit "data": null must still decode to a non-nil *Data (the zero value)`)
257 }
258 if err := ValidCapabilities("console", &s); err == nil {
259 t.Fatal("a zero-value Data (from explicit \"data\": null) must be refused for naming no replicate value")
260 }
261}
262
263// TestCapabilitiesEmptyObjectWithNoVIsRefused pins the reviewer's sharpest
264// edge case: "capabilities": {} must NOT be read as "no capabilities" — an
265// absent "v" on a PRESENT block is refused (now with an honest "missing v"
266// message; see TestVMissingKeyIsRefusedHonestly). Only an absent
267// capabilities KEY (a nil *Set, never constructed) means "none".
268func TestCapabilitiesEmptyObjectWithNoVIsRefused(t *testing.T) {
269 var s Set
270 if err := json.Unmarshal([]byte(`{}`), &s); err == nil {
271 t.Fatal("{} (no v) must be refused, not silently mean no capabilities")
272 }
273}
274
275// TestRuntimeDirCharsetAndReservedNames pins the charset and the reserved
276// list, including the names added on review (finding C1). NOTE: the plan's
277// Step-1 listing included "" in this bad list, but that contradicts both
278// TestNilSetIsValidAndMeansNothing and TestPortsReservedAndNonPrivilegedRefused's
279// final assertion (a Set with only Ports set, RuntimeDir at its zero value
280// "", must be valid) — an empty RuntimeDir means "not declared", not
281// "declared as empty". "" is dropped here and pinned separately as valid
282// below.
283func TestRuntimeDirCharsetAndReservedNames(t *testing.T) {
284 bad := []string{
285 "../etc", "carlos/apps", "/run/keymail", "Keymail",
286 "carlos", "systemd", "systemd-foo", "lock", "log", "user",
287 "dbus", "sshd", "udev", strings.Repeat("a", 33),
288 // added on review (C1): common OS/tool-owned /run subdirectories,
289 // none of which is a substitute for converge's box-wide existence
290 // check (see the note on reservedRuntimeDir and the Task 6 plan
291 // addendum) — this list can never be exhaustive.
292 "credentials", "sudo", "mount", "network", "host", "netns", "shm",
293 }
294 for _, name := range bad {
295 s := &Set{V: 1, RuntimeDir: name}
296 if err := ValidCapabilities("keymail", s); err == nil {
297 t.Errorf("runtime_dir %q was accepted", name)
298 }
299 }
300 if err := ValidCapabilities("keymail", &Set{V: 1, RuntimeDir: "keymail"}); err != nil {
301 t.Fatalf("keymail refused: %v", err)
302 }
303}
304
305// TestRuntimeDirEmptyMeansNotDeclared pins the deviation explained in the
306// comment above: the zero value is "not declared", not an invalid name.
307func TestRuntimeDirEmptyMeansNotDeclared(t *testing.T) {
308 if err := ValidCapabilities("keymail", &Set{V: 1, RuntimeDir: ""}); err != nil {
309 t.Fatalf("empty runtime_dir (not declared) must be valid: %v", err)
310 }
311}
312
313// TestPortsReservedAndNonPrivilegedRefused pins the port range and the
314// absolute reservation of 22/80/443 — an operator typo must not be able to
315// hand a tenant the edge's ports or the box's SSH (spec §7).
316func TestPortsReservedAndNonPrivilegedRefused(t *testing.T) {
317 for _, p := range []int{0, 22, 80, 443, 1024, 8080, 65535, -1} {
318 if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: p}}}); err == nil {
319 t.Errorf("port %d was accepted", p)
320 }
321 }
322 if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: 25, Name: "mx"}}}); err != nil {
323 t.Fatalf("port 25 refused: %v", err)
324 }
325}
326
327// TestPort1023IsAccepted pins the upper boundary of the privileged range
328// (review MINOR item): 1023 is the last privileged port and must be
329// accepted, not off-by-one refused.
330func TestPort1023IsAccepted(t *testing.T) {
331 if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: 1023}}}); err != nil {
332 t.Fatalf("port 1023 (upper bound) refused: %v", err)
333 }
334}
335
336// TestDuplicatePortIsRefused pins the plan's "Plus:" line item: the same
337// port twice in one ports list is refused rather than silently deduplicated.
338func TestDuplicatePortIsRefused(t *testing.T) {
339 if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: 25}, {Port: 25}}}); err == nil {
340 t.Fatal("duplicate port 25 was accepted")
341 }
342}
343
344// TestTooManyPortsIsRefused pins the len(ports) > 4 bound — a bound, not a
345// feature.
346func TestTooManyPortsIsRefused(t *testing.T) {
347 ports := []Port{{Port: 1}, {Port: 2}, {Port: 3}, {Port: 4}, {Port: 5}}
348 if err := ValidCapabilities("keymail", &Set{V: 1, Ports: ports}); err == nil {
349 t.Fatal("5 ports (bound is 4) was accepted")
350 }
351 if err := ValidCapabilities("keymail", &Set{V: 1, Ports: ports[:4]}); err != nil {
352 t.Fatalf("4 ports (at the bound) refused: %v", err)
353 }
354}
355
356// TestPortNameCharsetIsRefused pins ports[].name's charset — it lands in
357// logs.
358func TestPortNameCharsetIsRefused(t *testing.T) {
359 if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: 25, Name: "Mail Exchange!"}}}); err == nil {
360 t.Fatal("port name with spaces and punctuation was accepted")
361 }
362}
363
364// TestHostRoleRefusesAnAppNameTooLongForAUser pins the account-name limit,
365// refused at ship where a human can rename.
366func TestHostRoleRefusesAnAppNameTooLongForAUser(t *testing.T) {
367 long := strings.Repeat("a", 30) // carlos-<30> = 37 > 32
368 if err := ValidCapabilities(long, &Set{V: 1, HostRole: true}); err == nil {
369 t.Fatal("host_role on an over-long app name must be refused at ship")
370 }
371}
372
373// TestAppUserLengthBoundary pins the exact edge (review MINOR item):
374// carlos-<app> at exactly 32 characters is accepted, 33 is refused.
375func TestAppUserLengthBoundary(t *testing.T) {
376 at32 := strings.Repeat("a", 25) // "carlos-" (7) + 25 = 32
377 if err := ValidCapabilities(at32, &Set{V: 1, HostRole: true}); err != nil {
378 t.Fatalf("app user at exactly 32 chars refused: %v", err)
379 }
380 at33 := strings.Repeat("a", 26) // "carlos-" (7) + 26 = 33
381 if err := ValidCapabilities(at33, &Set{V: 1, HostRole: true}); err == nil {
382 t.Fatal("app user at 33 chars (one over the limit) was accepted")
383 }
384}
385
386// TestHostRoleRefusesAppNameCharsetBeforeLength pins review finding I5: an
387// app name failing the token charset is refused even when it would pass
388// the length check — release.ValidApp's charset is checked first, as
389// defense in depth for the User=<AppUser(app)> sink.
390func TestHostRoleRefusesAppNameCharsetBeforeLength(t *testing.T) {
391 for _, bad := range []string{"Keymail", "1relay", "key_mail", "key mail"} {
392 if err := ValidCapabilities(bad, &Set{V: 1, HostRole: true}); err == nil {
393 t.Errorf("host_role with app name %q (fails the charset) was accepted", bad)
394 }
395 }
396}
397
398// TestNilSetIsValidAndMeansNothing pins today's every-manifest behaviour:
399// no capabilities key parses to a nil *Set, and a nil *Set is valid.
400func TestNilSetIsValidAndMeansNothing(t *testing.T) {
401 if err := ValidCapabilities("console", nil); err != nil {
402 t.Fatalf("nil (today's every manifest) must be valid: %v", err)
403 }
404}
405
406// TestCommandCharsetIsRefused pins the plan's "Plus:" line item: a command
407// is one lowercase token, never flags or a capitalized subcommand.
408func TestCommandCharsetIsRefused(t *testing.T) {
409 for _, bad := range []string{"relay --flag", "Relay", "1relay", "-relay", "a b", strings.Repeat("a", 33)} {
410 if err := ValidCapabilities("keymail", &Set{V: 1, Command: bad}); err == nil {
411 t.Errorf("command %q was accepted", bad)
412 }
413 }
414 if err := ValidCapabilities("keymail", &Set{V: 1, Command: "relay"}); err != nil {
415 t.Fatalf("command \"relay\" refused: %v", err)
416 }
417}
418
419// TestDataDBsCharsetAndSeparatorRefusals pins the "used only as
420// filepath.Join(stateDir, name)" rail: dbs entries must be bare filenames,
421// never ".", "..", or containing any separator.
422func TestDataDBsCharsetAndSeparatorRefusals(t *testing.T) {
423 for _, bad := range []string{".", "..", "console.db/../../etc/passwd", "a/b", "con*", strings.Repeat("a", 65)} {
424 s := &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{bad}}}
425 if err := ValidCapabilities("console", s); err == nil {
426 t.Errorf("dbs entry %q was accepted", bad)
427 }
428 }
429 if err := ValidCapabilities("console", &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db"}}}); err != nil {
430 t.Fatalf("console.db refused: %v", err)
431 }
432}
433
434// TestDataDBsDuplicateIsRefused pins review finding I4: a repeated dbs name
435// is refused, mirroring validPorts' duplicate check — two litestream
436// entries pointed at one WAL is the checkpoint fight §3a exists to
437// prevent.
438func TestDataDBsDuplicateIsRefused(t *testing.T) {
439 s := &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db", "console.db"}}}
440 if err := ValidCapabilities("console", s); err == nil {
441 t.Fatal("duplicate dbs entry \"console.db\" was accepted")
442 }
443}
444
445// TestTooManyDBsIsRefused pins the len(data.dbs) > 8 bound.
446func TestTooManyDBsIsRefused(t *testing.T) {
447 dbs := make([]string, 9)
448 for i := range dbs {
449 dbs[i] = strings.Repeat("a", i+1) + ".db"
450 }
451 if err := ValidCapabilities("console", &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: dbs}}); err == nil {
452 t.Fatal("9 dbs (bound is 8) was accepted")
453 }
454}
455
456// TestDataDBsWithAppReplicateIsRefused pins the two-replicators hazard: dbs
457// is meaningless when the app streams its own WAL, so it is refused rather
458// than silently ignored.
459func TestDataDBsWithAppReplicateIsRefused(t *testing.T) {
460 s := &Set{V: 1, Data: &Data{Replicate: DataReplicateApp, DBs: []string{"x.db"}}}
461 if err := ValidCapabilities("keymail", s); err == nil {
462 t.Fatal("dbs alongside replicate:\"app\" must be refused")
463 }
464}
465
466// TestDataReplicateUnknownValueIsRefused pins the closed pair of
467// replicate values.
468func TestDataReplicateUnknownValueIsRefused(t *testing.T) {
469 if err := ValidCapabilities("keymail", &Set{V: 1, Data: &Data{Replicate: "bogus"}}); err == nil {
470 t.Fatal("data.replicate \"bogus\" was accepted")
471 }
472}
473
474// TestGrantorReplicateValuesAreValidOnTheirOwn pins that replicate:"app"
475// with no dbs, and replicate:"platform" with dbs, are both accepted —
476// only replicate:"app" WITH dbs is refused.
477func TestGrantorReplicateValuesAreValidOnTheirOwn(t *testing.T) {
478 if err := ValidCapabilities("keymail", &Set{V: 1, Data: &Data{Replicate: DataReplicateApp}}); err != nil {
479 t.Fatalf("replicate:\"app\" with no dbs refused: %v", err)
480 }
481 if err := ValidCapabilities("console", &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db"}}}); err != nil {
482 t.Fatalf("replicate:\"platform\" with dbs refused: %v", err)
483 }
484}
485