Files
miti99bot/internal/migration/policy_test.go
T
tiennm99 5117222e5e feat(migration): cf→aws migration toolchain + DynamoDB value as String
Operator-run tooling that moves durable Cloudflare KV data into the live
AWS DynamoDB table, plus a runtime swap of the `value` attribute from
Binary to String so payloads are human-readable in the AWS console.

- cmd/migrate_cf_data: subcommands inventory, kv-import (idempotent via
  attribute_not_exists), trading-audit-dump, convert-value-to-string
- internal/migration: policy allowlist, CF KV+D1 REST clients, DynamoDB
  writer, report formatter with per-prefix counts + tests
- internal/storage/dynamodb_kv.go: Put writes MemberS, Get reads MemberS;
  dropped empty-bytes sentinel (DynamoDB allows empty strings)
2026-05-16 10:33:35 +07:00

87 lines
2.7 KiB
Go

package migration
import "testing"
func TestClassify(t *testing.T) {
cases := []struct {
key string
wantAct Action
wantPK string
wantSK string
wantSkip string
}{
// Durable migrate paths — these are the 6 prefixes Phase 01 locked.
{"wordle:stats:-1001760292100", ActionMigrate, "wordle", "stats:-1001760292100", ""},
{"loldle:stats:1064111334", ActionMigrate, "loldle", "stats:1064111334", ""},
{"loldle:config:-1001760292100", ActionMigrate, "loldle", "config:-1001760292100", ""},
{"twentyq:stats:-1001760292100", ActionMigrate, "twentyq", "stats:-1001760292100", ""},
{"lolschedule:subscribers", ActionMigrate, "lolschedule", "subscribers", ""},
{"trading:user:1064111334", ActionMigrate, "trading", "user:1064111334", ""},
// Cache + ephemeral — skip.
{"trading:sym:FPT", ActionSkip, "", "", "cache"},
{"wordle:game:abc", ActionSkip, "", "", "ephemeral"},
{"lolschedule:matches:2026", ActionSkip, "", "", "cache"},
// Retired modules — skip.
{"doantu:stats:1064111334", ActionSkip, "", "", "retired"},
{"semantle:stats:-1001760292100", ActionSkip, "", "", "retired"},
{"loldle-emoji:stats:-1001760292100", ActionSkip, "", "", "retired"},
// Unknown prefix — skip with reason "unknown" so it surfaces in reports.
{"newmodule:foo", ActionSkip, "", "", "unknown"},
}
for _, c := range cases {
t.Run(c.key, func(t *testing.T) {
got := Classify(c.key)
if got.Action != c.wantAct {
t.Fatalf("action=%v want %v", got.Action, c.wantAct)
}
if got.PK != c.wantPK {
t.Errorf("pk=%q want %q", got.PK, c.wantPK)
}
if got.SK != c.wantSK {
t.Errorf("sk=%q want %q", got.SK, c.wantSK)
}
if got.Reason != c.wantSkip {
t.Errorf("reason=%q want %q", got.Reason, c.wantSkip)
}
})
}
}
func TestDurablePrefixes(t *testing.T) {
got := DurablePrefixes()
want := []string{
"wordle:stats:",
"loldle:stats:",
"loldle:config:",
"twentyq:stats:",
"lolschedule:subscribers",
"trading:user:",
}
if len(got) != len(want) {
t.Fatalf("got %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("idx %d: got %q want %q", i, got[i], want[i])
}
}
}
func TestPrefixOfLongestMatch(t *testing.T) {
// loldle:stats: and loldle:config: both share the loldle: super-prefix.
// Longest match wins so report buckets stay precise.
if got := PrefixOf("loldle:stats:1234"); got != "loldle:stats:" {
t.Errorf("loldle:stats:1234 → %q, want loldle:stats:", got)
}
if got := PrefixOf("loldle:config:5"); got != "loldle:config:" {
t.Errorf("loldle:config:5 → %q, want loldle:config:", got)
}
if got := PrefixOf("zzz:unknown"); got != "unknown" {
t.Errorf("unknown bucket: got %q", got)
}
}