diff --git a/pkg/behaviourtest/steps/branch_test.go b/pkg/behaviourtest/steps/branch_test.go index 7498fc5b6a..6be3146c7f 100644 --- a/pkg/behaviourtest/steps/branch_test.go +++ b/pkg/behaviourtest/steps/branch_test.go @@ -296,6 +296,8 @@ func TestThenBranchUnchanged_NoRepo(t *testing.T) { func TestParseDummyAgentTable_ExpandsIssueInCheckoutBranchOnly(t *testing.T) { w := &world.World{ + Org: "org", + RepoName: "repo", SCM: &fakeCleanupSCM{}, Install: &fakeInstallState{}, FixturesRoot: "e2e/behaviour", diff --git a/pkg/behaviourtest/steps/cleanup.go b/pkg/behaviourtest/steps/cleanup.go index 6cdb2788c2..686aa26bc9 100644 --- a/pkg/behaviourtest/steps/cleanup.go +++ b/pkg/behaviourtest/steps/cleanup.go @@ -143,9 +143,13 @@ func CleanupScenario(w *world.World) { // --- Dummy script cleanup --- if len(w.DummyOps) > 0 { - empty := []byte("ops: []\n") - if err := w.SCM.CommitFile(ctx, w.Install.ConfigOwner(), w.Install.ConfigRepo(), w.BehaviourScriptPath(), "behaviour: clear dummy agent script", empty); err != nil { - worldLogf(w, "behaviour cleanup: clear dummy script: %v", err) + if w.Org == "" || w.RepoName == "" { + worldLogf(w, "behaviour cleanup: clear dummy script: no repo configured; call 'Given the enrolled test repository' first") + } else { + empty := []byte("ops: []\n") + if err := w.SCM.CommitFile(ctx, w.Org, w.RepoName, w.BehaviourScriptPath(), "behaviour: clear dummy agent script", empty); err != nil { + worldLogf(w, "behaviour cleanup: clear dummy script: %v", err) + } } } } diff --git a/pkg/behaviourtest/steps/cleanup_test.go b/pkg/behaviourtest/steps/cleanup_test.go index 287c75018c..0264f13c00 100644 --- a/pkg/behaviourtest/steps/cleanup_test.go +++ b/pkg/behaviourtest/steps/cleanup_test.go @@ -614,12 +614,11 @@ func TestCleanupScenario_ClearsDummyOps(t *testing.T) { t.Parallel() scmDriver := &fakeCleanupSCM{} - installDriver := &fakeCleanupInstall{owner: "org", repo: "repo"} w := &world.World{ + Org: "org", RepoOwner: "org", RepoName: "repo", DummyOps: []runtime.BehaviourOperation{{Op: "echo", Args: "hello"}}, - Install: installDriver, SCM: scmDriver, } CleanupScenario(w) @@ -631,12 +630,11 @@ func TestCleanupScenario_ClearsDummyOps_Error(t *testing.T) { var logged []string scmDriver := &fakeCleanupSCM{commitFileErr: fmt.Errorf("commit failed")} - installDriver := &fakeCleanupInstall{owner: "org", repo: "repo"} w := &world.World{ + Org: "org", RepoOwner: "org", RepoName: "repo", DummyOps: []runtime.BehaviourOperation{{Op: "echo", Args: "hello"}}, - Install: installDriver, SCM: scmDriver, Logf: func(format string, args ...any) { logged = append(logged, fmt.Sprintf(format, args...)) }, } @@ -645,6 +643,27 @@ func TestCleanupScenario_ClearsDummyOps_Error(t *testing.T) { assert.Contains(t, logged[0], "clear dummy script") } +// --- Empty identity guard tests --- + +func TestCleanupScenario_ClearsDummyOps_EmptyIdentity(t *testing.T) { + t.Parallel() + + var logged []string + scmDriver := &fakeCleanupSCM{} + w := &world.World{ + RepoOwner: "org", + RepoName: "repo", + // Org deliberately not set — should log instead of calling SCM. + DummyOps: []runtime.BehaviourOperation{{Op: "echo", Args: "hello"}}, + SCM: scmDriver, + Logf: func(format string, args ...any) { logged = append(logged, fmt.Sprintf(format, args...)) }, + } + CleanupScenario(w) + assert.False(t, scmDriver.commitFileCalled, "should not call CommitFile with empty Org") + require.Len(t, logged, 1) + assert.Contains(t, logged[0], "no repo configured") +} + // --- Kill switch cleanup tests --- func TestCleanupScenario_DeactivatesKillSwitch(t *testing.T) { @@ -653,12 +672,11 @@ func TestCleanupScenario_DeactivatesKillSwitch(t *testing.T) { scmDriver := &fakeCleanupSCM{ fileContent: []byte("version: \"1\"\nkill_switch: true\nroles:\n - triage\n"), } - installDriver := &fakeCleanupInstall{owner: "org", repo: "repo"} w := &world.World{ + Org: "org", RepoOwner: "org", RepoName: "repo", KillSwitchActivated: true, - Install: installDriver, SCM: scmDriver, } CleanupScenario(w) @@ -687,12 +705,11 @@ func TestCleanupScenario_DeactivateKillSwitch_Error(t *testing.T) { fileContent: []byte("version: \"1\"\nkill_switch: true\nroles:\n - triage\n"), commitFileErr: fmt.Errorf("commit failed"), } - installDriver := &fakeCleanupInstall{owner: "org", repo: "repo"} w := &world.World{ + Org: "org", RepoOwner: "org", RepoName: "repo", KillSwitchActivated: true, - Install: installDriver, SCM: scmDriver, Logf: func(format string, args ...any) { logged = append(logged, fmt.Sprintf(format, args...)) }, } @@ -701,21 +718,6 @@ func TestCleanupScenario_DeactivateKillSwitch_Error(t *testing.T) { assert.Contains(t, logged[0], "deactivate kill switch") } -// fakeCleanupInstall satisfies the Install interface for cleanup tests. -type fakeCleanupInstall struct { - owner string - repo string -} - -func (f *fakeCleanupInstall) Mode() string { return "per-repo" } -func (f *fakeCleanupInstall) ConfigOwner() string { return f.owner } -func (f *fakeCleanupInstall) ConfigRepo() string { return f.repo } -func (f *fakeCleanupInstall) ConfigPathPrefix() string { return ".fullsend" } -func (f *fakeCleanupInstall) TriageWorkflowRepo() string { return f.repo } -func (f *fakeCleanupInstall) TriageWorkflowFile() string { return "fullsend.yaml" } -func (f *fakeCleanupInstall) AgentWorkflowFile() string { return "reusable-triage.yml" } -func (f *fakeCleanupInstall) AgentArtifactName() string { return "fullsend-triage" } - func TestCleanupScenario_BranchScenarioSweep(t *testing.T) { t.Parallel() diff --git a/pkg/behaviourtest/steps/dispatch.go b/pkg/behaviourtest/steps/dispatch.go index 4af3ac8308..dae7dfc3cf 100644 --- a/pkg/behaviourtest/steps/dispatch.go +++ b/pkg/behaviourtest/steps/dispatch.go @@ -47,8 +47,11 @@ func registerDispatchSteps(sc *godog.ScenarioContext) { // It also marks w.KillSwitchActivated so CleanupScenario deactivates the // switch before the slot is reused by another scenario. func givenKillSwitchActive(w *world.World) error { + if w.Org == "" || w.RepoName == "" { + return fmt.Errorf("no repo configured; call 'Given the enrolled test repository' before kill-switch operations") + } cfgPath := filepath.Join(".fullsend", "config.yaml") - cfgData, err := w.SCM.GetFileContent(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath) + cfgData, err := w.SCM.GetFileContent(context.Background(), w.Org, w.RepoName, cfgPath) if err != nil { return fmt.Errorf("reading config: %w", err) } @@ -61,7 +64,7 @@ func givenKillSwitchActive(w *world.World) error { if err != nil { return err } - if err := w.SCM.CommitFile(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath, "behaviour: activate kill switch", merged); err != nil { + if err := w.SCM.CommitFile(context.Background(), w.Org, w.RepoName, cfgPath, "behaviour: activate kill switch", merged); err != nil { return fmt.Errorf("updating config: %w", err) } w.KillSwitchActivated = true @@ -72,8 +75,11 @@ func givenKillSwitchActive(w *world.World) error { // config.yaml. Exported so CleanupScenario (in package steps) can call // it during scenario teardown. func DeactivateKillSwitch(w *world.World) error { + if w.Org == "" || w.RepoName == "" { + return fmt.Errorf("no repo configured; call 'Given the enrolled test repository' before kill-switch operations") + } cfgPath := filepath.Join(".fullsend", "config.yaml") - cfgData, err := w.SCM.GetFileContent(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath) + cfgData, err := w.SCM.GetFileContent(context.Background(), w.Org, w.RepoName, cfgPath) if err != nil { return fmt.Errorf("reading config: %w", err) } @@ -86,13 +92,16 @@ func DeactivateKillSwitch(w *world.World) error { if err != nil { return err } - if err := w.SCM.CommitFile(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath, "behaviour: deactivate kill switch", merged); err != nil { + if err := w.SCM.CommitFile(context.Background(), w.Org, w.RepoName, cfgPath, "behaviour: deactivate kill switch", merged); err != nil { return fmt.Errorf("updating config: %w", err) } return nil } func givenDisabledCustomHarness(w *world.World, name, doc string) error { + if w.Org == "" || w.RepoName == "" { + return fmt.Errorf("no repo configured; call 'Given the enrolled test repository' before harness operations") + } name = strings.TrimSpace(name) doc = strings.TrimSpace(doc) if name == "" || doc == "" { @@ -100,7 +109,7 @@ func givenDisabledCustomHarness(w *world.World, name, doc string) error { } harnessPath := filepath.Join(".fullsend", "harness", name+".yaml") - if err := w.SCM.CommitFile(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), harnessPath, fmt.Sprintf("behaviour: add harness %s", name), []byte(doc)); err != nil { + if err := w.SCM.CommitFile(context.Background(), w.Org, w.RepoName, harnessPath, fmt.Sprintf("behaviour: add harness %s", name), []byte(doc)); err != nil { return fmt.Errorf("committing harness: %w", err) } @@ -109,7 +118,7 @@ func givenDisabledCustomHarness(w *world.World, name, doc string) error { } cfgPath := filepath.Join(".fullsend", "config.yaml") - cfgData, err := w.SCM.GetFileContent(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath) + cfgData, err := w.SCM.GetFileContent(context.Background(), w.Org, w.RepoName, cfgPath) if err != nil { return fmt.Errorf("reading config: %w", err) } @@ -136,13 +145,16 @@ func givenDisabledCustomHarness(w *world.World, name, doc string) error { if err != nil { return err } - if err := w.SCM.CommitFile(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath, fmt.Sprintf("behaviour: register disabled harness %s", name), merged); err != nil { + if err := w.SCM.CommitFile(context.Background(), w.Org, w.RepoName, cfgPath, fmt.Sprintf("behaviour: register disabled harness %s", name), merged); err != nil { return fmt.Errorf("updating config: %w", err) } return nil } func givenCustomHarness(w *world.World, name, doc string) error { + if w.Org == "" || w.RepoName == "" { + return fmt.Errorf("no repo configured; call 'Given the enrolled test repository' before harness operations") + } name = strings.TrimSpace(name) doc = strings.TrimSpace(doc) if name == "" || doc == "" { @@ -151,7 +163,7 @@ func givenCustomHarness(w *world.World, name, doc string) error { w.DispatchAgent = name harnessPath := filepath.Join(".fullsend", "harness", name+".yaml") - if err := w.SCM.CommitFile(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), harnessPath, fmt.Sprintf("behaviour: add harness %s", name), []byte(doc)); err != nil { + if err := w.SCM.CommitFile(context.Background(), w.Org, w.RepoName, harnessPath, fmt.Sprintf("behaviour: add harness %s", name), []byte(doc)); err != nil { return fmt.Errorf("committing harness: %w", err) } @@ -160,7 +172,7 @@ func givenCustomHarness(w *world.World, name, doc string) error { } cfgPath := filepath.Join(".fullsend", "config.yaml") - cfgData, err := w.SCM.GetFileContent(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath) + cfgData, err := w.SCM.GetFileContent(context.Background(), w.Org, w.RepoName, cfgPath) if err != nil { return fmt.Errorf("reading config: %w", err) } @@ -186,7 +198,7 @@ func givenCustomHarness(w *world.World, name, doc string) error { if err != nil { return err } - if err := w.SCM.CommitFile(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath, fmt.Sprintf("behaviour: register harness %s", name), merged); err != nil { + if err := w.SCM.CommitFile(context.Background(), w.Org, w.RepoName, cfgPath, fmt.Sprintf("behaviour: register harness %s", name), merged); err != nil { return fmt.Errorf("updating config: %w", err) } return nil @@ -201,6 +213,9 @@ func givenCustomHarness(w *world.World, name, doc string) error { // to the config repo with the .fullsend/ prefix instead of to a hosting // repo at the repo root. func commitLocalHarnessResources(ctx context.Context, w *world.World, harnessName, doc string) error { + if w.Org == "" || w.RepoName == "" { + return fmt.Errorf("no repo configured; call 'Given the enrolled test repository' before harness operations") + } var h struct { Agent string `yaml:"agent"` Policy string `yaml:"policy"` @@ -209,8 +224,8 @@ func commitLocalHarnessResources(ctx context.Context, w *world.World, harnessNam return fmt.Errorf("parsing harness YAML for resource paths: %w", err) } - owner := w.Install.ConfigOwner() - repo := w.Install.ConfigRepo() + owner := w.Org + repo := w.RepoName if h.Agent != "" && !strings.HasPrefix(h.Agent, "/") && !strings.HasPrefix(h.Agent, "https://") { agentPath := filepath.Join(".fullsend", h.Agent) diff --git a/pkg/behaviourtest/steps/dispatch_test.go b/pkg/behaviourtest/steps/dispatch_test.go index cd187dd826..bb12f28cff 100644 --- a/pkg/behaviourtest/steps/dispatch_test.go +++ b/pkg/behaviourtest/steps/dispatch_test.go @@ -14,7 +14,7 @@ import ( ) func TestGivenCustomHarness_Validation(t *testing.T) { - w := &world.World{} + w := &world.World{Org: "org", RepoName: "repo"} require.Error(t, givenCustomHarness(w, "", "doc")) require.Error(t, givenCustomHarness(w, "agent", "")) } @@ -45,8 +45,9 @@ func TestGivenKillSwitchActive_SetsKillSwitch(t *testing.T) { fileContent: []byte("version: \"1\"\nroles:\n - triage\n"), } w := &world.World{ - SCM: scm, - Install: &fakeDispatchInstall{owner: "org", repo: "repo"}, + SCM: scm, + Org: "org", + RepoName: "repo", } err := givenKillSwitchActive(w) require.NoError(t, err) @@ -60,8 +61,9 @@ func TestDeactivateKillSwitch_ClearsKillSwitch(t *testing.T) { fileContent: []byte("version: \"1\"\nkill_switch: true\nroles:\n - triage\n"), } w := &world.World{ - SCM: scm, - Install: &fakeDispatchInstall{owner: "org", repo: "repo"}, + SCM: scm, + Org: "org", + RepoName: "repo", } err := DeactivateKillSwitch(w) require.NoError(t, err) @@ -74,8 +76,9 @@ func TestDeactivateKillSwitch_GetFileContentError(t *testing.T) { getFileErr: fmt.Errorf("not found"), } w := &world.World{ - SCM: scm, - Install: &fakeDispatchInstall{owner: "org", repo: "repo"}, + SCM: scm, + Org: "org", + RepoName: "repo", } err := DeactivateKillSwitch(w) require.Error(t, err) @@ -88,8 +91,9 @@ func TestDeactivateKillSwitch_CommitFileError(t *testing.T) { commitErr: fmt.Errorf("commit failed"), } w := &world.World{ - SCM: scm, - Install: &fakeDispatchInstall{owner: "org", repo: "repo"}, + SCM: scm, + Org: "org", + RepoName: "repo", } err := DeactivateKillSwitch(w) require.Error(t, err) @@ -101,8 +105,9 @@ func TestGivenKillSwitchActive_GetFileContentError(t *testing.T) { getFileErr: fmt.Errorf("not found"), } w := &world.World{ - SCM: scm, - Install: &fakeDispatchInstall{owner: "org", repo: "repo"}, + SCM: scm, + Org: "org", + RepoName: "repo", } err := givenKillSwitchActive(w) require.Error(t, err) @@ -115,28 +120,69 @@ func TestGivenKillSwitchActive_CommitFileError(t *testing.T) { commitErr: fmt.Errorf("commit failed"), } w := &world.World{ - SCM: scm, - Install: &fakeDispatchInstall{owner: "org", repo: "repo"}, + SCM: scm, + Org: "org", + RepoName: "repo", } err := givenKillSwitchActive(w) require.Error(t, err) assert.Contains(t, err.Error(), "updating config") } -// fakeDispatchInstall implements install.State for dispatch step tests. -type fakeDispatchInstall struct { - owner string - repo string +// --- empty identity guard tests --- + +func TestGivenKillSwitchActive_EmptyIdentity(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + org string + repo string + }{ + {"empty org", "", "repo"}, + {"empty repo", "org", ""}, + {"both empty", "", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + w := &world.World{Org: tc.org, RepoName: tc.repo, SCM: &fakeDispatchSCM{}} + err := givenKillSwitchActive(w) + require.Error(t, err) + assert.Contains(t, err.Error(), "no repo configured") + }) + } +} + +func TestDeactivateKillSwitch_EmptyIdentity(t *testing.T) { + t.Parallel() + w := &world.World{SCM: &fakeDispatchSCM{}} + err := DeactivateKillSwitch(w) + require.Error(t, err) + assert.Contains(t, err.Error(), "no repo configured") +} + +func TestGivenCustomHarness_EmptyIdentity(t *testing.T) { + t.Parallel() + w := &world.World{SCM: &fakeDispatchSCM{}} + err := givenCustomHarness(w, "test", "role: triage") + require.Error(t, err) + assert.Contains(t, err.Error(), "no repo configured") } -func (f *fakeDispatchInstall) Mode() string { return "per-repo" } -func (f *fakeDispatchInstall) ConfigOwner() string { return f.owner } -func (f *fakeDispatchInstall) ConfigRepo() string { return f.repo } -func (f *fakeDispatchInstall) ConfigPathPrefix() string { return ".fullsend" } -func (f *fakeDispatchInstall) TriageWorkflowRepo() string { return f.repo } -func (f *fakeDispatchInstall) TriageWorkflowFile() string { return "" } -func (f *fakeDispatchInstall) AgentWorkflowFile() string { return "" } -func (f *fakeDispatchInstall) AgentArtifactName() string { return "" } +func TestGivenDisabledCustomHarness_EmptyIdentity(t *testing.T) { + t.Parallel() + w := &world.World{SCM: &fakeDispatchSCM{}} + err := givenDisabledCustomHarness(w, "test", "role: triage") + require.Error(t, err) + assert.Contains(t, err.Error(), "no repo configured") +} + +func TestCommitLocalHarnessResources_EmptyIdentity(t *testing.T) { + t.Parallel() + w := &world.World{SCM: &fakeDispatchSCM{}} + err := commitLocalHarnessResources(context.Background(), w, "test", "role: triage") + require.Error(t, err) + assert.Contains(t, err.Error(), "no repo configured") +} // fakeDispatchSCM implements scm.Driver for dispatch step tests. type fakeDispatchSCM struct { @@ -280,8 +326,9 @@ func TestGivenCustomHarness_CommitsAgentFile(t *testing.T) { "test-org/test-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, - SCM: scm, + Org: "test-org", + RepoName: "test-repo", + SCM: scm, } err := givenCustomHarness(w, "local-test", "agent: agents/triage.md\nrole: triage\nslug: local-test") require.NoError(t, err) @@ -301,8 +348,9 @@ func TestGivenCustomHarness_CommitsAgentAndPolicy(t *testing.T) { "test-org/test-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, - SCM: scm, + Org: "test-org", + RepoName: "test-repo", + SCM: scm, } err := givenCustomHarness(w, "local-test", "agent: agents/test.md\npolicy: policies/test.md\nrole: triage\nslug: local-test") require.NoError(t, err) @@ -321,8 +369,9 @@ func TestGivenCustomHarness_SkipsAbsoluteAgentPath(t *testing.T) { "test-org/test-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, - SCM: scm, + Org: "test-org", + RepoName: "test-repo", + SCM: scm, } err := givenCustomHarness(w, "local-test", "agent: https://example.com/agent.md\nrole: triage\nslug: local-test") require.NoError(t, err) @@ -338,8 +387,9 @@ func TestGivenDisabledCustomHarness_CommitsAgentFile(t *testing.T) { "test-org/test-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, - SCM: scm, + Org: "test-org", + RepoName: "test-repo", + SCM: scm, } err := givenDisabledCustomHarness(w, "disabled-test", "agent: agents/triage.md\nrole: triage\nslug: disabled-test") require.NoError(t, err) @@ -352,8 +402,9 @@ func TestGivenDisabledCustomHarness_CommitsAgentFile(t *testing.T) { func TestCommitLocalHarnessResources_CommitsAgentFile(t *testing.T) { scm := &fakeURLSCM{files: map[string][]byte{}} w := &world.World{ - Install: &fakeURLInstall{owner: "org", repo: "repo"}, - SCM: scm, + Org: "org", + RepoName: "repo", + SCM: scm, } err := commitLocalHarnessResources(context.Background(), w, "test", "agent: agents/triage.md\nrole: triage") @@ -364,8 +415,9 @@ func TestCommitLocalHarnessResources_CommitsAgentFile(t *testing.T) { func TestCommitLocalHarnessResources_SkipsURLAgentPath(t *testing.T) { scm := &fakeURLSCM{files: map[string][]byte{}} w := &world.World{ - Install: &fakeURLInstall{owner: "org", repo: "repo"}, - SCM: scm, + Org: "org", + RepoName: "repo", + SCM: scm, } err := commitLocalHarnessResources(context.Background(), w, "test", "agent: https://example.com/agents/triage.md\nrole: triage") @@ -376,8 +428,9 @@ func TestCommitLocalHarnessResources_SkipsURLAgentPath(t *testing.T) { func TestCommitLocalHarnessResources_SkipsAbsoluteAgentPath(t *testing.T) { scm := &fakeURLSCM{files: map[string][]byte{}} w := &world.World{ - Install: &fakeURLInstall{owner: "org", repo: "repo"}, - SCM: scm, + Org: "org", + RepoName: "repo", + SCM: scm, } err := commitLocalHarnessResources(context.Background(), w, "test", "agent: /absolute/agents/triage.md\nrole: triage") @@ -388,8 +441,9 @@ func TestCommitLocalHarnessResources_SkipsAbsoluteAgentPath(t *testing.T) { func TestCommitLocalHarnessResources_NoAgentField(t *testing.T) { scm := &fakeURLSCM{files: map[string][]byte{}} w := &world.World{ - Install: &fakeURLInstall{owner: "org", repo: "repo"}, - SCM: scm, + Org: "org", + RepoName: "repo", + SCM: scm, } err := commitLocalHarnessResources(context.Background(), w, "test", "role: triage\nslug: test") @@ -400,8 +454,9 @@ func TestCommitLocalHarnessResources_NoAgentField(t *testing.T) { func TestCommitLocalHarnessResources_InvalidYAML(t *testing.T) { scm := &fakeURLSCM{files: map[string][]byte{}} w := &world.World{ - Install: &fakeURLInstall{owner: "org", repo: "repo"}, - SCM: scm, + Org: "org", + RepoName: "repo", + SCM: scm, } err := commitLocalHarnessResources(context.Background(), w, "test", "invalid: [yaml: content") diff --git a/pkg/behaviourtest/steps/dummy_agent.go b/pkg/behaviourtest/steps/dummy_agent.go index 40307fa9f5..6460781a55 100644 --- a/pkg/behaviourtest/steps/dummy_agent.go +++ b/pkg/behaviourtest/steps/dummy_agent.go @@ -33,6 +33,9 @@ func registerDummyAgentSteps(sc *godog.ScenarioContext) { } func parseDummyAgentTable(w *world.World, table *godog.Table) error { + if w.Org == "" || w.RepoName == "" { + return fmt.Errorf("no repo configured; call 'Given the enrolled test repository' before dummy-agent operations") + } if len(table.Rows) < 2 { return fmt.Errorf("dummy agent table requires a header and at least one row") } @@ -97,7 +100,7 @@ func parseDummyAgentTable(w *world.World, table *godog.Table) error { } message := fmt.Sprintf("behaviour: set dummy agent script (%s)", time.Now().UTC().Format(time.RFC3339)) - if err := w.SCM.CommitFile(context.Background(), w.Install.ConfigOwner(), w.Install.ConfigRepo(), w.BehaviourScriptPath(), message, data); err != nil { + if err := w.SCM.CommitFile(context.Background(), w.Org, w.RepoName, w.BehaviourScriptPath(), message, data); err != nil { return fmt.Errorf("committing behaviour script: %w", err) } diff --git a/pkg/behaviourtest/steps/dummy_agent_test.go b/pkg/behaviourtest/steps/dummy_agent_test.go index bcd1cadeaf..782d571efc 100644 --- a/pkg/behaviourtest/steps/dummy_agent_test.go +++ b/pkg/behaviourtest/steps/dummy_agent_test.go @@ -12,7 +12,8 @@ import ( "github.com/fullsend-ai/fullsend/pkg/behaviourtest/world" ) -func TestParseDummyAgentTable_RequiresFixturesRoot(t *testing.T) { +func TestParseDummyAgentTable_EmptyIdentity(t *testing.T) { + t.Parallel() w := &world.World{} table := &godog.Table{ Rows: []*messages.PickleTableRow{ @@ -22,6 +23,19 @@ func TestParseDummyAgentTable_RequiresFixturesRoot(t *testing.T) { } err := parseDummyAgentTable(w, table) require.Error(t, err) + assert.Contains(t, err.Error(), "no repo configured") +} + +func TestParseDummyAgentTable_RequiresFixturesRoot(t *testing.T) { + w := &world.World{Org: "org", RepoName: "repo"} + table := &godog.Table{ + Rows: []*messages.PickleTableRow{ + {Cells: []*messages.PickleTableCell{{Value: "description"}, {Value: "op"}, {Value: "args"}}}, + {Cells: []*messages.PickleTableCell{{Value: "x"}, {Value: "read_file"}, {Value: "foo"}}}, + }, + } + err := parseDummyAgentTable(w, table) + require.Error(t, err) assert.Contains(t, err.Error(), "FixturesRoot") } diff --git a/pkg/behaviourtest/steps/url_dispatch.go b/pkg/behaviourtest/steps/url_dispatch.go index b55f1a4bb8..2f12876653 100644 --- a/pkg/behaviourtest/steps/url_dispatch.go +++ b/pkg/behaviourtest/steps/url_dispatch.go @@ -105,6 +105,9 @@ func resolveHostRepoName(w *world.World, logicalName string) string { // enrolled test repository. The URL points to the file via // raw.githubusercontent.com on the default branch of the hosting repo. func givenURLSourcedCustomHarness(w *world.World, name, doc string, opts urlHarnessOpts) error { + if w.Org == "" || w.RepoName == "" { + return fmt.Errorf("no repo configured; call 'Given the enrolled test repository' before URL-harness operations") + } name = strings.TrimSpace(name) doc = strings.TrimSpace(doc) if name == "" || doc == "" { @@ -189,8 +192,8 @@ func givenURLSourcedCustomHarness(w *world.World, name, doc string, opts urlHarn // Update config.yaml on the enrolled test repo: register agent with URL // source and update allowlist. - cfgOwner := w.Install.ConfigOwner() - cfgRepo := w.Install.ConfigRepo() + cfgOwner := w.Org + cfgRepo := w.RepoName cfgPath := path.Join(".fullsend", "config.yaml") cfgData, err := w.SCM.GetFileContent(ctx, cfgOwner, cfgRepo, cfgPath) if err != nil { diff --git a/pkg/behaviourtest/steps/url_dispatch_test.go b/pkg/behaviourtest/steps/url_dispatch_test.go index 6425a3e575..74abfad56a 100644 --- a/pkg/behaviourtest/steps/url_dispatch_test.go +++ b/pkg/behaviourtest/steps/url_dispatch_test.go @@ -165,16 +165,25 @@ func TestGivenHarnessHostingRepo_LeasedRepoResolvesHostName(t *testing.T) { assert.Equal(t, "org", w.URLHarnessRepoOwner) } +func TestGivenURLSourcedCustomHarness_EmptyIdentity(t *testing.T) { + t.Parallel() + w := &world.World{SCM: &fakeURLSCM{files: map[string][]byte{}}} + err := givenURLSourcedCustomHarness(w, "test", "role: triage", urlHarnessOpts{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "no repo configured") +} + func TestGivenURLSourcedCustomHarness_Validation(t *testing.T) { - w := &world.World{} + w := &world.World{Org: "org", RepoName: "repo"} require.Error(t, givenURLSourcedCustomHarness(w, "", "doc", urlHarnessOpts{})) require.Error(t, givenURLSourcedCustomHarness(w, "agent", "", urlHarnessOpts{})) } func TestGivenURLSourcedCustomHarness_RequiresHostingRepo(t *testing.T) { w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, - SCM: &fakeURLSCM{files: map[string][]byte{}}, + Org: "test-org", + RepoName: "test-repo", + SCM: &fakeURLSCM{files: map[string][]byte{}}, } err := givenURLSourcedCustomHarness(w, "url-test", "agent: agents/triage.md", urlHarnessOpts{}) require.Error(t, err) @@ -187,7 +196,8 @@ func TestGivenURLSourcedCustomHarness_SetsDispatchAgent(t *testing.T) { "test-org/test-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, + Org: "test-org", + RepoName: "test-repo", SCM: scm, URLHarnessRepoOwner: "test-org", URLHarnessRepoName: "harness-host", @@ -206,7 +216,8 @@ func TestGivenURLSourcedCustomHarness_URLFormat(t *testing.T) { "my-org/my-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -242,7 +253,8 @@ func TestGivenURLSourcedCustomHarness_BadHash(t *testing.T) { "my-org/my-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -262,7 +274,8 @@ func TestGivenURLSourcedCustomHarness_SkipAllowlist(t *testing.T) { "my-org/my-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -292,7 +305,8 @@ func TestGivenURLSourcedCustomHarness_UpdatesExistingAgent(t *testing.T) { "my-org/my-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents:\n - name: url-test\n source: harness/url-test.yaml\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -317,7 +331,8 @@ func TestGivenURLSourcedCustomHarness_AllowlistDedup(t *testing.T) { "my-org/my-repo/.fullsend/config.yaml": []byte(fmt.Sprintf("version: \"1\"\nagents: []\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n - %q\n", hostPrefix)), }} w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -362,7 +377,8 @@ func TestGivenURLSourcedCustomHarness_CommitHarnessError(t *testing.T) { commitFileRepo: "harness-host", } w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -379,7 +395,8 @@ func TestGivenURLSourcedCustomHarness_LogsDiagnostics(t *testing.T) { }} var logged []string w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, + Org: "test-org", + RepoName: "test-repo", SCM: scm, URLHarnessRepoOwner: "test-org", URLHarnessRepoName: "harness-host", @@ -400,7 +417,8 @@ func TestGivenURLSourcedCustomHarness_InvalidConfigYAML(t *testing.T) { "my-org/my-repo/.fullsend/config.yaml": []byte("invalid: [yaml: content"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -417,7 +435,8 @@ func TestGivenURLSourcedCustomHarness_FileNotAccessibleAfterCommit(t *testing.T) getFileContentAlways: fmt.Errorf("file not found"), } w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -431,7 +450,8 @@ func TestGivenURLSourcedCustomHarness_GetConfigError(t *testing.T) { stubRawHTTPClient(t) scm := &fakeURLSCM{files: map[string][]byte{}} // no config file w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -453,7 +473,8 @@ func TestGivenURLSourcedCustomHarness_NonMainDefaultBranch(t *testing.T) { defaultBranch: "master", } w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -477,7 +498,8 @@ func TestGivenURLSourcedCustomHarness_GetDefaultBranchError(t *testing.T) { defaultBranchErr: fmt.Errorf("API rate limited"), } w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -495,7 +517,8 @@ func TestGivenURLSourcedCustomHarness_RawURLNotAccessible(t *testing.T) { "my-org/my-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -569,7 +592,8 @@ func TestGivenURLSourcedCustomHarness_CommitsAgentResource(t *testing.T) { "test-org/test-repo/.fullsend/config.yaml": []byte("version: \"1\"\nagents: []\nallowed_remote_resources:\n - \"https://raw.githubusercontent.com/fullsend-ai/fullsend/\"\n"), }} w := &world.World{ - Install: &fakeURLInstall{owner: "test-org", repo: "test-repo"}, + Org: "test-org", + RepoName: "test-repo", SCM: scm, URLHarnessRepoOwner: "test-org", URLHarnessRepoName: "harness-host", @@ -669,7 +693,8 @@ func TestGivenURLSourcedCustomHarness_CommitRelativeResourcesError(t *testing.T) commitFileRepo: "harness-host", } w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: scm, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -695,7 +720,8 @@ func TestGivenURLSourcedCustomHarness_RelativeResourceNotAccessible(t *testing.T } // Override GetFileContent to fail only for the agent resource path. w := &world.World{ - Install: &fakeURLInstall{owner: "my-org", repo: "my-repo"}, + Org: "my-org", + RepoName: "my-repo", SCM: &selectiveFailSCM{fakeURLSCM: scm, failPath: "agents/triage.md", calls: &calls}, URLHarnessRepoOwner: "my-org", URLHarnessRepoName: "harness-host", @@ -752,20 +778,6 @@ func TestWaitForFileAccessible_FileNotFound(t *testing.T) { // --- fakes --- -type fakeURLInstall struct { - owner string - repo string -} - -func (f *fakeURLInstall) Mode() string { return "per-repo" } -func (f *fakeURLInstall) ConfigOwner() string { return f.owner } -func (f *fakeURLInstall) ConfigRepo() string { return f.repo } -func (f *fakeURLInstall) ConfigPathPrefix() string { return ".fullsend" } -func (f *fakeURLInstall) TriageWorkflowRepo() string { return f.repo } -func (f *fakeURLInstall) TriageWorkflowFile() string { return "fullsend.yaml" } -func (f *fakeURLInstall) AgentWorkflowFile() string { return "reusable-triage.yml" } -func (f *fakeURLInstall) AgentArtifactName() string { return "fullsend-triage" } - // fakeURLSCM keys files by "owner/repo/path" so multi-repo tests // cannot silently collide. type fakeURLSCM struct { diff --git a/pkg/behaviourtest/world/world.go b/pkg/behaviourtest/world/world.go index 9974481f07..1498467f24 100644 --- a/pkg/behaviourtest/world/world.go +++ b/pkg/behaviourtest/world/world.go @@ -110,12 +110,7 @@ func (w *World) Clone() *World { const BehaviourScriptRepoPath = "behaviour/current-scenario.yaml" // BehaviourScriptPath returns the repo-relative path for the dummy agent script. +// BT is per-repo only; config always lives under .fullsend/. func (w *World) BehaviourScriptPath() string { - if w.Install == nil { - return BehaviourScriptRepoPath - } - if prefix := w.Install.ConfigPathPrefix(); prefix != "" { - return filepath.Join(prefix, BehaviourScriptRepoPath) - } - return BehaviourScriptRepoPath + return filepath.Join(".fullsend", BehaviourScriptRepoPath) } diff --git a/pkg/behaviourtest/world/world_test.go b/pkg/behaviourtest/world/world_test.go index 1f297133fa..e2f736a0d4 100644 --- a/pkg/behaviourtest/world/world_test.go +++ b/pkg/behaviourtest/world/world_test.go @@ -154,20 +154,10 @@ func TestClone_ConcurrentFieldIndependence(t *testing.T) { } } -func TestBehaviourScriptPath_NilInstall(t *testing.T) { +func TestBehaviourScriptPath(t *testing.T) { + // BT is per-repo only — BehaviourScriptPath always prefixes with + // .fullsend regardless of Install state. w := &World{} got := w.BehaviourScriptPath() - assert.Equal(t, BehaviourScriptRepoPath, got) -} - -func TestBehaviourScriptPath_EmptyPrefix(t *testing.T) { - w := &World{Install: &fakeInstallState{prefix: ""}} - got := w.BehaviourScriptPath() - assert.Equal(t, BehaviourScriptRepoPath, got) -} - -func TestBehaviourScriptPath_WithPrefix(t *testing.T) { - w := &World{Install: &fakeInstallState{prefix: ".fullsend"}} - got := w.BehaviourScriptPath() assert.Equal(t, ".fullsend/behaviour/current-scenario.yaml", got) }