From 491cdebf054025d1e404b2fa5b19559562f86f10 Mon Sep 17 00:00:00 2001 From: Asad Iqbal Date: Tue, 30 Jun 2026 19:15:22 +0500 Subject: [PATCH] fix(cli): don't misread a v2-shaped config as v1 (Bugbot/#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Load routed any config with version < 2 to migrateV1, which reads only the flat v1 fields — so a v2-shaped file with a missing/wrong version would be migrated to an empty record, silently dropping its profiles. migrateV1 now fires only for a genuine v1 record (old version AND no `profiles` object); a profiles-bearing file is always parsed as v2. Test: TestLoadV2ShapedWithoutVersion_NotMigrated. Co-Authored-By: Claude Opus 4.8 --- internal/config/config.go | 10 +++++++--- internal/config/config_test.go | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index a09f0a06..da625574 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -122,14 +122,18 @@ func Load() (*Config, error) { } // Detect the on-disk schema. v1 (cli#83) had no "version" key and a flat - // {env,email,token,active_client_id}; it decodes here as version 0 → migrate. + // {env,email,token,active_client_id}; it decodes here as version 0. Migrate + // only a GENUINE v1 record: an old version AND no v2 `profiles` object — so a + // v2-shaped file with a missing/wrong version is still parsed as v2 and never + // has its profiles silently dropped by migrateV1. var probe struct { - Version int `json:"version"` + Version int `json:"version"` + Profiles json.RawMessage `json:"profiles"` } if err := json.Unmarshal(data, &probe); err != nil { return nil, fmt.Errorf("parsing %s: %w", path, err) } - if probe.Version < schemaVersion { + if probe.Version < schemaVersion && len(probe.Profiles) == 0 { return migrateV1(data, path) } var c Config diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4381efe9..e49934ae 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -176,3 +176,26 @@ func TestSignedInRequiresCurrentEnvToken(t *testing.T) { t.Error("signed-in should be true once the current env has a token") } } + +// TestLoadV2ShapedWithoutVersion_NotMigrated pins the Bugbot fix: a config that +// already has v2 `profiles` but a missing/old version must be parsed as v2, not +// misread as v1 and migrated (which reads only flat fields → would drop profiles). +func TestLoadV2ShapedWithoutVersion_NotMigrated(t *testing.T) { + dir := t.TempDir() + t.Setenv("TRACEBLOC_CONFIG_DIR", dir) + // No "version" key, but a v2-shaped profiles object. + raw := `{"current_env":"dev","profiles":{"dev":{"token":"keep-me","active_client_id":"7"}}}` + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(raw), 0o600); err != nil { + t.Fatal(err) + } + c, err := Load() + if err != nil { + t.Fatal(err) + } + if !c.SignedIn() { + t.Fatalf("v2-shaped config without a version was misread as v1 (profiles dropped): %+v", c) + } + if p := c.Profile("dev"); p.Token != "keep-me" || p.ActiveClientID != "7" { + t.Errorf("dev profile not preserved: %+v", p) + } +}