-
Notifications
You must be signed in to change notification settings - Fork 2
feat(state): port slck to cli-common state components [MON-5372] #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0978222
f627642
a31675a
424dfb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,18 +16,18 @@ import ( | |
| "github.com/open-cli-collective/cli-common/credstore" | ||
|
|
||
| "github.com/open-cli-collective/slack-chat-api/internal/client" | ||
| appconfig "github.com/open-cli-collective/slack-chat-api/internal/config" | ||
| "github.com/open-cli-collective/slack-chat-api/internal/keychain" | ||
| "github.com/open-cli-collective/slack-chat-api/internal/testutil" | ||
| ) | ||
|
|
||
| // writeLegacyCreds writes the legacy plaintext credentials file at the path | ||
| // the one-time migration scans ($XDG_CONFIG_HOME/slack-chat-api/credentials, | ||
| // per testutil.Setup's isolated XDG). | ||
| // the keychain migrator's legacyCredentialsPath() scans (the hand-rolled | ||
| // XDG/$HOME/.config path — distinct from the new canonical config dir on | ||
| // macOS/Windows post-MON-5372). | ||
| func writeLegacyCreds(t *testing.T, kv map[string]string) string { | ||
| t.Helper() | ||
| dir := filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "slack-chat-api") | ||
| require.NoError(t, os.MkdirAll(dir, 0o700)) | ||
| path := filepath.Join(dir, "credentials") | ||
| path := testutil.LegacyCredentialsPath(t) | ||
| var b strings.Builder | ||
| for k, v := range kv { | ||
| b.WriteString(k + "=" + v + "\n") | ||
|
|
@@ -185,6 +185,99 @@ func TestRunInit_OverwriteResolvesMigrationConflict(t *testing.T) { | |
| assert.True(t, os.IsNotExist(statErr), "legacy file must be removed after forced migrate") | ||
| } | ||
|
|
||
| // TestRunInit_RelocationGate_OldOnlyCopied pins the MON-5372 init-level | ||
| // contract: when only the old hand-rolled config.yml exists, runInit must | ||
| // copy it to the new canonical dir BEFORE any token migration / save runs. | ||
| // Tests the actual gate ordering through runInit (not just the unit-level | ||
| // Detect+Apply pair), so a regression that removed or reordered the gate | ||
| // would surface here. | ||
| func TestRunInit_RelocationGate_OldOnlyCopied(t *testing.T) { | ||
| testutil.Setup(t) | ||
| // On Linux old == new path; the gate is a no-op and the test simply | ||
| // confirms init still works. On macOS/Windows these diverge and the | ||
| // gate actually performs the copy. | ||
| configHome := os.Getenv("XDG_CONFIG_HOME") | ||
| if configHome == "" { | ||
| // statedirtest sets it; defensive. | ||
| t.Skip("hermetic env did not set XDG_CONFIG_HOME") | ||
| } | ||
| oldDir := filepath.Join(configHome, "slack-chat-api") | ||
| require.NoError(t, os.MkdirAll(oldDir, 0o700)) | ||
| require.NoError(t, os.WriteFile(filepath.Join(oldDir, "config.yml"), | ||
| []byte("credential_ref: slack-chat-api/from-old\nworkspace: T_OLD_PRE_INIT\n"), 0o600)) | ||
|
|
||
| // Run init with no tokens — wizard short-circuits at "no tokens provided" | ||
| // AFTER the relocation gate has run (and AFTER EnsureMigrated). The gate | ||
| // must have copied old→new by then if they differ. | ||
| err := runInit(&initOptions{stdin: strings.NewReader("\nn\n"), noVerify: true}) | ||
| require.NoError(t, err) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 Low (harness-engineering:harness-self-documenting-code-reviewer): TestRunInit_RelocationGate_OldOnlyCopied asserts the new canonical config.yml contains Reply to this thread when addressed. |
||
| // New canonical path must exist with the old content (or, on Linux, | ||
| // remain at the same path — same assertion either way). | ||
| newDir, err := appconfig.Dir() | ||
| require.NoError(t, err) | ||
| data, err := os.ReadFile(filepath.Join(newDir, "config.yml")) | ||
| require.NoError(t, err, "new canonical config.yml must exist after init") | ||
| assert.Contains(t, string(data), "slack-chat-api/from-old", | ||
| "new canonical must carry the old config content") | ||
| } | ||
|
|
||
| // TestRunInit_RelocationGate_DivergentAbortsBeforeMutation pins the MON-5372 | ||
| // fail-loud contract through runInit: divergent old/new config aborts init | ||
| // BEFORE any token migration or config write papers over the conflict. Skips | ||
| // on Linux where old == new path (the gate short-circuits to relocNone). | ||
| func TestRunInit_RelocationGate_DivergentAbortsBeforeMutation(t *testing.T) { | ||
| testutil.Setup(t) | ||
| configHome := os.Getenv("XDG_CONFIG_HOME") | ||
| if configHome == "" { | ||
| t.Skip("hermetic env did not set XDG_CONFIG_HOME") | ||
| } | ||
| // On Linux the resolved new dir IS the XDG-rooted dir — old == new, no | ||
| // way to construct divergence at the public seam. The unit-level test | ||
| // already covers this branch on every OS via the injectable newDir. | ||
| newDir, err := appconfig.Dir() | ||
| require.NoError(t, err) | ||
| oldDir := filepath.Join(configHome, "slack-chat-api") | ||
| if newDir == oldDir { | ||
| t.Skip("Linux: old path equals new path; divergence covered at unit level") | ||
| } | ||
|
|
||
| require.NoError(t, os.MkdirAll(oldDir, 0o700)) | ||
| require.NoError(t, os.MkdirAll(newDir, 0o700)) | ||
| require.NoError(t, os.WriteFile(filepath.Join(oldDir, "config.yml"), | ||
| []byte("credential_ref: slack-chat-api/old\n"), 0o600)) | ||
| require.NoError(t, os.WriteFile(filepath.Join(newDir, "config.yml"), | ||
| []byte("credential_ref: slack-chat-api/new\n"), 0o600)) | ||
|
|
||
| // Seed a legacy credentials file too. If the relocation gate were | ||
| // accidentally moved AFTER keychain.Open, the migrator would consume | ||
| // this file before the abort and the post-abort stat would fail. With | ||
| // the gate ordered correctly, the legacy file stays untouched. | ||
| legacy := writeLegacyCreds(t, map[string]string{"api_token": "xoxb-MUST-NOT-BE-MIGRATED"}) | ||
|
|
||
| // Snapshot pre-init bytes to assert mutate-nothing. | ||
| oldBefore, _ := os.ReadFile(filepath.Join(oldDir, "config.yml")) | ||
| newBefore, _ := os.ReadFile(filepath.Join(newDir, "config.yml")) | ||
|
|
||
| err = runInit(&initOptions{botEnv: "BOT_TOK", noVerify: true}) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "detecting config relocation", | ||
| "init must abort at the relocation gate") | ||
|
|
||
| // Nothing mutated by the abort. | ||
| oldAfter, _ := os.ReadFile(filepath.Join(oldDir, "config.yml")) | ||
| newAfter, _ := os.ReadFile(filepath.Join(newDir, "config.yml")) | ||
| assert.Equal(t, string(oldBefore), string(oldAfter), "old config must not be mutated by aborted init") | ||
| assert.Equal(t, string(newBefore), string(newAfter), "new config must not be mutated by aborted init") | ||
|
|
||
| // Critical proof the gate ran BEFORE keychain.Open / migration: the | ||
| // legacy credentials file must still exist. If migration had run, it | ||
| // would have consumed and removed this file. | ||
| if _, statErr := os.Stat(legacy); statErr != nil { | ||
| t.Errorf("legacy credentials file must still exist (gate must abort before migration); stat err=%v", statErr) | ||
| } | ||
| } | ||
|
|
||
| func TestRunInit_VerificationFailed(t *testing.T) { | ||
| testutil.Setup(t) | ||
| s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Low (harness-engineering:harness-architecture-reviewer): OldConfigPath() errors are silently swallowed: if os.UserHomeDir() fails, the old config.yml is quietly skipped. After --all, the runtime old-only fallback in Load would then resurrect the stale config — exactly the scenario this code exists to prevent. At minimum surface a warning so the user knows --all was incomplete; ideally propagate the error.
Reply to this thread when addressed.