@@ -19,6 +19,7 @@ import (
1919 "strings"
2020 "testing"
2121
22+ "github.com/enola-labs/enola/internal/config"
2223 "github.com/enola-labs/enola/pkg/bootstrap"
2324 "github.com/modelcontextprotocol/go-sdk/mcp"
2425)
@@ -46,14 +47,31 @@ type session struct {
4647// and returns a connected client session plus a fresh temp copy of go_sample.
4748func startInMemory (t * testing.T ) * session {
4849 t .Helper ()
49- ctx := context .Background ()
50+ eng , cfg := newTestEngine (t )
51+ s := connect (t , eng , cfg )
52+ s .repo = copyTree (t , filepath .Join (".." , "engine" , "testdata" , "repos" , "go_sample" ), t .TempDir ())
53+ return s
54+ }
5055
56+ // newTestEngine builds a bootstrap engine with all OSS plugins and a config that
57+ // falls back to defaults (no config file on disk).
58+ func newTestEngine (t * testing.T ) (* bootstrap.Engine , * config.Config ) {
59+ t .Helper ()
5160 eng , cfg , err := bootstrap .NewEngine (bootstrap.Options {
5261 ConfigPath : filepath .Join (t .TempDir (), "no-such-config.yaml" ),
5362 })
5463 if err != nil {
5564 t .Fatalf ("bootstrap.NewEngine: %v" , err )
5665 }
66+ return eng , cfg
67+ }
68+
69+ // connect wires the given engine into an MCP server over an in-memory transport
70+ // and returns a connected client session.
71+ func connect (t * testing.T , eng * bootstrap.Engine , cfg * config.Config ) * session {
72+ t .Helper ()
73+ ctx := context .Background ()
74+
5775 srv , err := bootstrap .NewServer (eng , cfg )
5876 if err != nil {
5977 t .Fatalf ("bootstrap.NewServer: %v" , err )
@@ -70,8 +88,7 @@ func startInMemory(t *testing.T) *session {
7088 }
7189 t .Cleanup (func () { _ = cs .Close () })
7290
73- repo := copyTree (t , filepath .Join (".." , "engine" , "testdata" , "repos" , "go_sample" ), t .TempDir ())
74- return & session {cs : cs , repo : repo }
91+ return & session {cs : cs }
7592}
7693
7794// call invokes a tool and fails the test on transport error (a transport error
@@ -269,6 +286,88 @@ func TestE2E_RequiredArgValidation(t *testing.T) {
269286 }
270287}
271288
289+ // writeSnapshotToDisk indexes repo with a throwaway engine and writes its
290+ // artifacts (including .enola/facts.jsonl) to disk, simulating a workspace that
291+ // already has a prior snapshot on disk for AutoLoadSnapshot to pick up.
292+ func writeSnapshotToDisk (t * testing.T , repo string ) {
293+ t .Helper ()
294+ eng , _ := newTestEngine (t )
295+ if _ , err := eng .GenerateSnapshot (context .Background (), repo , false ); err != nil {
296+ t .Fatalf ("prep GenerateSnapshot(%s): %v" , repo , err )
297+ }
298+ if err := eng .WriteArtifacts (repo ); err != nil {
299+ t .Fatalf ("prep WriteArtifacts(%s): %v" , repo , err )
300+ }
301+ }
302+
303+ // TestE2E_AutoLoadedSnapshotResetOnFreshGenerate is a regression test for the
304+ // bug where a snapshot auto-loaded at startup caused the first
305+ // generate_snapshot(append=false) to silently switch to append mode, carrying
306+ // the auto-loaded repo forward as a stale service node. A non-append call must
307+ // discard the auto-loaded state and index only the requested repo.
308+ func TestE2E_AutoLoadedSnapshotResetOnFreshGenerate (t * testing.T ) {
309+ // repoA: a fixture whose snapshot we pre-write to disk so AutoLoadSnapshot
310+ // picks it up at startup. ts_sample gives a distinct repo label from repoB.
311+ repoA := copyTree (t , filepath .Join (".." , "engine" , "testdata" , "repos" , "ts_sample" ), t .TempDir ())
312+ writeSnapshotToDisk (t , repoA )
313+
314+ // Build an engine pointed at repoA and auto-load its snapshot, exactly as the
315+ // server does on startup in a pre-populated workspace.
316+ eng , cfg := newTestEngine (t )
317+ cfg .Repo = repoA
318+ bootstrap .AutoLoadSnapshot (eng , cfg )
319+ if eng .Store ().Count () == 0 {
320+ t .Fatalf ("expected AutoLoadSnapshot to populate the store from %s" , repoA )
321+ }
322+ s := connect (t , eng , cfg )
323+
324+ // First generate_snapshot, for a DIFFERENT repo, with no append. It must reset.
325+ repoB := copyTree (t , filepath .Join (".." , "engine" , "testdata" , "repos" , "go_sample" ), t .TempDir ())
326+ res := s .call (t , "generate_snapshot" , map [string ]any {"repo_path" : repoB })
327+ if res .IsError {
328+ t .Fatalf ("generate_snapshot(repoB) errored: %s" , text (res ))
329+ }
330+ if out := text (res ); strings .Contains (out , "Multi-repo mode active" ) || strings .Contains (out , "auto-enabled" ) {
331+ t .Errorf ("non-append generate_snapshot over auto-loaded state must not enter append mode; got:\n %s" , out )
332+ }
333+
334+ // coverage_report must report no service nodes (single-repo) — the stale
335+ // repoA service must be gone.
336+ if cov := text (s .call (t , "coverage_report" , map [string ]any {})); ! strings .Contains (cov , "No service nodes" ) {
337+ t .Errorf ("expected no service nodes after fresh single-repo snapshot; got:\n %s" , cov )
338+ }
339+
340+ // repoA's facts must have been discarded entirely.
341+ repoALabel := filepath .Base (repoA )
342+ if q := text (s .call (t , "query_facts" , map [string ]any {"kind" : "service" })); strings .Contains (q , repoALabel ) {
343+ t .Errorf ("expected repoA (%s) to be discarded, but it still appears as a service node; got:\n %s" , repoALabel , q )
344+ }
345+ }
346+
347+ // TestE2E_MultiRepoAppendStillAccumulates guards against the session-flag gate
348+ // over-resetting: a genuine multi-repo flow (first snapshot resets, then
349+ // append=true) must still accumulate both repos as service nodes.
350+ func TestE2E_MultiRepoAppendStillAccumulates (t * testing.T ) {
351+ s := startInMemory (t )
352+ s .snapshot (t ) // go_sample, first snapshot (no append): resets, marks session
353+
354+ repoB := copyTree (t , filepath .Join (".." , "engine" , "testdata" , "repos" , "ts_sample" ), t .TempDir ())
355+ res := s .call (t , "generate_snapshot" , map [string ]any {"repo_path" : repoB , "append" : true })
356+ if res .IsError {
357+ t .Fatalf ("append generate_snapshot errored: %s" , text (res ))
358+ }
359+ if ! strings .Contains (text (res ), "Multi-repo mode active" ) {
360+ t .Errorf ("append=true should report multi-repo mode; got:\n %s" , text (res ))
361+ }
362+
363+ cov := text (s .call (t , "coverage_report" , map [string ]any {}))
364+ for _ , label := range []string {"go_sample" , "ts_sample" } {
365+ if ! strings .Contains (cov , label ) {
366+ t .Errorf ("coverage_report should list service %q after append; got:\n %s" , label , cov )
367+ }
368+ }
369+ }
370+
272371func keys (m map [string ]bool ) []string {
273372 out := make ([]string , 0 , len (m ))
274373 for k := range m {
0 commit comments