diff --git a/internal/app/dependency_direction_test.go b/internal/app/dependency_direction_test.go deleted file mode 100644 index 8f3477d8..00000000 --- a/internal/app/dependency_direction_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package app - -import ( - "go/parser" - "go/token" - "path/filepath" - "strconv" - "strings" - "testing" -) - -func TestProductPackagesDoNotImportInboundAdapters(t *testing.T) { - t.Parallel() - productPackages := []string{ - ".", - "../contracts", - "../failure", - "../mcpcontract", - "../tuicontract", - } - forbidden := []string{ - "github.com/morluto/gitcontribute/internal/cli", - "github.com/morluto/gitcontribute/internal/mcpserver", - "github.com/morluto/gitcontribute/internal/tui", - } - for _, dir := range productPackages { - files, err := filepath.Glob(filepath.Join(dir, "*.go")) - if err != nil { - t.Fatal(err) - } - for _, path := range files { - if strings.HasSuffix(path, "_test.go") { - continue - } - file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly) - if err != nil { - t.Fatalf("parse %s: %v", path, err) - } - for _, spec := range file.Imports { - importPath, err := strconv.Unquote(spec.Path.Value) - if err != nil { - t.Fatalf("unquote import in %s: %v", path, err) - } - for _, adapter := range forbidden { - if importPath == adapter { - t.Errorf("%s imports inbound adapter %s", path, adapter) - } - } - } - } - } -} diff --git a/internal/app/mcp_portfolio_test.go b/internal/app/mcp_portfolio_test.go index 36745e24..e4ca5976 100644 --- a/internal/app/mcp_portfolio_test.go +++ b/internal/app/mcp_portfolio_test.go @@ -73,7 +73,6 @@ func TestPullRequestPortfolioDerivesConflictAndPreservesUnknownCoverage(t *testi if len(conciseJSON) >= len(detailedJSON) { t.Fatalf("concise portfolio is not smaller: concise=%d detailed=%d", len(conciseJSON), len(detailedJSON)) } - t.Logf("portfolio response bytes: concise=%d detailed=%d", len(conciseJSON), len(detailedJSON)) } func TestPullRequestPortfolioClassifiesClosedUnmerged(t *testing.T) { diff --git a/internal/app/mcp_stdio_e2e_test.go b/internal/app/mcp_stdio_e2e_test.go index 671716ac..6b0e75d8 100644 --- a/internal/app/mcp_stdio_e2e_test.go +++ b/internal/app/mcp_stdio_e2e_test.go @@ -83,16 +83,6 @@ func TestMCPStdioScalableResearchFlow(t *testing.T) { if initialized == nil || initialized.ServerInfo == nil || initialized.ServerInfo.Name != "gitcontribute" { t.Fatalf("initialize result = %+v", initialized) } - for _, phrase := range []string{ - "corpus.* tools are offline reads", "never refresh implicitly", "explicit bounded network reads", - "polling through jobs.get", "observations are unknown rather than negative evidence", - "Only advertised tools are available", "never mutates GitHub", - } { - if !strings.Contains(initialized.Instructions, phrase) { - t.Errorf("instructions missing %q: %s", phrase, initialized.Instructions) - } - } - tools := make(map[string]*mcp.Tool) for tool, err := range session.Tools(ctx, nil) { if err != nil { diff --git a/internal/app/runtime_contract.go b/internal/app/runtime_contract.go index fe6150b6..2d31ab03 100644 --- a/internal/app/runtime_contract.go +++ b/internal/app/runtime_contract.go @@ -1,7 +1,6 @@ package app import ( - "context" "errors" "strings" @@ -27,11 +26,3 @@ func NewRuntimeContract(version string) (*contracts.RuntimeContractResult, error SupportedSchemaVersion: schema, }, nil } - -// RuntimeContract reports immutable executable compatibility metadata. -func (s *Service) RuntimeContract(ctx context.Context) (*contracts.RuntimeContractResult, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - return NewRuntimeContract(s.version) -} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 50fe8ccd..98313d00 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -77,7 +77,6 @@ type rootCmd struct { Setup setupCmd `cmd:"" help:"Set up GitContribute for MCP, CLI, or both"` Remove removeCmd `cmd:"" help:"Remove GitContribute coding-agent integrations"` Upgrade upgradeCmd `cmd:"" help:"Check for or install the latest release"` - Contract contractCmd `cmd:"" name:"runtime-contract" help:"Print the executable runtime compatibility contract"` Init initCmd `cmd:"" help:"Initialize the local corpus"` Corpus corpusCmd `cmd:"" help:"Inspect, back up, or migrate the local corpus"` Configure configureCmd `cmd:"" help:"Inspect or update typed configuration"` @@ -154,8 +153,6 @@ type upgradeCmd struct { JSON bool `name:"json" help:"Print the result as JSON"` } -type contractCmd struct{} - type configureCmd struct { Database *string `name:"database" help:"Corpus database path"` TokenSource *string `name:"token-source" help:"GitHub token source (none, env, gh-cli, or keyring)"` diff --git a/internal/cli/dispatch.go b/internal/cli/dispatch.go index 52fb74cd..d112badb 100644 --- a/internal/cli/dispatch.go +++ b/internal/cli/dispatch.go @@ -11,54 +11,53 @@ import ( // group owns its own behavior. func (c *CLI) dispatchCommand(ctx context.Context, cmd, command string, parsed *rootCmd) error { handlers := map[string]func() error{ - "setup": func() error { return c.runSetupCommand(ctx, &parsed.Setup) }, - "remove": func() error { return c.runRemoveCommand(ctx, &parsed.Remove) }, - "upgrade": func() error { return c.runUpgrade(ctx, &parsed.Upgrade) }, - "runtime-contract": func() error { return c.runRuntimeContract(ctx) }, - "init": func() error { return c.runInit(ctx, &parsed.Init) }, - "corpus": func() error { return c.runCorpus(ctx, command, &parsed.Corpus) }, - "configure": func() error { return c.runConfigure(ctx, &parsed.Configure) }, - "metadata": func() error { return c.runMetadata(ctx, &parsed.Metadata) }, - "status": func() error { return c.runStatus(ctx, &parsed.Status) }, - "doctor": func() error { return c.runDoctor(ctx, &parsed.Doctor) }, - "health": func() error { return c.runHealth(ctx, &parsed.Health) }, - "radar": func() error { return c.runRadar(ctx, &parsed.Radar) }, - "search": func() error { return c.runSearch(ctx, command, &parsed.Search) }, - "dossier": func() error { return c.runDossier(ctx, command, &parsed.Dossier) }, - "research": func() error { return c.runResearch(ctx, command, &parsed.Research) }, - "seeds": func() error { return c.runSeeds(ctx, &parsed.Seeds) }, - "index": func() error { return c.runIndex(ctx, &parsed.Index) }, - "acquire": func() error { return c.runAcquire(ctx, &parsed.Acquire) }, - "source": func() error { return c.runSource(ctx, command, &parsed.Source) }, - "crawl": func() error { return c.runCrawl(ctx, &parsed.Crawl) }, - "tail": func() error { return c.runTail(ctx, &parsed.Tail) }, - "investigation": func() error { return c.runInvestigation(ctx, command, &parsed.Investigation) }, - "hypothesis": func() error { return c.runHypothesis(ctx, command, &parsed.Hypothesis) }, - "duplicates": func() error { return c.runCheck(ctx, command, "duplicates", &parsed.Duplicates) }, - "collisions": func() error { return c.runCheck(ctx, command, "collisions", &parsed.Collisions) }, - "opportunity": func() error { return c.runOpportunity(ctx, command, &parsed.Opportunity) }, - "concern": func() error { return c.runConcern(ctx, command, &parsed.Concern) }, - "workspace": func() error { return c.runWorkspace(ctx, command, &parsed.Workspace) }, - "diff": func() error { return c.runDiff(ctx, &parsed.Diff) }, - "validation": func() error { return c.runValidation(ctx, command, &parsed.Validation) }, - "evidence": func() error { return c.runEvidence(ctx, command, &parsed.Evidence) }, - "readiness": func() error { return c.runReadiness(ctx, command, &parsed.Readiness) }, - "prepare": func() error { return c.runPrepare(ctx, command, &parsed.Prepare) }, - "archive": func() error { return c.runArchive(ctx, command, &parsed.Archive) }, - "coverage": func() error { return c.runCoverage(ctx, &parsed.Coverage) }, - "runs": func() error { return c.runRuns(ctx, &parsed.Runs) }, - "jobs": func() error { return c.runJobs(ctx, command, &parsed.Jobs) }, - "neighbors": func() error { return c.runNeighbors(ctx, &parsed.Neighbors) }, - "export": func() error { return c.runExport(ctx, command, &parsed.Export) }, - "clusters": func() error { return c.runClusters(ctx, command, &parsed.Clusters) }, - "cluster": func() error { return c.runCluster(ctx, command, &parsed.Cluster) }, - "lens": func() error { return c.runLens(ctx, command, &parsed.Lens) }, - "collection": func() error { return c.runCollection(ctx, command, &parsed.Collection) }, - "triage": func() error { return c.runTriage(ctx, command, &parsed.Triage) }, - "contribution": func() error { return c.runContribution(ctx, command, &parsed.Contribution) }, - "tracking": func() error { return c.runTracking(ctx, command, &parsed.Tracking) }, - "mcp": func() error { return c.runMCP(ctx, &parsed.MCP) }, - "tui": func() error { return c.runTUI(ctx, &parsed.TUI) }, + "setup": func() error { return c.runSetupCommand(ctx, &parsed.Setup) }, + "remove": func() error { return c.runRemoveCommand(ctx, &parsed.Remove) }, + "upgrade": func() error { return c.runUpgrade(ctx, &parsed.Upgrade) }, + "init": func() error { return c.runInit(ctx, &parsed.Init) }, + "corpus": func() error { return c.runCorpus(ctx, command, &parsed.Corpus) }, + "configure": func() error { return c.runConfigure(ctx, &parsed.Configure) }, + "metadata": func() error { return c.runMetadata(ctx, &parsed.Metadata) }, + "status": func() error { return c.runStatus(ctx, &parsed.Status) }, + "doctor": func() error { return c.runDoctor(ctx, &parsed.Doctor) }, + "health": func() error { return c.runHealth(ctx, &parsed.Health) }, + "radar": func() error { return c.runRadar(ctx, &parsed.Radar) }, + "search": func() error { return c.runSearch(ctx, command, &parsed.Search) }, + "dossier": func() error { return c.runDossier(ctx, command, &parsed.Dossier) }, + "research": func() error { return c.runResearch(ctx, command, &parsed.Research) }, + "seeds": func() error { return c.runSeeds(ctx, &parsed.Seeds) }, + "index": func() error { return c.runIndex(ctx, &parsed.Index) }, + "acquire": func() error { return c.runAcquire(ctx, &parsed.Acquire) }, + "source": func() error { return c.runSource(ctx, command, &parsed.Source) }, + "crawl": func() error { return c.runCrawl(ctx, &parsed.Crawl) }, + "tail": func() error { return c.runTail(ctx, &parsed.Tail) }, + "investigation": func() error { return c.runInvestigation(ctx, command, &parsed.Investigation) }, + "hypothesis": func() error { return c.runHypothesis(ctx, command, &parsed.Hypothesis) }, + "duplicates": func() error { return c.runCheck(ctx, command, "duplicates", &parsed.Duplicates) }, + "collisions": func() error { return c.runCheck(ctx, command, "collisions", &parsed.Collisions) }, + "opportunity": func() error { return c.runOpportunity(ctx, command, &parsed.Opportunity) }, + "concern": func() error { return c.runConcern(ctx, command, &parsed.Concern) }, + "workspace": func() error { return c.runWorkspace(ctx, command, &parsed.Workspace) }, + "diff": func() error { return c.runDiff(ctx, &parsed.Diff) }, + "validation": func() error { return c.runValidation(ctx, command, &parsed.Validation) }, + "evidence": func() error { return c.runEvidence(ctx, command, &parsed.Evidence) }, + "readiness": func() error { return c.runReadiness(ctx, command, &parsed.Readiness) }, + "prepare": func() error { return c.runPrepare(ctx, command, &parsed.Prepare) }, + "archive": func() error { return c.runArchive(ctx, command, &parsed.Archive) }, + "coverage": func() error { return c.runCoverage(ctx, &parsed.Coverage) }, + "runs": func() error { return c.runRuns(ctx, &parsed.Runs) }, + "jobs": func() error { return c.runJobs(ctx, command, &parsed.Jobs) }, + "neighbors": func() error { return c.runNeighbors(ctx, &parsed.Neighbors) }, + "export": func() error { return c.runExport(ctx, command, &parsed.Export) }, + "clusters": func() error { return c.runClusters(ctx, command, &parsed.Clusters) }, + "cluster": func() error { return c.runCluster(ctx, command, &parsed.Cluster) }, + "lens": func() error { return c.runLens(ctx, command, &parsed.Lens) }, + "collection": func() error { return c.runCollection(ctx, command, &parsed.Collection) }, + "triage": func() error { return c.runTriage(ctx, command, &parsed.Triage) }, + "contribution": func() error { return c.runContribution(ctx, command, &parsed.Contribution) }, + "tracking": func() error { return c.runTracking(ctx, command, &parsed.Tracking) }, + "mcp": func() error { return c.runMCP(ctx, &parsed.MCP) }, + "tui": func() error { return c.runTUI(ctx, &parsed.TUI) }, } if handler, ok := handlers[cmd]; ok { return handler() diff --git a/internal/cli/runtime_contract.go b/internal/cli/runtime_contract.go deleted file mode 100644 index ce3680a1..00000000 --- a/internal/cli/runtime_contract.go +++ /dev/null @@ -1,21 +0,0 @@ -package cli - -import ( - "context" - "encoding/json" - "errors" - - "github.com/morluto/gitcontribute/internal/contracts" -) - -func (c *CLI) runRuntimeContract(ctx context.Context) error { - service, ok := c.svc.(contracts.RuntimeContractService) - if !ok { - return NewCLIError(ExitNotWired, errors.New("runtime contract service is not available")) - } - contract, err := service.RuntimeContract(ctx) - if err != nil { - return c.mapError(err) - } - return json.NewEncoder(c.stdout).Encode(contract) -} diff --git a/internal/cli/runtime_contract_test.go b/internal/cli/runtime_contract_test.go deleted file mode 100644 index ccb43982..00000000 --- a/internal/cli/runtime_contract_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package cli_test - -import ( - "context" - "strings" - "testing" - - "github.com/morluto/gitcontribute/internal/contracts" -) - -type runtimeContractService struct { - *fakeService - called bool -} - -func (s *runtimeContractService) RuntimeContract(context.Context) (*contracts.RuntimeContractResult, error) { - s.called = true - return &contracts.RuntimeContractResult{Name: "gitcontribute", Version: "1.2.4", SupportedSchemaLineage: "canonical-v1", SupportedSchemaVersion: 28}, nil -} - -func TestRuntimeContractCommandIsAlwaysMachineReadable(t *testing.T) { - t.Parallel() - service := &runtimeContractService{fakeService: &fakeService{}} - command, stdout, _ := newTestCLI(service, nil) - requireNoErr(t, command.Run(context.Background(), []string{"runtime-contract"})) - if !service.called || !strings.Contains(stdout.String(), `"supported_schema_lineage":"canonical-v1"`) || !strings.Contains(stdout.String(), `"supported_schema_version":28`) { - t.Fatalf("called=%t output=%q", service.called, stdout.String()) - } -} diff --git a/internal/contracts/workflow_contracts.go b/internal/contracts/workflow_contracts.go index 5139960f..89fc1f0d 100644 --- a/internal/contracts/workflow_contracts.go +++ b/internal/contracts/workflow_contracts.go @@ -209,12 +209,6 @@ type ReadinessCheck struct { EvaluatedAt string `json:"evaluated_at"` } -// RuntimeContractService reports only immutable executable compatibility -// metadata. Implementations must not inspect configuration or the corpus. -type RuntimeContractService interface { - RuntimeContract(ctx context.Context) (*RuntimeContractResult, error) -} - // RuntimeContractResult is immutable executable compatibility metadata. type RuntimeContractResult struct { Name string `json:"name"` diff --git a/internal/corpus/corpus_fixture_test.go b/internal/corpus/corpus_fixture_test.go index fde10ef2..d39bac0b 100644 --- a/internal/corpus/corpus_fixture_test.go +++ b/internal/corpus/corpus_fixture_test.go @@ -81,22 +81,3 @@ func copyTestDatabase(source, destination string) error { } return out.Close() } - -func TestTestCorpusTemplateIsCurrentAndStandalone(t *testing.T) { - t.Parallel() - c, path := openTestCorpus(t) - if _, err := os.Stat(testCorpusTemplate(t) + "-wal"); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("template retained a WAL sidecar: %v", err) - } - _, target, err := c.SchemaVersions(context.Background()) - if err != nil { - t.Fatalf("schema versions: %v", err) - } - current, exists, err := InspectSchemaVersion(context.Background(), path) - if err != nil { - t.Fatalf("inspect copied schema: %v", err) - } - if !exists || current != target { - t.Fatalf("copied schema version = %d (exists=%t), want current %d", current, exists, target) - } -} diff --git a/internal/corpus/dossiers_test.go b/internal/corpus/dossiers_test.go index 0cefc491..42317dfa 100644 --- a/internal/corpus/dossiers_test.go +++ b/internal/corpus/dossiers_test.go @@ -9,53 +9,6 @@ import ( "github.com/morluto/gitcontribute/internal/domain" ) -func TestDossiersMigration(t *testing.T) { - t.Parallel() - ctx := context.Background() - c, _ := openTestCorpus(t) - - rows, err := c.db.QueryContext(ctx, `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('dossiers', 'dossier_sources') ORDER BY name`) - if err != nil { - t.Fatalf("query tables: %v", err) - } - defer func() { _ = rows.Close() }() - - var names []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - t.Fatalf("scan table name: %v", err) - } - names = append(names, name) - } - if err := rows.Err(); err != nil { - t.Fatal(err) - } - if len(names) != 2 || names[0] != "dossier_sources" || names[1] != "dossiers" { - t.Fatalf("expected dossier tables, got %v", names) - } - - for _, col := range []string{"id", "repository_id", "commit_sha", "as_of", "section_metadata", "snapshot", "generated_at", "created_at"} { - var found int - if err := c.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM pragma_table_info('dossiers') WHERE name=?`, col).Scan(&found); err != nil { - t.Fatalf("pragma dossiers %s: %v", col, err) - } - if found != 1 { - t.Fatalf("dossiers missing column %s", col) - } - } - - for _, col := range []string{"id", "dossier_id", "source", "url", "commit_sha", "observed_at", "as_of"} { - var found int - if err := c.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM pragma_table_info('dossier_sources') WHERE name=?`, col).Scan(&found); err != nil { - t.Fatalf("pragma dossier_sources %s: %v", col, err) - } - if found != 1 { - t.Fatalf("dossier_sources missing column %s", col) - } - } -} - func TestDossierSaveGetListAndRefresh(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/internal/corpus/migration_test.go b/internal/corpus/migration_test.go index 6839b4ef..7fa846e7 100644 --- a/internal/corpus/migration_test.go +++ b/internal/corpus/migration_test.go @@ -2,69 +2,10 @@ package corpus import ( "context" - "database/sql" - "path/filepath" "testing" "time" ) -func TestBaselineMigrationCreatesCurrentSchema(t *testing.T) { - t.Parallel() - ctx := context.Background() - path := filepath.Join(t.TempDir(), "corpus.db") - c, err := Open(ctx, path) - if err != nil { - t.Fatalf("open corpus: %v", err) - } - defer func() { _ = c.Close() }() - - for _, table := range []string{ - "repositories", "repository_observations", "threads", "thread_observations", - "facet_coverage", "facet_observations", "code_snapshots", "code_documents", - "threads_fts", "facet_observations_fts", "code_documents_fts", "projection_states", - "investigations", "opportunities", "workspaces", "dossiers", "cluster_runs", "clusters", - "contribution_manifests", - "concerns", "concern_links", "concerns_fts", - "validation_run_groups", - "code_index_artifacts", "corpus_snapshot_tokens", "corpus_read_artifacts", - "pull_request_feedback_discovery", "pull_request_feedback_projection", "pull_request_feedback_fts", - "actors", "actor_aliases", "actor_observations", "actor_profiles", "actor_social_accounts", - "actor_organization_memberships", "actor_pinned_items", "actor_repository_affiliations", - "actor_contribution_periods", "actor_contribution_days", "actor_contribution_items", - "actor_repository_contribution_totals", "actors_fts", - } { - if !migrationTableExists(ctx, t, c.db, table) { - t.Fatalf("table %s missing after baseline migration", table) - } - } - for _, table := range []string{ - "actors", "actor_aliases", "actor_observations", "actor_profiles", "actor_social_accounts", - "actor_organization_memberships", "actor_pinned_items", "actor_repository_affiliations", - "actor_contribution_periods", "actor_contribution_days", "actor_contribution_items", - "actor_repository_contribution_totals", - } { - for _, suffix := range []string{"ai", "au", "ad"} { - trigger := "corpus_revision_" + table + "_" + suffix - if !migrationTriggerExists(ctx, t, c.db, trigger) { - t.Fatalf("trigger %s missing after baseline migration", trigger) - } - } - } - - for _, col := range []string{"merged_known", "author_association", "assignees", "draft", "locked", "state_reason", "milestone"} { - if !migrationColumnExists(ctx, t, c.db, "threads", col) { - t.Fatalf("column threads.%s missing after baseline migration", col) - } - } - - if !migrationColumnExists(ctx, t, c.db, "projection_states", "source_revision") { - t.Fatal("projection_states.source_revision missing after baseline migration") - } - if !migrationColumnExists(ctx, t, c.db, "projection_states", "content_hash") { - t.Fatal("projection_states.content_hash missing after baseline migration") - } -} - func TestActorMigrationDeduplicatesExistingLoginsCaseInsensitively(t *testing.T) { t.Parallel() ctx := context.Background() @@ -100,30 +41,3 @@ func TestActorMigrationDeduplicatesExistingLoginsCaseInsensitively(t *testing.T) t.Fatalf("case-insensitive actor count = %d, want 1", count) } } - -func migrationTableExists(ctx context.Context, t *testing.T, db *sql.DB, table string) bool { - t.Helper() - var found int - if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&found); err != nil { - t.Fatalf("query migration table %s: %v", table, err) - } - return found == 1 -} - -func migrationColumnExists(ctx context.Context, t *testing.T, db *sql.DB, table, column string) bool { - t.Helper() - var found int - if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM pragma_table_info(?) WHERE name=?`, table, column).Scan(&found); err != nil { - t.Fatalf("query migration column %s.%s: %v", table, column, err) - } - return found == 1 -} - -func migrationTriggerExists(ctx context.Context, t *testing.T, db *sql.DB, trigger string) bool { - t.Helper() - var found int - if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM sqlite_master WHERE type='trigger' AND name=?`, trigger).Scan(&found); err != nil { - t.Fatalf("query migration trigger %s: %v", trigger, err) - } - return found == 1 -} diff --git a/internal/corpus/tracking_test.go b/internal/corpus/tracking_test.go index 3d9d232a..5a4c2505 100644 --- a/internal/corpus/tracking_test.go +++ b/internal/corpus/tracking_test.go @@ -72,20 +72,6 @@ func TestExportLocalMetadataRejectsTruncationButAllowsExactLimit(t *testing.T) { } } -func TestTrackingMigrationCreatesTables(t *testing.T) { - t.Parallel() - ctx := context.Background() - c, _ := openTestCorpus(t) - - var count int - if err := c.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('triage_events', 'contributions', 'contribution_outcomes')`).Scan(&count); err != nil { - t.Fatalf("count tracking tables: %v", err) - } - if count != 3 { - t.Fatalf("expected 3 tracking tables, got %d", count) - } -} - func TestTriageEventPersistsWithOptionalForeignKeyLinks(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/internal/deepwiki/client.go b/internal/deepwiki/client.go index ac408828..c466fc38 100644 --- a/internal/deepwiki/client.go +++ b/internal/deepwiki/client.go @@ -40,7 +40,6 @@ type Reader interface { // Client calls a public DeepWiki MCP endpoint. An empty Endpoint uses DefaultEndpoint. type Client struct { Endpoint string - callTool func(context.Context, string, string, map[string]any) (*mcp.CallToolResult, error) } var ( @@ -63,11 +62,7 @@ func (c *Client) Read(ctx context.Context, req Request) (_ Response, err error) if err != nil { return Response{}, err } - callTool := c.callTool - if callTool == nil { - callTool = callDeepWikiTool - } - result, err := callTool(ctx, endpoint, name, arguments) + result, err := callDeepWikiTool(ctx, endpoint, name, arguments) if err != nil { return Response{}, fmt.Errorf("call DeepWiki %s: %w", name, err) } diff --git a/internal/deepwiki/client_test.go b/internal/deepwiki/client_test.go index 391fdd7b..2b34f15a 100644 --- a/internal/deepwiki/client_test.go +++ b/internal/deepwiki/client_test.go @@ -2,15 +2,17 @@ package deepwiki import ( "context" - "errors" - "reflect" + "encoding/json" + "net/http" + "net/http/httptest" "strings" + "sync" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" ) -func TestToolCall(t *testing.T) { +func TestClientReadRoutesRequests(t *testing.T) { t.Parallel() tests := []struct { name, action, repository, question, wantName string @@ -24,31 +26,54 @@ func TestToolCall(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - name, args, err := toolCall(Request{Action: tt.action, Repository: tt.repository, Repositories: tt.repositories, Question: tt.question}) - if err != nil || name != tt.wantName || !reflect.DeepEqual(args, tt.wantArgs) { - t.Fatalf("toolCall = %q, %#v, %v; want %q, %#v", name, args, err, tt.wantName, tt.wantArgs) + var ( + mu sync.Mutex + name string + args map[string]any + ) + client := newTestClient(t, func(gotName string, gotArgs map[string]any) (*mcp.CallToolResult, error) { + mu.Lock() + defer mu.Unlock() + name, args = gotName, gotArgs + return &mcp.CallToolResult{}, nil + }) + if _, err := client.Read(context.Background(), Request{Action: tt.action, Repository: tt.repository, Repositories: tt.repositories, Question: tt.question}); err != nil { + t.Fatal(err) + } + mu.Lock() + defer mu.Unlock() + gotArgs, err := json.Marshal(args) + if err != nil { + t.Fatal(err) + } + wantArgs, err := json.Marshal(tt.wantArgs) + if err != nil { + t.Fatal(err) + } + if name != tt.wantName || string(gotArgs) != string(wantArgs) { + t.Fatalf("tool call = %q, %#v; want %q, %#v", name, args, tt.wantName, tt.wantArgs) } }) } } -func TestToolCallRejectsMissingAndUnsupportedInputs(t *testing.T) { +func TestClientReadRejectsMissingAndUnsupportedInputs(t *testing.T) { t.Parallel() for _, req := range []Request{{Action: "structure"}, {Action: "contents"}, {Action: "question"}, {Action: "unknown"}} { - if _, _, err := toolCall(req); err == nil { - t.Fatalf("toolCall(%+v) accepted invalid input", req) + if _, err := (&Client{}).Read(context.Background(), req); err == nil { + t.Fatalf("Read(%+v) accepted invalid input", req) } } } func TestClientReadMapsResponse(t *testing.T) { t.Parallel() - client := &Client{callTool: func(_ context.Context, endpoint, name string, args map[string]any) (*mcp.CallToolResult, error) { - if endpoint != DefaultEndpoint || name != "read_wiki_contents" || args["repoName"] != "owner/repo" { - t.Fatalf("call = %q, %q, %#v", endpoint, name, args) + client := newTestClient(t, func(name string, args map[string]any) (*mcp.CallToolResult, error) { + if name != "read_wiki_contents" || args["repoName"] != "owner/repo" { + return nil, &unexpectedToolCallError{name: name, args: args} } return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "first"}, &mcp.TextContent{Text: "https://deepwiki.com/owner/repo#topic"}}}, nil - }} + }) got, err := client.Read(context.Background(), Request{Action: "contents", Repository: "owner/repo"}) if err != nil { t.Fatal(err) @@ -60,19 +85,19 @@ func TestClientReadMapsResponse(t *testing.T) { func TestClientReadHandlesProviderAndTransportFailures(t *testing.T) { t.Parallel() - provider := &Client{callTool: func(context.Context, string, string, map[string]any) (*mcp.CallToolResult, error) { + provider := newTestClient(t, func(string, map[string]any) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{IsError: true}, nil - }} + }) got, err := provider.Read(context.Background(), Request{Action: "structure", Repository: "owner/repo"}) if err != nil || got.Available { t.Fatalf("provider error = %+v, %v", got, err) } - transport := &Client{callTool: func(context.Context, string, string, map[string]any) (*mcp.CallToolResult, error) { - return nil, errors.New("offline") - }} + transportServer := httptest.NewServer(http.NotFoundHandler()) + t.Cleanup(transportServer.Close) + transport := &Client{Endpoint: transportServer.URL} _, err = transport.Read(context.Background(), Request{Action: "structure", Repository: "owner/repo"}) - if err == nil || !strings.Contains(err.Error(), "call DeepWiki read_wiki_structure: offline") { + if err == nil || !strings.Contains(err.Error(), "call DeepWiki read_wiki_structure:") { t.Fatalf("transport error = %v", err) } } @@ -98,9 +123,9 @@ func TestClientReadClassifiesProviderErrorTextAsUnavailable(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := &Client{callTool: func(context.Context, string, string, map[string]any) (*mcp.CallToolResult, error) { + client := newTestClient(t, func(string, map[string]any) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: tt.text}}}, nil - }} + }) got, err := client.Read(context.Background(), Request{ Action: "question", Repositories: []string{"indexed/repo", "missing/repo"}, @@ -116,9 +141,9 @@ func TestClientReadClassifiesProviderErrorTextAsUnavailable(t *testing.T) { func TestClientReadKeepsNormalMultiRepositoryAnswerAvailable(t *testing.T) { t.Parallel() const answer = "indexed/repo and other/repo both organize documentation by subsystem." - client := &Client{callTool: func(context.Context, string, string, map[string]any) (*mcp.CallToolResult, error) { + client := newTestClient(t, func(string, map[string]any) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: answer}}}, nil - }} + }) got, err := client.Read(context.Background(), Request{ Action: "question", Repositories: []string{"indexed/repo", "other/repo"}, @@ -131,11 +156,36 @@ func TestClientReadKeepsNormalMultiRepositoryAnswerAvailable(t *testing.T) { func TestClientReadAcceptsEmptySuccessfulResponse(t *testing.T) { t.Parallel() - client := &Client{callTool: func(context.Context, string, string, map[string]any) (*mcp.CallToolResult, error) { + client := newTestClient(t, func(string, map[string]any) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{}, nil - }} + }) got, err := client.Read(context.Background(), Request{Action: "structure", Repository: "owner/repo"}) if err != nil || !got.Available || got.Text != "" || got.SourceURL != "" { t.Fatalf("empty response = %+v, %v", got, err) } } + +type unexpectedToolCallError struct { + name string + args map[string]any +} + +func (e *unexpectedToolCallError) Error() string { + return "unexpected DeepWiki tool call " + e.name +} + +func newTestClient(t *testing.T, respond func(string, map[string]any) (*mcp.CallToolResult, error)) *Client { + t.Helper() + server := mcp.NewServer(&mcp.Implementation{Name: "deepwiki-fixture", Version: "1"}, nil) + for _, name := range []string{"read_wiki_structure", "read_wiki_contents", "ask_question"} { + mcp.AddTool(server, &mcp.Tool{Name: name}, func(_ context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + result, err := respond(name, args) + return result, nil, err + }) + } + httpServer := httptest.NewServer(mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { + return server + }, &mcp.StreamableHTTPOptions{JSONResponse: true})) + t.Cleanup(httpServer.Close) + return &Client{Endpoint: httpServer.URL} +} diff --git a/internal/mcpcontract/source_audit_workflow_test.go b/internal/mcpcontract/source_audit_workflow_test.go deleted file mode 100644 index bff4e3de..00000000 --- a/internal/mcpcontract/source_audit_workflow_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package mcpcontract - -import "testing" - -func TestCanonicalSourceAuditWorkflowTransitionsAndAuthorities(t *testing.T) { - wf := CanonicalSourceAuditWorkflow() - if wf.Version != SourceAuditWorkflowVersion || len(wf.Transitions) != 9 { - t.Fatalf("workflow = %+v", wf) - } - for i, transition := range wf.Transitions { - if transition.ID == "" || transition.Operation == "" || transition.ExpectedResultType == "" || transition.IncompleteSemantics == "" { - t.Fatalf("transition %d is incomplete: %+v", i, transition) - } - } - if wf.Transitions[0].Authority.Network || !wf.Transitions[1].Authority.Network || !wf.Transitions[1].Authority.LocalWrite { - t.Fatalf("coverage authorities = %+v -> %+v", wf.Transitions[0].Authority, wf.Transitions[1].Authority) - } - if wf.Transitions[3].RequiredInputToken != "snapshot_token" || wf.Transitions[3].Authority.Network { - t.Fatalf("offline reread transition = %+v", wf.Transitions[3]) - } -} diff --git a/internal/mcpserver/catalog_test.go b/internal/mcpserver/catalog_test.go index 84eea207..55422a5e 100644 --- a/internal/mcpserver/catalog_test.go +++ b/internal/mcpserver/catalog_test.go @@ -9,29 +9,12 @@ import ( "strconv" "strings" "testing" - "unicode" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/morluto/gitcontribute/internal/facets" "github.com/morluto/gitcontribute/internal/mcpcontract" ) -var selectionSynonyms = map[string]string{ - "execute": "run", - "read": "get", - "rebuild": "build", - "refresh": "sync", - "review": "get", - "stop": "cancel", -} - -var selectionStopWords = map[string]bool{ - "a": true, "an": true, "and": true, "for": true, "from": true, "in": true, - "it": true, "of": true, "one": true, "or": true, "the": true, "this": true, - "to": true, "tool": true, "use": true, "with": true, "without": true, - "gitcontribute": true, "local": true, "stored": true, -} - func listedTools(t *testing.T) (map[string]*mcp.Tool, func()) { t.Helper() return listedToolsFromReader(t, &fakeReader{searchStarted: make(chan struct{})}) @@ -97,28 +80,6 @@ func TestCanonicalToolCatalogIsNamespacedAndUnambiguous(t *testing.T) { } } -func TestUnifiedCatalogReportsSerializedContextMeasurements(t *testing.T) { - tools, closeSessions := listedTools(t) - defer closeSessions() - payload, err := json.Marshal(tools) - if err != nil { - t.Fatal(err) - } - t.Logf("MCP unified catalog tools=%d serialized_bytes=%d", len(tools), len(payload)) - names := make([]string, 0, len(tools)) - for name := range tools { - names = append(names, name) - } - sort.Strings(names) - for _, name := range names { - payload, err := json.Marshal(tools[name]) - if err != nil { - t.Fatalf("marshal tool %s: %v", name, err) - } - t.Logf("MCP catalog tool=%s serialized_bytes=%d", name, len(payload)) - } -} - func TestStructuredCancellationIsNotRetryable(t *testing.T) { handler := structuredToolErrors(func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, struct{}, error) { return nil, struct{}{}, context.Canceled @@ -223,12 +184,6 @@ func TestToolSchemasExposeMachineReadableContracts(t *testing.T) { assertSchemaValue(t, tools[mcpcontract.ToolGetThreadFacets].InputSchema, []string{"properties", "threads", "maxItems"}, float64(100)) assertSchemaValue(t, tools[mcpcontract.ToolGetThreadFacets].InputSchema, []string{"properties", "facets", "maxItems"}, float64(10)) assertSchemaValue(t, tools[mcpcontract.ToolGetThreadFacets].InputSchema, []string{"properties", "facets", "items", "enum"}, facets.AllNames()) - if !strings.Contains(tools[mcpcontract.ToolGetCoverage].Description, "typed recovery action") { - t.Fatalf("coverage description does not expose recovery routing: %q", tools[mcpcontract.ToolGetCoverage].Description) - } - if !strings.Contains(tools[mcpcontract.ToolSyncThreads].Description, "poll jobs.get and reread") { - t.Fatalf("thread sync description does not expose the follow-up route: %q", tools[mcpcontract.ToolSyncThreads].Description) - } assertSchemaValue(t, tools[mcpcontract.ToolHydrateThreads].InputSchema, []string{"properties", "max_pages", "default"}, float64(3)) assertSchemaValue(t, tools[mcpcontract.ToolCreateWorkspace].InputSchema, []string{"required"}, []any{"investigation_id"}) assertSchemaValue(t, tools[mcpcontract.ToolAdoptWorkspace].InputSchema, []string{"required"}, []any{"investigation_id", "path", "base_ref"}) @@ -335,77 +290,6 @@ func TestCatalogRegistrationReportsToolSchemaError(t *testing.T) { } } -func TestAgentToolSelectionProxy(t *testing.T) { - tools, closeSessions := listedTools(t) - defer closeSessions() - // Keep this historical proxy corpus stable; repository-wide feedback has - // its own focused routing assertions below. - proxyTools := make(map[string]*mcp.Tool, len(tools)) - for name, tool := range tools { - if name != mcpcontract.ToolIndexPullRequestFeedback && name != mcpcontract.ToolSearchPullRequestFeedback { - proxyTools[name] = tool - } - } - - cases := []struct { - prompt string - want string - }{ - {"Search locally stored issue titles for a retry deadlock", mcpcontract.ToolSearchThreads}, - {"Search live GitHub for highly starred inference repositories", mcpcontract.ToolSearchGitHubRepositories}, - {"Read metadata for twelve repositories already stored in the corpus", mcpcontract.ToolGetRepositories}, - {"Fetch current GitHub stars, metadata, and contribution guidance for twelve repositories", mcpcontract.ToolSyncRepositoryContext}, - {"Read the complete stored body of pull request 42", mcpcontract.ToolGetThreads}, - {"Refresh issue and pull request thread headers for selected repositories from GitHub", mcpcontract.ToolSyncThreads}, - {"Fetch comments and reviews for one stored pull request from GitHub", mcpcontract.ToolSyncPullRequestFeedback}, - {"Find similar completed and rejected historical work for this issue", mcpcontract.ToolFindPrecedents}, - {"List my stored pull requests that need contributor attention", mcpcontract.ToolListPullRequestPortfolio}, - {"Acquire and index code for several repositories", mcpcontract.ToolIndexRepositories}, - {"Check actual Git merge conflicts between fetched revisions", mcpcontract.ToolCheckMergeConflicts}, - {"Create a local investigation without cloning a worktree", mcpcontract.ToolStartInvestigation}, - {"Clone the remote and create a managed Git worktree", mcpcontract.ToolCreateWorkspace}, - {"Render and persist a pull request draft from a verified managed workspace diff", mcpcontract.ToolPrepareContribution}, - {"Execute the stored validation command against the candidate workspace", mcpcontract.ToolRunValidation}, - {"Run a repeat stress validation group with concurrency and telemetry", mcpcontract.ToolRunValidation}, - {"Stop a running durable job", mcpcontract.ToolCancelJob}, - {"Poll several durable jobs together with structured progress", mcpcontract.ToolGetJob}, - {"Read stored facet coverage for several exact threads", mcpcontract.ToolGetThreadFacets}, - {"Read repository and thread coverage across several targets", mcpcontract.ToolGetCoverage}, - {"Compare contribution candidates with my authored pull requests for overlap", mcpcontract.ToolFindPortfolioOverlaps}, - {"Link an authored pull request to a local opportunity", mcpcontract.ToolLinkPullRequest}, - } - - correct := 0 - for _, tc := range cases { - got := selectToolByWords(tc.prompt, proxyTools) - if got == tc.want { - correct++ - continue - } - t.Errorf("prompt %q selected %q, want %q", tc.prompt, got, tc.want) - } - if correct != len(cases) { - t.Fatalf("tool-selection proxy accuracy = %d/%d", correct, len(cases)) - } -} - -func TestFeedbackToolSelectionProxy(t *testing.T) { - tools, closeSessions := listedTools(t) - defer closeSessions() - feedbackTools := map[string]*mcp.Tool{ - mcpcontract.ToolFindPrecedents: tools[mcpcontract.ToolFindPrecedents], - mcpcontract.ToolIndexPullRequestFeedback: tools[mcpcontract.ToolIndexPullRequestFeedback], - mcpcontract.ToolSearchPullRequestFeedback: tools[mcpcontract.ToolSearchPullRequestFeedback], - mcpcontract.ToolSyncPullRequestFeedback: tools[mcpcontract.ToolSyncPullRequestFeedback], - } - if got := selectToolByWords("Find every pull-request comment written by chatgpt-codex-connector[bot] across one repository", feedbackTools); got != mcpcontract.ToolIndexPullRequestFeedback { - t.Fatalf("repository-wide feedback discovery selected %q", got) - } - if got := selectToolByWords("Search indexed pull-request feedback by exact commenter login", feedbackTools); got != mcpcontract.ToolSearchPullRequestFeedback { - t.Fatalf("exact feedback author search selected %q", got) - } -} - func TestInvalidToolCallEvaluation(t *testing.T) { client, closeSessions := connect(t, &fakeReader{searchStarted: make(chan struct{})}) defer closeSessions() @@ -556,11 +440,6 @@ func TestSideEffectAuthorizationEvaluation(t *testing.T) { if prepare.Annotations == nil || prepare.Annotations.ReadOnlyHint || prepare.Annotations.OpenWorldHint == nil || *prepare.Annotations.OpenWorldHint { t.Fatalf("prepare contribution annotations = %+v", prepare.Annotations) } - for _, phrase := range []string{"inspects the managed workspace", "non-mutating Git", "Never posts", "mutates GitHub"} { - if !strings.Contains(prepare.Description, phrase) { - t.Errorf("prepare contribution description does not disclose boundary phrase %q", phrase) - } - } } func assertSchemaValue(t *testing.T, raw any, path []string, want any) { @@ -589,57 +468,3 @@ func stringValue(value any) string { text, _ := value.(string) return text } - -func selectToolByWords(prompt string, tools map[string]*mcp.Tool) string { - promptWords := meaningfulWords(prompt) - intent := firstIntentWord(prompt) - bestName := "" - bestScore := -1 - for name, tool := range tools { - nameAndTitle := meaningfulWords(strings.ReplaceAll(name, ".", " ") + " " + tool.Title) - description := meaningfulWords(tool.Description) - score := 0 - if intent != "" && nameAndTitle[intent] { - score += 5 - } - for word := range promptWords { - if nameAndTitle[word] { - score += 3 - } else if description[word] { - score++ - } - } - if score > bestScore || score == bestScore && name < bestName { - bestName, bestScore = name, score - } - } - return bestName -} - -func firstIntentWord(text string) string { - fields := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) - if len(fields) == 0 { - return "" - } - return selectionSynonyms[strings.TrimSuffix(fields[0], "s")] -} - -func meaningfulWords(text string) map[string]bool { - words := make(map[string]bool) - fields := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) - for _, word := range fields { - word = normalizeSelectionWord(word) - if len(word) > 1 && !selectionStopWords[word] { - words[word] = true - } - } - return words -} - -func normalizeSelectionWord(word string) string { - word = strings.TrimSuffix(word, "s") - if synonym := selectionSynonyms[word]; synonym != "" { - return synonym - } - return word -} diff --git a/internal/mcpserver/server_contract_test.go b/internal/mcpserver/server_contract_test.go index 016dbe64..514e2f85 100644 --- a/internal/mcpserver/server_contract_test.go +++ b/internal/mcpserver/server_contract_test.go @@ -10,32 +10,6 @@ import ( "github.com/morluto/gitcontribute/internal/mcpcontract" ) -func TestServerInstructionsContainRoutingPhrases(t *testing.T) { - client, closeSessions := connect(t, &fakeReader{searchStarted: make(chan struct{})}) - defer closeSessions() - - init := client.InitializeResult() - if init == nil { - t.Fatal("missing initialize result") - } - for _, phrase := range []string{ - "corpus.* tools are offline reads", - "never refresh implicitly", - "explicit bounded network reads", - "missing, stale, paginated, or truncated observations are unknown", - "polling through jobs.get", - "exact returned resource URIs", - "github.search_users", - "github.sync_user_*", - "Only advertised tools are available", - "never mutates GitHub", - } { - if !strings.Contains(init.Instructions, phrase) { - t.Errorf("instructions missing routing phrase %q:\n%s", phrase, init.Instructions) - } - } -} - func TestCatalogContractMatchesAdvertisedFeedbackRoute(t *testing.T) { client, closeSessions := connect(t, &fakeReader{searchStarted: make(chan struct{})}) defer closeSessions() @@ -108,35 +82,6 @@ func callCatalogContract(t *testing.T, client *mcp.ClientSession) mcpcontract.Ca return catalog } -func TestFeedbackToolDescriptionsDeclareRoutingBoundaries(t *testing.T) { - client, closeSessions := connect(t, &fakeReader{searchStarted: make(chan struct{})}) - defer closeSessions() - tools := make(map[string]*mcp.Tool) - for tool, err := range client.Tools(context.Background(), nil) { - if err != nil { - t.Fatal(err) - } - tools[tool.Name] = tool - } - cases := map[string][]string{ - mcpcontract.ToolIndexPullRequestFeedback: {"repository-wide audits", "bounded GitHub reads", "Poll jobs.get", mcpcontract.ToolSearchPullRequestFeedback, "never mutates GitHub"}, - mcpcontract.ToolSyncPullRequestFeedback: {"exact pull requests", "bounded GitHub network reads", "Poll jobs.get", mcpcontract.ToolIndexPullRequestFeedback, "never mutates GitHub"}, - mcpcontract.ToolSearchPullRequestFeedback: {"offline search", "after github.index_pull_request_feedback and jobs.get", "never contacts GitHub", "partial/unknown coverage"}, - mcpcontract.ToolSearchThreads: {"not a comment-level feedback search", mcpcontract.ToolSearchPullRequestFeedback}, - } - for name, phrases := range cases { - tool := tools[name] - if tool == nil { - t.Fatalf("missing tool %q", name) - } - for _, phrase := range phrases { - if !strings.Contains(tool.Description, phrase) { - t.Errorf("tool %q description missing %q: %s", name, phrase, tool.Description) - } - } - } -} - func TestSourceAuditContractUsesAdvertisedOperations(t *testing.T) { client, closeSessions := connect(t, &fakeReader{searchStarted: make(chan struct{})}) defer closeSessions() @@ -157,13 +102,30 @@ func TestSourceAuditContractUsesAdvertisedOperations(t *testing.T) { if err != nil || json.Unmarshal(data, &workflow) != nil { t.Fatalf("decode source-audit contract: result=%#v err=%v", result.StructuredContent, err) } + if workflow.Version == "" || len(workflow.Transitions) == 0 { + t.Fatalf("source-audit contract is incomplete: %+v", workflow) + } + transitions := make(map[string]mcpcontract.WorkflowTransition, len(workflow.Transitions)) for _, transition := range workflow.Transitions { + if transition.ID == "" || transition.Operation == "" || transition.ExpectedResultType == "" || transition.IncompleteSemantics == "" { + t.Errorf("source-audit transition is incomplete: %+v", transition) + } + transitions[transition.ID] = transition for _, operation := range append([]string{transition.Operation}, transition.AllowedNextActions...) { if !tools[operation] { t.Errorf("source-audit transition %q references unadvertised operation %q", transition.ID, operation) } } } + if coverage := transitions["coverage"]; coverage.Operation != mcpcontract.ToolGetCoverage || coverage.Authority.Network || coverage.Authority.LocalWrite { + t.Errorf("coverage transition = %+v", coverage) + } + if ensure := transitions["ensure_coverage"]; ensure.Operation != mcpcontract.ToolEnsureCoverage || !ensure.Authority.Network || !ensure.Authority.LocalWrite { + t.Errorf("ensure-coverage transition = %+v", ensure) + } + if reread := transitions["offline_reread"]; reread.RequiredInputToken != "snapshot_token" || reread.Authority.Network { + t.Errorf("offline reread transition = %+v", reread) + } } func TestDurableToolResultsIncludeSDKResourceLinks(t *testing.T) { @@ -189,11 +151,6 @@ func TestDurableToolResultsIncludeSDKResourceLinks(t *testing.T) { if !ok || link.URI != "gitcontribute://investigation/inv-1" || link.MIMEType != "application/json" { t.Fatalf("resource link = %#v", result.Content[0]) } - for _, phrase := range []string{"exact opaque URI unchanged", "do not shorten, pluralize, or reconstruct it"} { - if !strings.Contains(link.Description, phrase) { - t.Errorf("resource link description missing %q: %q", phrase, link.Description) - } - } } func TestJobArtifactResultsContainOnlyResourceLinks(t *testing.T) { @@ -207,7 +164,7 @@ func TestJobArtifactResultsContainOnlyResourceLinks(t *testing.T) { t.Fatalf("job artifact content = %+v", result) } link, ok := result.Content[0].(*mcp.ResourceLink) - if !ok || strings.Contains(strings.ToLower(link.Description), "codex") || !strings.Contains(link.Description, "exact opaque URI unchanged") { + if !ok || strings.Contains(strings.ToLower(link.Description), "codex") { t.Fatalf("job artifact link = %#v", result.Content[0]) } } diff --git a/internal/tracking/sanitize_test.go b/internal/tracking/sanitize_test.go index 37f1b8f1..b67e1fc2 100644 --- a/internal/tracking/sanitize_test.go +++ b/internal/tracking/sanitize_test.go @@ -3,25 +3,8 @@ package tracking import ( "strings" "testing" - - "github.com/morluto/gitcontribute/internal/redaction" ) -func TestSanitizeStringUsesCanonicalCredentialRedaction(t *testing.T) { - t.Parallel() - fixtures := []string{ - "Authorization: Bearer fixture-secret", - "api_key=fixture-secret", - "github_pat_" + strings.Repeat("a", 22), - "ghp_" + strings.Repeat("a", 36), - } - for _, fixture := range fixtures { - if got, want := sanitizeString(fixture), redaction.String(fixture); got != want { - t.Fatalf("sanitizeString(%q) = %q, canonical redaction = %q", fixture, got, want) - } - } -} - func TestSanitizeMetadataRedactsSensitiveKeysRecursively(t *testing.T) { t.Parallel() fixtureToken := strings.Join([]string{"fixture", "token"}, "-")