fix(core): derive workspace session store from the engine store path (#1926)

getOrCreateWorkspaceAgent built the per-workspace session file as
filepath.Join(filepath.Dir(e.sessions.StorePath()), ...). With no store
path configured — which is how every engine in the test suite is built —
filepath.Dir("") is ".", so the path collapsed to a bare relative
"<engine>_ws_<hash>.json" and the session manager wrote it into the
process working directory. That is what littered core/ with test_ws_*.json
during `go test ./core/`.

An empty store path already means "no persistence" everywhere else in the
session manager (saveLocked returns early), so it now means the same here:
the workspace manager is built unpersisted instead of inventing a path.
A configured store path still derives the workspace file beside it, as
before.

Regressions: an engine without a store path yields a workspace manager
whose StorePath() is empty, and a configured one still derives
test_ws_<hash>.json in the store's directory.

Also ignores the stale files contributors already have on disk.

Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
Niko WuandClaude Code authored and GitHub committed 2026-09-29 10:40:53 +08:00
1 parent f8acb2c6c6
commit 1d38af3fe3
3 files changed
+83 -3

No files matched your search

+7
View File
@@ -82,6 +82,13 @@ temp/
*.pid
*.log
# Per-workspace session stores (<engine>_ws_<hash>.json, see
# Engine.getOrCreateWorkspaceAgent). They are derived from the engine's own
# session store, so they belong next to it — never in the working tree. This
# is a guard for files already on disk from before that derivation was
# corrected; the derivation itself no longer produces them.
*_ws_????????.json
# ============================================================================
# IDE & Editor
# ============================================================================
+10 -3
View File
@@ -4180,10 +4180,17 @@ func (e *Engine) getOrCreateWorkspaceAgent(workspace string) (Agent, *SessionMan
}
}
// Create per-workspace session manager
// Create per-workspace session manager. An empty store path means "no
// persistence" everywhere else in the session manager, so it has to mean
// the same here: filepath.Dir("") is ".", which would otherwise drop
// "<engine>_ws_<hash>.json" into the process working directory — that is
// what littered core/ with test_ws_*.json during `go test ./core/`.
h := sha256.Sum256([]byte(workspace))
sessionFile := filepath.Join(filepath.Dir(e.sessions.StorePath()),
fmt.Sprintf("%s_ws_%s.json", e.name, hex.EncodeToString(h[:4])))
sessionFile := ""
if storePath := e.sessions.StorePath(); storePath != "" {
sessionFile = filepath.Join(filepath.Dir(storePath),
fmt.Sprintf("%s_ws_%s.json", e.name, hex.EncodeToString(h[:4])))
}
sessions := NewSessionManager(sessionFile)
sessions.InvalidateForAgent(agent.Name())
+66
View File
@@ -0,0 +1,66 @@
package core
import (
"path/filepath"
"strings"
"testing"
)
// registerWorkspaceStoreAgent registers a throwaway agent type so
// getOrCreateWorkspaceAgent can build a per-workspace instance.
func registerWorkspaceStoreAgent(t *testing.T, agentName string) {
t.Helper()
RegisterAgent(agentName, func(opts map[string]any) (Agent, error) {
workDir, _ := opts["work_dir"].(string)
return &sendWorkDirAgent{
name: agentName,
workDir: workDir,
session: newResultAgentSession("workspace session"),
}, nil
})
}
// A workspace session manager has to inherit the engine's "no store path means
// no persistence" contract. filepath.Dir("") is ".", so deriving a path from an
// empty store path used to drop "<engine>_ws_<hash>.json" into the process
// working directory — that is how `go test ./core/` littered core/ with
// test_ws_*.json files.
func TestGetOrCreateWorkspaceAgent_EmptyStorePathStaysUnpersisted(t *testing.T) {
agentName := "test-ws-store-unpersisted"
registerWorkspaceStoreAgent(t, agentName)
p := &stubPlatformEngine{n: "telegram"}
e := NewEngine("test", &sendWorkDirAgent{name: agentName}, []Platform{p}, "", LangEnglish)
_, sessions, err := e.getOrCreateWorkspaceAgent(t.TempDir())
if err != nil {
t.Fatalf("getOrCreateWorkspaceAgent() error = %v", err)
}
if got := sessions.StorePath(); got != "" {
t.Fatalf("workspace store path = %q, want empty when the engine has no store path", got)
}
}
// The derived path keeps living beside the engine's own store, so a configured
// deployment still persists workspace sessions.
func TestGetOrCreateWorkspaceAgent_DerivesStorePathBesideEngineStore(t *testing.T) {
agentName := "test-ws-store-derived"
registerWorkspaceStoreAgent(t, agentName)
dir := t.TempDir()
p := &stubPlatformEngine{n: "telegram"}
e := NewEngine("test", &sendWorkDirAgent{name: agentName}, []Platform{p},
filepath.Join(dir, "sessions.json"), LangEnglish)
_, sessions, err := e.getOrCreateWorkspaceAgent(t.TempDir())
if err != nil {
t.Fatalf("getOrCreateWorkspaceAgent() error = %v", err)
}
got := sessions.StorePath()
if filepath.Dir(got) != dir {
t.Fatalf("workspace store dir = %q, want %q", filepath.Dir(got), dir)
}
if base := filepath.Base(got); !strings.HasPrefix(base, "test_ws_") || !strings.HasSuffix(base, ".json") {
t.Fatalf("workspace store name = %q, want test_ws_<hash>.json", base)
}
}