From 82ca3f2c5e6f56492ace0166b6aa03d54bca47b1 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 23 Aug 2026 16:23:50 +0200 Subject: [PATCH 1/7] refactor(graphres): name the gate answers for what they are --- internal/graphres/scope.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/graphres/scope.go b/internal/graphres/scope.go index 12a72d3..72e7e5b 100644 --- a/internal/graphres/scope.go +++ b/internal/graphres/scope.go @@ -146,28 +146,28 @@ func ScopeGate(scopes ScopeMap) graphql.OperationMiddleware { tier := role.Role(authkit.IdentityFromContext(ctx).Role) operation := graphql.GetOperationContext(ctx) if operation.Operation == nil { - return scopeRefusal("the operation") + return scopeError("the operation") } kind := operation.Operation.Operation for _, selected := range graphql.CollectFields(operation, operation.Operation.SelectionSet, nil) { if carried && !scopes.Allows(kind, selected.Name, token.Scopes) { - return scopeRefusal(scopes.Needed(kind, selected.Name)) + return scopeError(scopes.Needed(kind, selected.Name)) } if needed := scopes.Capability(kind, selected.Name); needed != "" && !role.Can(tier, needed) { - return capabilityRefusal(scopes.Needed(kind, selected.Name), needed) + return capabilityError(scopes.Needed(kind, selected.Name), needed) } } return next(ctx) } } -// scopeRefusal answers one operation with the scope its token lacks. -func scopeRefusal(needed string) graphql.ResponseHandler { - return refusal("scope required: "+needed, needed) +// scopeError answers one operation with the scope its token lacks. +func scopeError(needed string) graphql.ResponseHandler { + return refusedWith("scope required: "+needed, needed) } -// capabilityRefusal answers one operation naming the scope and the capability the caller's role lacked. -func capabilityRefusal(needed string, lacked role.Capability) graphql.ResponseHandler { +// capabilityError answers one operation naming the scope and the capability the caller's role lacked. +func capabilityError(needed string, lacked role.Capability) graphql.ResponseHandler { return graphql.OneShot(&graphql.Response{Errors: gqlerror.List{&gqlerror.Error{ Message: "admin required", Extensions: map[string]any{ @@ -178,8 +178,8 @@ func capabilityRefusal(needed string, lacked role.Capability) graphql.ResponseHa }}}) } -// refusal answers one operation with the message and the scope the refused field wanted. -func refusal(message, needed string) graphql.ResponseHandler { +// refusedWith answers one operation with the message and the scope the refused field wanted. +func refusedWith(message, needed string) graphql.ResponseHandler { return graphql.OneShot(&graphql.Response{Errors: gqlerror.List{&gqlerror.Error{ Message: message, Extensions: map[string]any{"code": "UNAUTHORIZED", "scope": needed}, From b50227b21837da3ecdbebabca0f4e0bad40242f0 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 23 Aug 2026 16:24:05 +0200 Subject: [PATCH 2/7] refactor(plugins): name the error explainers for what they return --- plugins/fields/store.go | 6 +++--- plugins/importer/commit.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/fields/store.go b/plugins/fields/store.go index 96ee402..6b7167e 100644 --- a/plugins/fields/store.go +++ b/plugins/fields/store.go @@ -37,13 +37,13 @@ func (s *store) define(ctx context.Context, definition Definition) error { return fmt.Errorf("fields: define definition: %w", err) } if tag.RowsAffected() == 0 { - return s.refusalFor(ctx, definition) + return s.errorFor(ctx, definition) } return nil } -// refusalFor reports why a definition the store refused could not be written. -func (s *store) refusalFor(ctx context.Context, definition Definition) error { +// errorFor reports why a definition the store refused could not be written. +func (s *store) errorFor(ctx context.Context, definition Definition) error { const query = `SELECT archived_at IS NULL FROM plugin_fields.definitions WHERE name = $1` var live bool if err := s.pool.QueryRow(ctx, query, definition.Name).Scan(&live); err != nil { diff --git a/plugins/importer/commit.go b/plugins/importer/commit.go index 8f3c2c8..8ffed4a 100644 --- a/plugins/importer/commit.go +++ b/plugins/importer/commit.go @@ -101,11 +101,11 @@ func (p *Plugin) settle(ctx context.Context, d draft, known registry) (settlemen // refusedText returns the settlement of a row carrying a value no field accepts. func refusedText(err error) settlement { - return settlement{outcome: outcomeFailed, reason: refusalDetail(err)} + return settlement{outcome: outcomeFailed, reason: errorDetail(err)} } -// refusalDetail returns the half of a refusal naming what the row got wrong. -func refusalDetail(err error) string { +// errorDetail returns the half of an error naming what the row got wrong. +func errorDetail(err error) string { var wrapped interface{ Unwrap() []error } if errors.As(err, &wrapped) { for _, held := range wrapped.Unwrap() { From 2762f02ba38e55a052c1ec596133104100554833 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 23 Aug 2026 16:26:08 +0200 Subject: [PATCH 3/7] docs: say error where the docblocks said refusal --- internal/graphres/errors.go | 4 ++-- internal/mcp/executor.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/graphres/errors.go b/internal/graphres/errors.go index 1186bcc..07a9f2c 100644 --- a/internal/graphres/errors.go +++ b/internal/graphres/errors.go @@ -63,7 +63,7 @@ var notFoundErrors = []error{ apitoken.ErrNotFound, } -// spokenAs names every brick refusal in the deployment's own voice. +// spokenAs names every brick error in the deployment's own voice. var spokenAs = []struct { sentinel error message string @@ -74,7 +74,7 @@ var spokenAs = []struct { {role.ErrBeyondReach, "that role is beyond your own"}, } -// speak rewrites a brick refusal so no package name reaches a caller. +// speak rewrites a brick error so no package name reaches a caller. func speak(presented *gqlerror.Error, err error) { for _, held := range spokenAs { if errors.Is(err, held.sentinel) { diff --git a/internal/mcp/executor.go b/internal/mcp/executor.go index bc24003..155963a 100644 --- a/internal/mcp/executor.go +++ b/internal/mcp/executor.go @@ -14,13 +14,13 @@ import ( // graphPath is where every tool posts its operation. const graphPath = "/api/graphql" -// graphError is one refusal the graph answered with. +// graphError is one error the graph answered with. type graphError struct { Message string `json:"message"` Extensions map[string]any `json:"extensions"` } -// Error names the refusal beside its code. +// Error names the error beside its code. func (e graphError) Error() string { if code, ok := e.Extensions["code"].(string); ok && code != "" { return fmt.Sprintf("%s (%s)", e.Message, code) From 8c7fd9eafbd646f6efc29fe507d7d1e212d04b6a Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 23 Aug 2026 16:26:47 +0200 Subject: [PATCH 4/7] test: name the error helpers and say error in their messages --- cmd/alphone/token_test.go | 2 +- internal/graphres/errors_test.go | 4 +- internal/graphres/scopegate_test.go | 72 ++++++++++---------- internal/graphres/tokens_test.go | 4 +- internal/mcp/contacts_test.go | 8 +-- internal/mcp/executor_test.go | 6 +- internal/mcp/tasks_test.go | 4 +- internal/mcp/workload_test.go | 4 +- internal/postgres/tokens_test.go | 2 +- internal/server/graphql_auth_test.go | 2 +- internal/server/scopegate_test.go | 4 +- plugins/fields/frontend/test/fields.test.tsx | 2 +- plugins/fields/values_internal_test.go | 6 +- plugins/importer/commit_test.go | 4 +- plugins/importer/provider_test.go | 4 +- plugins/importer/stage_test.go | 8 +-- plugins/whatsapp/send_test.go | 10 +-- 17 files changed, 73 insertions(+), 73 deletions(-) diff --git a/cmd/alphone/token_test.go b/cmd/alphone/token_test.go index 6853809..4a059bf 100644 --- a/cmd/alphone/token_test.go +++ b/cmd/alphone/token_test.go @@ -248,7 +248,7 @@ func TestTokenCreateRejectsAnUnreadableLifetime(t *testing.T) { }, io.Discard) if err == nil { - t.Error("token() error = nil, want a refusal of an unreadable lifetime") + t.Error("token() error = nil, want an unreadable lifetime refused") } } diff --git a/internal/graphres/errors_test.go b/internal/graphres/errors_test.go index b9eb3a9..24e0968 100644 --- a/internal/graphres/errors_test.go +++ b/internal/graphres/errors_test.go @@ -68,7 +68,7 @@ func TestPresentErrorMapsDomainErrors(t *testing.T) { } } -func TestPresentErrorSpeaksTheBrickRefusalsInItsOwnVoice(t *testing.T) { +func TestPresentErrorSpeaksTheBrickErrorsInItsOwnVoice(t *testing.T) { t.Parallel() cases := []struct { @@ -99,7 +99,7 @@ func TestPresentErrorSpeaksTheBrickRefusalsInItsOwnVoice(t *testing.T) { } } -func TestEverySpokenRefusalKeepsItsMessage(t *testing.T) { +func TestEverySpokenErrorKeepsItsMessage(t *testing.T) { t.Parallel() for _, spoken := range []error{ diff --git a/internal/graphres/scopegate_test.go b/internal/graphres/scopegate_test.go index 0f44596..9f2c9a0 100644 --- a/internal/graphres/scopegate_test.go +++ b/internal/graphres/scopegate_test.go @@ -75,11 +75,11 @@ func gatedWith(t *testing.T, query string, ctx context.Context) *graphql.Respons return graphres.ScopeGate(graphres.NewScopeMap(schema))(ctx, passed)(ctx) } -// refusalOf returns the single error message of a refused answer. -func refusalOf(t *testing.T, answered *graphql.Response) string { +// errorOf returns the single error message of a refused answer. +func errorOf(t *testing.T, answered *graphql.Response) string { t.Helper() if len(answered.Errors) != 1 { - t.Fatalf("errors = %v, want exactly one refusal", answered.Errors) + t.Fatalf("errors = %v, want exactly one error", answered.Errors) } return answered.Errors[0].Message } @@ -99,8 +99,8 @@ func TestScopeGateRefusesAnAdminFieldToAMemberSession(t *testing.T) { answered := gatedAsRole(t, `mutation { createUser }`, role.Member) - if got, want := refusalOf(t, answered), "admin required"; got != want { - t.Errorf("refusal = %q, want %q", got, want) + if got, want := errorOf(t, answered), "admin required"; got != want { + t.Errorf("error = %q, want %q", got, want) } if got := answered.Errors[0].Extensions["code"]; got != "UNAUTHORIZED" { t.Errorf("code = %v, want UNAUTHORIZED", got) @@ -123,13 +123,13 @@ func TestScopeGateNamesTheCapabilityARoleLacks(t *testing.T) { } } -func TestAScopeRefusalNamesNoCapability(t *testing.T) { +func TestAScopeErrorNamesNoCapability(t *testing.T) { t.Parallel() answered := gatedAsToken(t, `mutation { createContact }`, apitoken.ParseScopes("tasks:read")) if _, named := answered.Errors[0].Extensions["capability"]; named { - t.Error("a scope refusal named a capability, want the extension only where a role fell short") + t.Error("a scope error named a capability, want the extension only where a role fell short") } } @@ -139,8 +139,8 @@ func TestScopeGateRefusesEveryUserManagementFieldToAMember(t *testing.T) { for _, field := range []string{"createUser", "setUserRole", "setUserDisabled"} { answered := gatedAsRole(t, `mutation { `+field+` }`, role.Member) - if got, want := refusalOf(t, answered), "admin required"; got != want { - t.Errorf("%s refusal = %q, want %q", field, got, want) + if got, want := errorOf(t, answered), "admin required"; got != want { + t.Errorf("%s error = %q, want %q", field, got, want) } } } @@ -150,8 +150,8 @@ func TestScopeGateRefusesAFieldWhoseCapabilityTheRoleLacks(t *testing.T) { answered := gatedAsRole(t, `mutation { needsReports }`, role.Admin) - if got, want := refusalOf(t, answered), "admin required"; got != want { - t.Errorf("refusal = %q, want %q, an admin holding no manage_reports is refused", got, want) + if got, want := errorOf(t, answered), "admin required"; got != want { + t.Errorf("error = %q, want %q, an admin holding no manage_reports is refused", got, want) } } @@ -180,8 +180,8 @@ func TestScopeGateReadsAnUnstampedSessionAsAMember(t *testing.T) { answered := gatedWith(t, `mutation { createUser }`, t.Context()) - if got, want := refusalOf(t, answered), "admin required"; got != want { - t.Errorf("refusal = %q, want %q, losing the stamp demotes rather than widens", got, want) + if got, want := errorOf(t, answered), "admin required"; got != want { + t.Errorf("error = %q, want %q, losing the stamp demotes rather than widens", got, want) } } @@ -210,8 +210,8 @@ func TestScopeGateRefusesUserManagementToAnAccountHoldingNoRole(t *testing.T) { answered := gatedAsRole(t, `mutation { createUser }`, "") - if got, want := refusalOf(t, answered), "admin required"; got != want { - t.Errorf("refusal = %q, want %q", got, want) + if got, want := errorOf(t, answered), "admin required"; got != want { + t.Errorf("error = %q, want %q", got, want) } } @@ -230,8 +230,8 @@ func TestScopeGateHoldsAWildcardTokenToItsOwnersRole(t *testing.T) { answered := gatedAsTokenOf(t, `mutation { createUser }`, apitoken.Full(), role.Member) - if got, want := refusalOf(t, answered), "admin required"; got != want { - t.Errorf("refusal = %q, want %q, effective access is the role and the token together", got, want) + if got, want := errorOf(t, answered), "admin required"; got != want { + t.Errorf("error = %q, want %q, effective access is the role and the token together", got, want) } } @@ -263,8 +263,8 @@ func TestScopeGateRefusesAnAdminsNarrowTokenOnItsScope(t *testing.T) { answered := gatedAsTokenOf(t, `mutation { createUser }`, apitoken.ParseScopes("contacts:read"), role.Admin) - if got, want := refusalOf(t, answered), "scope required: users:write"; got != want { - t.Errorf("refusal = %q, want %q, the token check answers first", got, want) + if got, want := errorOf(t, answered), "scope required: users:write"; got != want { + t.Errorf("error = %q, want %q, the token check answers first", got, want) } } @@ -274,8 +274,8 @@ func TestScopeGateNamesTheScopeBeforeTheTierWhenBothRefuse(t *testing.T) { answered := gatedAsTokenOf(t, `mutation { createUser }`, apitoken.ParseScopes("contacts:read"), role.Member) - if got, want := refusalOf(t, answered), "scope required: users:write"; got != want { - t.Errorf("refusal = %q, want %q, the message the n8n docs quote stays put", got, want) + if got, want := errorOf(t, answered), "scope required: users:write"; got != want { + t.Errorf("error = %q, want %q, the message the n8n docs quote stays put", got, want) } } @@ -294,8 +294,8 @@ func TestScopeGateRefusesAWriteToAReadToken(t *testing.T) { answered := gatedAsToken(t, `mutation { createContact }`, apitoken.ParseScopes("contacts:read")) - if got := refusalOf(t, answered); !strings.Contains(got, "contacts:write") { - t.Errorf("refusal = %q, want it to name contacts:write", got) + if got := errorOf(t, answered); !strings.Contains(got, "contacts:write") { + t.Errorf("error = %q, want it to name contacts:write", got) } if got := answered.Errors[0].Extensions["code"]; got != "UNAUTHORIZED" { t.Errorf("code = %v, want UNAUTHORIZED", got) @@ -309,8 +309,8 @@ func TestScopeGateSeesThroughATopLevelFragmentSpread(t *testing.T) { `mutation { ...writes } fragment writes on Mutation { createContact }`, apitoken.ParseScopes("contacts:read")) - if got := refusalOf(t, answered); !strings.Contains(got, "contacts:write") { - t.Errorf("refusal = %q, want a fragment wrapped field checked like any other", got) + if got := errorOf(t, answered); !strings.Contains(got, "contacts:write") { + t.Errorf("error = %q, want a fragment wrapped field checked like any other", got) } } @@ -319,8 +319,8 @@ func TestScopeGateSeesThroughAnInlineFragment(t *testing.T) { answered := gatedAsToken(t, `{ ... on Query { contacts } }`, apitoken.ParseScopes("tasks:read")) - if got := refusalOf(t, answered); !strings.Contains(got, "contacts:read") { - t.Errorf("refusal = %q, want an inline fragment checked like any other", got) + if got := errorOf(t, answered); !strings.Contains(got, "contacts:read") { + t.Errorf("error = %q, want an inline fragment checked like any other", got) } } @@ -331,8 +331,8 @@ func TestScopeGateSeesThroughANestedFragmentSpread(t *testing.T) { `{ ...outer } fragment outer on Query { ...inner } fragment inner on Query { contacts }`, apitoken.ParseScopes("tasks:read")) - if got := refusalOf(t, answered); !strings.Contains(got, "contacts:read") { - t.Errorf("refusal = %q, want nesting to be no escape", got) + if got := errorOf(t, answered); !strings.Contains(got, "contacts:read") { + t.Errorf("error = %q, want nesting to be no escape", got) } } @@ -341,8 +341,8 @@ func TestScopeGateRefusesEveryFieldOfAMixedOperation(t *testing.T) { answered := gatedAsToken(t, `{ contacts me }`, apitoken.ParseScopes("tasks:read")) - if got := refusalOf(t, answered); !strings.Contains(got, "contacts:read") { - t.Errorf("refusal = %q, want the unheld field to refuse the whole operation", got) + if got := errorOf(t, answered); !strings.Contains(got, "contacts:read") { + t.Errorf("error = %q, want the unheld field to refuse the whole operation", got) } } @@ -371,8 +371,8 @@ func TestScopeGateRefusesTokenManagementToAWildcardToken(t *testing.T) { answered := gatedAsToken(t, `{ apiTokens }`, apitoken.Full()) - if got := refusalOf(t, answered); !strings.Contains(got, "tokens:read") { - t.Errorf("refusal = %q, want token management to need a session", got) + if got := errorOf(t, answered); !strings.Contains(got, "tokens:read") { + t.Errorf("error = %q, want token management to need a session", got) } } @@ -381,8 +381,8 @@ func TestScopeGateRefusesAFieldTheSchemaDoesNotScope(t *testing.T) { answered := gatedAsToken(t, `{ unscoped }`, apitoken.Full()) - if got := refusalOf(t, answered); !strings.Contains(got, "unscoped") { - t.Errorf("refusal = %q, want an unscoped field refused, the gate fails closed", got) + if got := errorOf(t, answered); !strings.Contains(got, "unscoped") { + t.Errorf("error = %q, want an unscoped field refused, the gate fails closed", got) } } @@ -395,6 +395,6 @@ func TestScopeGateRefusesAnOperationItCannotRead(t *testing.T) { answered := graphres.ScopeGate(graphres.NewScopeMap(loadScopedSchema(t)))(ctx, passed)(ctx) if len(answered.Errors) != 1 { - t.Errorf("errors = %v, want a refusal when there is no operation to read", answered.Errors) + t.Errorf("errors = %v, want an error when there is no operation to read", answered.Errors) } } diff --git a/internal/graphres/tokens_test.go b/internal/graphres/tokens_test.go index 9ae0cc7..9852a35 100644 --- a/internal/graphres/tokens_test.go +++ b/internal/graphres/tokens_test.go @@ -127,7 +127,7 @@ func TestAPITokenCreateRefusesAnAreaNoSchemaDeclares(t *testing.T) { t.Errorf("code = %q, want VALIDATION", got) } if !strings.Contains(string(answered.Errors), `contact`) { - t.Errorf("errors = %s, want the refusal to name the area", answered.Errors) + t.Errorf("errors = %s, want the error to name the area", answered.Errors) } held, err := tokens.ListForUser(t.Context(), owner) if err != nil { @@ -340,6 +340,6 @@ func TestAPITokenRevokeRefusesSomeoneElsesToken(t *testing.T) { t.Fatalf("RawPost() error = %v, want nil", err) } if len(answered.Errors) == 0 { - t.Error("errors = none, want a refusal for a token the caller does not own") + t.Error("errors = none, want an error for a token the caller does not own") } } diff --git a/internal/mcp/contacts_test.go b/internal/mcp/contacts_test.go index 300c289..1c4856b 100644 --- a/internal/mcp/contacts_test.go +++ b/internal/mcp/contacts_test.go @@ -189,7 +189,7 @@ func TestContactRefusesAnIdNoContactHolds(t *testing.T) { } } -func TestContactReportsAGraphRefusal(t *testing.T) { +func TestContactReportsAGraphError(t *testing.T) { t.Parallel() run := &tools{graph: answering(`{"errors":[{"message":"scalar: invalid value"}]}`)} @@ -197,11 +197,11 @@ func TestContactReportsAGraphRefusal(t *testing.T) { _, _, err := run.contact(t.Context(), ContactInput{ContactID: "not-a-uuid"}) if err == nil || !strings.Contains(err.Error(), "invalid value") { - t.Errorf("error = %v, want the refusal", err) + t.Errorf("error = %v, want the error", err) } } -func TestContactsReportsAGraphRefusal(t *testing.T) { +func TestContactsReportsAGraphError(t *testing.T) { t.Parallel() run := &tools{graph: answering(`{"errors":[{"message":"refused","extensions":{"code":"VALIDATION"}}]}`)} @@ -209,6 +209,6 @@ func TestContactsReportsAGraphRefusal(t *testing.T) { _, _, err := run.contacts(t.Context(), ContactsInput{}) if err == nil || !strings.Contains(err.Error(), "refused") { - t.Errorf("error = %v, want the refusal", err) + t.Errorf("error = %v, want the error", err) } } diff --git a/internal/mcp/executor_test.go b/internal/mcp/executor_test.go index d131a40..d84a50e 100644 --- a/internal/mcp/executor_test.go +++ b/internal/mcp/executor_test.go @@ -70,7 +70,7 @@ func TestRunReportsTheFirstGraphError(t *testing.T) { err := run.execute(t.Context(), "{ task }", nil, &struct{}{}) if err == nil { - t.Fatal("execute() error = nil, want the graph refusal") + t.Fatal("execute() error = nil, want the graph error") } if !strings.Contains(err.Error(), "task: not found") { t.Errorf("error = %v, want the message", err) @@ -83,11 +83,11 @@ func TestRunReportsTheFirstGraphError(t *testing.T) { func TestRunReportsAGraphErrorWithoutACode(t *testing.T) { t.Parallel() - run := &tools{graph: answering(`{"errors":[{"message":"plain refusal"}]}`)} + run := &tools{graph: answering(`{"errors":[{"message":"plain error"}]}`)} err := run.execute(t.Context(), "{ task }", nil, &struct{}{}) - if err == nil || err.Error() != "plain refusal" { + if err == nil || err.Error() != "plain error" { t.Errorf("error = %v, want the bare message", err) } } diff --git a/internal/mcp/tasks_test.go b/internal/mcp/tasks_test.go index f215138..367b293 100644 --- a/internal/mcp/tasks_test.go +++ b/internal/mcp/tasks_test.go @@ -139,7 +139,7 @@ func TestTasksSendsOnlyTheNamedFilter(t *testing.T) { } } -func TestTasksReportsAGraphRefusal(t *testing.T) { +func TestTasksReportsAGraphError(t *testing.T) { t.Parallel() run := &tools{graph: answering( @@ -148,7 +148,7 @@ func TestTasksReportsAGraphRefusal(t *testing.T) { _, _, err := run.tasks(t.Context(), TasksInput{Date: "2026-08-11", DueBefore: "2026-08-11"}) if err == nil { - t.Fatal("tasks() error = nil, want the refusal") + t.Fatal("tasks() error = nil, want the error") } if !strings.Contains(err.Error(), "VALIDATION") { t.Errorf("error = %v, want the validation code", err) diff --git a/internal/mcp/workload_test.go b/internal/mcp/workload_test.go index 2a32011..1dc547d 100644 --- a/internal/mcp/workload_test.go +++ b/internal/mcp/workload_test.go @@ -61,7 +61,7 @@ func TestWorkloadCountsTheAnswer(t *testing.T) { } } -func TestWorkloadReportsAGraphRefusal(t *testing.T) { +func TestWorkloadReportsAGraphError(t *testing.T) { t.Parallel() run := &tools{graph: answering(`{"errors":[{"message":"refused"}]}`)} @@ -69,7 +69,7 @@ func TestWorkloadReportsAGraphRefusal(t *testing.T) { _, _, err := run.workload(t.Context(), WorkloadInput{}) if err == nil || !strings.Contains(err.Error(), "refused") { - t.Errorf("error = %v, want the refusal", err) + t.Errorf("error = %v, want the error", err) } } diff --git a/internal/postgres/tokens_test.go b/internal/postgres/tokens_test.go index 69db632..9c159b4 100644 --- a/internal/postgres/tokens_test.go +++ b/internal/postgres/tokens_test.go @@ -319,7 +319,7 @@ func TestMigrationRefusesToGrantFullScopeToATokenMintedAfterwards(t *testing.T) uuid.Must(uuid.NewV7()), uuid.Must(uuid.NewV7())) if err == nil { - t.Error("insert without scopes error = nil, want a refusal, the grandfather default is spent") + t.Error("insert without scopes error = nil, want an error, the grandfather default is spent") } } diff --git a/internal/server/graphql_auth_test.go b/internal/server/graphql_auth_test.go index cb4424c..4023ed0 100644 --- a/internal/server/graphql_auth_test.go +++ b/internal/server/graphql_auth_test.go @@ -253,7 +253,7 @@ func TestGraphRefusesUserManagementToAMemberSession(t *testing.T) { body := decodeBody[graphResponse](t, recorder) if len(body.Errors) != 1 { - t.Fatalf("errors = %v, want one refusal, a member does not manage users", body.Errors) + t.Fatalf("errors = %v, want one error, a member does not manage users", body.Errors) } if got, want := body.Errors[0].Message, "admin required"; got != want { t.Errorf("message = %q, want %q", got, want) diff --git a/internal/server/scopegate_test.go b/internal/server/scopegate_test.go index da6cdac..a8ce3aa 100644 --- a/internal/server/scopegate_test.go +++ b/internal/server/scopegate_test.go @@ -63,7 +63,7 @@ func TestReadScopedTokenIsRefusedAContactWrite(t *testing.T) { answered := decodeBody[gateAnswer](t, recorder) if len(answered.Errors) != 1 { - t.Fatalf("errors = %v, want exactly one refusal", answered.Errors) + t.Fatalf("errors = %v, want exactly one error", answered.Errors) } if got := answered.Errors[0].Extensions["scope"]; got != "contacts:write" { t.Errorf("scope = %v, want contacts:write named by the real schema", got) @@ -83,7 +83,7 @@ func TestScopedTokenStaysInsideItsGrantedAreas(t *testing.T) { answered := decodeBody[gateAnswer](t, recorder) if len(answered.Errors) != 1 { - t.Fatalf("errors = %v, want exactly one refusal", answered.Errors) + t.Fatalf("errors = %v, want exactly one error", answered.Errors) } if got := answered.Errors[0].Extensions["scope"]; got != "webhooks:read" { t.Errorf("scope = %v, want webhooks:read", got) diff --git a/plugins/fields/frontend/test/fields.test.tsx b/plugins/fields/frontend/test/fields.test.tsx index 3e813cb..243d150 100644 --- a/plugins/fields/frontend/test/fields.test.tsx +++ b/plugins/fields/frontend/test/fields.test.tsx @@ -201,7 +201,7 @@ test('a failed archive is reported', async () => { ) }) -test('a validation refusal is reported word for word', async () => { +test('a validation error is reported word for word', async () => { serveFields([]) server.use( graphql.mutation('DefineField', () => diff --git a/plugins/fields/values_internal_test.go b/plugins/fields/values_internal_test.go index 425c590..4543fb0 100644 --- a/plugins/fields/values_internal_test.go +++ b/plugins/fields/values_internal_test.go @@ -76,15 +76,15 @@ func TestWriteContactFieldsReportsAStoreFailure(t *testing.T) { } } -func TestRefusalForReportsAClosedPool(t *testing.T) { +func TestErrorForReportsAClosedPool(t *testing.T) { t.Parallel() p := newClosedPlugin(t) - err := p.store.refusalFor(t.Context(), defined(t, "birthDate", "DATE")) + err := p.store.errorFor(t.Context(), defined(t, "birthDate", "DATE")) if err == nil { - t.Error("refusalFor() error = nil, want the closed pool reported") + t.Error("errorFor() error = nil, want the closed pool reported") } } diff --git a/plugins/importer/commit_test.go b/plugins/importer/commit_test.go index 48317c0..8aa1770 100644 --- a/plugins/importer/commit_test.go +++ b/plugins/importer/commit_test.go @@ -362,7 +362,7 @@ func TestCommitRefusesAnImportThatIsAlreadyCommitted(t *testing.T) { _, err := commitImport(t, p, id) - if code := refusalCode(t, err); code != "CONFLICT" { + if code := errorCode(t, err); code != "CONFLICT" { t.Fatalf("replay code = %q, want CONFLICT", code) } if len(events.names) != 1 { @@ -378,7 +378,7 @@ func TestCommitRefusesAnImportWithoutAMapping(t *testing.T) { _, err := commitImport(t, p, id) - if code := refusalCode(t, err); code != "VALIDATION" { + if code := errorCode(t, err); code != "VALIDATION" { t.Fatalf("code = %q, want VALIDATION", code) } } diff --git a/plugins/importer/provider_test.go b/plugins/importer/provider_test.go index 9b681a6..364ff8d 100644 --- a/plugins/importer/provider_test.go +++ b/plugins/importer/provider_test.go @@ -293,7 +293,7 @@ func TestSetMappingRefusesAFieldNoProviderServes(t *testing.T) { err := mapNameEmailAndField(t, p, id, "neverDefined") - if got := refusalCode(t, err); got != "VALIDATION" { + if got := errorCode(t, err); got != "VALIDATION" { t.Errorf("code = %q, want VALIDATION", got) } } @@ -422,7 +422,7 @@ func TestCommitRefusesAMappingNamingAVanishedField(t *testing.T) { _, err := commitImport(t, p, id) - if got := refusalCode(t, err); got != "VALIDATION" { + if got := errorCode(t, err); got != "VALIDATION" { t.Fatalf("code = %q, want VALIDATION", got) } if !strings.Contains(err.Error(), "birthDate") { diff --git a/plugins/importer/stage_test.go b/plugins/importer/stage_test.go index 9ec7985..4ef2f86 100644 --- a/plugins/importer/stage_test.go +++ b/plugins/importer/stage_test.go @@ -54,7 +54,7 @@ func mapNameAndEmail(t *testing.T, p *importer.Plugin, id uuid.UUID) { } } -// commitImport commits one import, returning its counts or the refusal. +// commitImport commits one import, returning its counts or the error. func commitImport( t *testing.T, p *importer.Plugin, id uuid.UUID, ) (*model.ImportCommitPayload, error) { @@ -62,7 +62,7 @@ func commitImport( return p.MutationResolvers().ImportCommit(t.Context(), id) } -// mustCommit commits one import, failing the test on any refusal. +// mustCommit commits one import, failing the test on any error. func mustCommit(t *testing.T, p *importer.Plugin, id uuid.UUID) *model.ImportCommitPayload { t.Helper() committed, err := commitImport(t, p, id) @@ -72,8 +72,8 @@ func mustCommit(t *testing.T, p *importer.Plugin, id uuid.UUID) *model.ImportCom return committed } -// refusalCode returns the graph code an error carries. -func refusalCode(t *testing.T, err error) string { +// errorCode returns the graph code an error carries. +func errorCode(t *testing.T, err error) string { t.Helper() var refused sdk.GraphError if !errors.As(err, &refused) { diff --git a/plugins/whatsapp/send_test.go b/plugins/whatsapp/send_test.go index 32e3e57..8537114 100644 --- a/plugins/whatsapp/send_test.go +++ b/plugins/whatsapp/send_test.go @@ -86,7 +86,7 @@ func sendMessage( return p.MutationResolvers().WhatsAppSendMessage(t.Context(), conversationID, content) } -// mustSend sends one reply, failing the test on any refusal. +// mustSend sends one reply, failing the test on any error. func mustSend( t *testing.T, p *whatsapp.Plugin, conversationID uuid.UUID, content string, ) *model.WhatsAppMessage { @@ -98,8 +98,8 @@ func mustSend( return sent } -// sendRefusalCode returns the graph code a refused send carries. -func sendRefusalCode(t *testing.T, err error) string { +// sendErrorCode returns the graph code a refused send carries. +func sendErrorCode(t *testing.T, err error) string { t.Helper() var refused sdk.GraphError if !errors.As(err, &refused) { @@ -200,7 +200,7 @@ func TestSendMessageReportsUpstreamFailure(t *testing.T) { _, err := sendMessage(t, p, conversationID, "hey") - if code := sendRefusalCode(t, err); code != "UPSTREAM" { + if code := sendErrorCode(t, err); code != "UPSTREAM" { t.Fatalf("code = %q, want UPSTREAM", code) } messages := listMessages(t, p, conversationID) @@ -245,7 +245,7 @@ func TestSendMessageRejectsMisconfiguredGraphURL(t *testing.T) { _, err := sendMessage(t, p, conversationID, "hey") - if code := sendRefusalCode(t, err); code != "UPSTREAM" { + if code := sendErrorCode(t, err); code != "UPSTREAM" { t.Fatalf("code = %q, want UPSTREAM", code) } } From 82a37053afd51cf9ca75b6e51a4f9fd90d2632a0 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 23 Aug 2026 16:27:15 +0200 Subject: [PATCH 5/7] test(e2e): move the step text and its feature line together --- test/features/connector_walk_test.go | 4 ++-- test/features/features/tokens.feature | 2 +- test/features/steps_contacts_test.go | 2 +- test/features/steps_fields_values_test.go | 4 ++-- test/features/steps_import_fields_test.go | 2 +- test/features/steps_roles_test.go | 12 ++++++------ test/features/steps_session_test.go | 6 +++--- test/features/steps_tasks_test.go | 2 +- test/features/steps_tokens_mcp_test.go | 2 +- test/features/steps_tokens_test.go | 20 ++++++++++---------- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/test/features/connector_walk_test.go b/test/features/connector_walk_test.go index 7bd5743..00cac04 100644 --- a/test/features/connector_walk_test.go +++ b/test/features/connector_walk_test.go @@ -43,7 +43,7 @@ func TestEngineScopesReachEveryDocumentedOperation(t *testing.T) { if err := w.postGraphScoped(ctx, document); err != nil { t.Fatalf("posting %s: %v", step, err) } - if err := w.answeredWithoutRefusal(); err != nil { + if err := w.answeredWithoutError(); err != nil { t.Errorf("%s was refused under %q: %v", step, engineScopes, err) } } @@ -117,7 +117,7 @@ func TestTheGateChecksRootFieldsOnlyNotNestedTraversal(t *testing.T) { t.Fatalf("posting the traversal: %v", err) } - if err := w.answeredWithoutRefusal(); err != nil { + if err := w.answeredWithoutError(); err != nil { t.Fatalf("the traversal was refused: %v", err) } if !strings.Contains(string(w.answered), "Call the supplier") { diff --git a/test/features/features/tokens.feature b/test/features/features/tokens.feature index 818c5b0..ee25037 100644 --- a/test/features/features/tokens.feature +++ b/test/features/features/tokens.feature @@ -53,4 +53,4 @@ Feature: API tokens are scoped and mortal Given the user holds a token scoped to "contacts:read" And an MCP session connected with that token When the agent calls the tool listing today's tasks - Then the tool answers the refusal naming "tasks:read" + Then the tool answers the error naming "tasks:read" diff --git a/test/features/steps_contacts_test.go b/test/features/steps_contacts_test.go index e295b58..b20c290 100644 --- a/test/features/steps_contacts_test.go +++ b/test/features/steps_contacts_test.go @@ -179,7 +179,7 @@ func registerContactSteps(sc *godog.ScenarioContext, t *testing.T) { return fmt.Errorf("the tool succeeded, want it refused") } if !strings.Contains(contentText(w.called), "NOT_FOUND") { - return fmt.Errorf("failure = %q, want the graph's not found refusal", contentText(w.called)) + return fmt.Errorf("failure = %q, want the graph's not found error", contentText(w.called)) } return nil }) diff --git a/test/features/steps_fields_values_test.go b/test/features/steps_fields_values_test.go index a2428d5..2e56c05 100644 --- a/test/features/steps_fields_values_test.go +++ b/test/features/steps_fields_values_test.go @@ -110,7 +110,7 @@ func registerFieldsValuesSteps(sc *godog.ScenarioContext, t *testing.T) { return err } if !strings.Contains(string(w.answered), name) { - return fmt.Errorf("the refusal does not name %q, answered %s", name, w.answered) + return fmt.Errorf("the error does not name %q, answered %s", name, w.answered) } return nil }) @@ -121,7 +121,7 @@ func registerFieldsValuesSteps(sc *godog.ScenarioContext, t *testing.T) { return err } if !strings.Contains(string(w.answered), "does not match the kind") { - return fmt.Errorf("the refusal does not name the kind, answered %s", w.answered) + return fmt.Errorf("the error does not name the kind, answered %s", w.answered) } return nil }) diff --git a/test/features/steps_import_fields_test.go b/test/features/steps_import_fields_test.go index c9f490b..2511db2 100644 --- a/test/features/steps_import_fields_test.go +++ b/test/features/steps_import_fields_test.go @@ -246,7 +246,7 @@ func registerImportFieldsSteps(sc *godog.ScenarioContext, t *testing.T) { return err } if !strings.Contains(string(w.answered), name) { - return fmt.Errorf("the refusal does not name %q, answered %s", name, w.answered) + return fmt.Errorf("the error does not name %q, answered %s", name, w.answered) } return nil }) diff --git a/test/features/steps_roles_test.go b/test/features/steps_roles_test.go index a3b72ef..42a4190 100644 --- a/test/features/steps_roles_test.go +++ b/test/features/steps_roles_test.go @@ -78,7 +78,7 @@ func registerRoleSteps(sc *godog.ScenarioContext, t *testing.T) { registerRoleTokenSteps(sc) registerRoleWriteSteps(sc) registerTokenOperations(sc) - registerRefusalSteps(sc) + registerErrorSteps(sc) } // registerRoleWriteSteps binds the steps standing a user in another tier. @@ -97,7 +97,7 @@ func registerRoleWriteSteps(sc *godog.ScenarioContext) { if err := w.postGraphAsSession(ctx, settingUserRole(w.memberID, tier)); err != nil { return err } - return w.answeredWithoutRefusal() + return w.answeredWithoutError() }) sc.When(`^the admin's session demotes itself to "([^"]*)"$`, func(ctx context.Context, tier string) error { @@ -138,7 +138,7 @@ func (w *world) refusedAsOwnRole() error { return err } if len(parsed.Errors) != 1 { - return fmt.Errorf("errors = %v, want exactly one refusal", parsed.Errors) + return fmt.Errorf("errors = %v, want exactly one error", parsed.Errors) } refused := parsed.Errors[0] if code := refused.Extensions["code"]; code != "VALIDATION" { @@ -187,11 +187,11 @@ func registerMemberSteps(sc *godog.ScenarioContext) { }) sc.Then(`^the contact is answered$`, func(ctx context.Context) error { - return worldFrom(ctx).answeredWithoutRefusal() + return worldFrom(ctx).answeredWithoutError() }) sc.Then(`^the task is answered$`, func(ctx context.Context) error { - return worldFrom(ctx).answeredWithoutRefusal() + return worldFrom(ctx).answeredWithoutError() }) sc.When(`^the member's session disables another user$`, func(ctx context.Context) error { @@ -212,7 +212,7 @@ func registerMemberSteps(sc *godog.ScenarioContext) { if err := w.postGraphAsSession(ctx, disablingUser(colleague)); err != nil { return err } - return w.answeredWithoutRefusal() + return w.answeredWithoutError() }) } diff --git a/test/features/steps_session_test.go b/test/features/steps_session_test.go index c22d08b..f7a3e1e 100644 --- a/test/features/steps_session_test.go +++ b/test/features/steps_session_test.go @@ -119,9 +119,9 @@ func registerSessionSteps(sc *godog.ScenarioContext, t *testing.T) { if w.connErr == nil { return fmt.Errorf("the connection succeeded, want it refused") } - refusal := w.connErr.Error() - if !strings.Contains(refusal, "401") && !strings.Contains(refusal, "Unauthorized") { - return fmt.Errorf("error = %v, want an unauthorized refusal", w.connErr) + message := w.connErr.Error() + if !strings.Contains(message, "401") && !strings.Contains(message, "Unauthorized") { + return fmt.Errorf("error = %v, want it refused as unauthorized", w.connErr) } return nil }) diff --git a/test/features/steps_tasks_test.go b/test/features/steps_tasks_test.go index dee6cfe..45b1138 100644 --- a/test/features/steps_tasks_test.go +++ b/test/features/steps_tasks_test.go @@ -172,7 +172,7 @@ func registerTaskSteps(sc *godog.ScenarioContext, t *testing.T) { return fmt.Errorf("the tool succeeded, want it refused") } if !strings.Contains(contentText(w.called), "VALIDATION") { - return fmt.Errorf("failure = %q, want a validation refusal", contentText(w.called)) + return fmt.Errorf("failure = %q, want it refused as invalid", contentText(w.called)) } return nil }) diff --git a/test/features/steps_tokens_mcp_test.go b/test/features/steps_tokens_mcp_test.go index ab14289..05c722f 100644 --- a/test/features/steps_tokens_mcp_test.go +++ b/test/features/steps_tokens_mcp_test.go @@ -27,7 +27,7 @@ func registerTokenConnectorSteps(sc *godog.ScenarioContext) { return worldFrom(ctx).callTool(ctx, "list_my_tasks", map[string]any{}) }) - sc.Then(`^the tool answers the refusal naming "([^"]*)"$`, func(ctx context.Context, scope string) error { + sc.Then(`^the tool answers the error naming "([^"]*)"$`, func(ctx context.Context, scope string) error { w := worldFrom(ctx) if w.called == nil { return fmt.Errorf("the agent called no tool") diff --git a/test/features/steps_tokens_test.go b/test/features/steps_tokens_test.go index d3a243e..d2f008b 100644 --- a/test/features/steps_tokens_test.go +++ b/test/features/steps_tokens_test.go @@ -57,8 +57,8 @@ func (w *world) scopeErrors() (scopeAnswer, error) { return parsed, nil } -// answeredWithoutRefusal reports whether the last operation carried no error. -func (w *world) answeredWithoutRefusal() error { +// answeredWithoutError reports whether the last operation carried no error. +func (w *world) answeredWithoutError() error { parsed, err := w.scopeErrors() if err != nil { return err @@ -69,14 +69,14 @@ func (w *world) answeredWithoutRefusal() error { return nil } -// refusedWith reports whether the last operation carried one refusal reading message and naming scope. +// refusedWith reports whether the last operation carried one error reading message and naming scope. func (w *world) refusedWith(message, scope string) error { parsed, err := w.scopeErrors() if err != nil { return err } if len(parsed.Errors) != 1 { - return fmt.Errorf("errors = %v, want exactly one refusal", parsed.Errors) + return fmt.Errorf("errors = %v, want exactly one error", parsed.Errors) } refused := parsed.Errors[0] if refused.Message != message { @@ -181,18 +181,18 @@ func registerTokenOperations(sc *godog.ScenarioContext) { // registerTokenOutcomes binds the outcomes a scoped operation answers with. func registerTokenOutcomes(sc *godog.ScenarioContext) { sc.Then(`^the list is answered$`, func(ctx context.Context) error { - return worldFrom(ctx).answeredWithoutRefusal() + return worldFrom(ctx).answeredWithoutError() }) sc.Then(`^the task is answered$`, func(ctx context.Context) error { - return worldFrom(ctx).answeredWithoutRefusal() + return worldFrom(ctx).answeredWithoutError() }) sc.Then(`^the contact is answered$`, func(ctx context.Context) error { - return worldFrom(ctx).answeredWithoutRefusal() + return worldFrom(ctx).answeredWithoutError() }) - registerRefusalSteps(sc) + registerErrorSteps(sc) sc.Then(`^the request is refused as an invalid token$`, func(ctx context.Context) error { w := worldFrom(ctx) @@ -206,8 +206,8 @@ func registerTokenOutcomes(sc *godog.ScenarioContext) { }) } -// registerRefusalSteps binds the scope refusal both the token and the role features assert. -func registerRefusalSteps(sc *godog.ScenarioContext) { +// registerErrorSteps binds the scope error both the token and the role features assert. +func registerErrorSteps(sc *godog.ScenarioContext) { sc.Then(`^the operation is refused as unauthorized naming "([^"]*)"$`, func(ctx context.Context, scope string) error { return worldFrom(ctx).refusedNaming(scope) From 1658713a94e13804217f24c41b062e8a656298c3 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 23 Aug 2026 16:27:32 +0200 Subject: [PATCH 6/7] docs: say an operation was refused rather than name a refusal --- docs/src/content/docs/reference/graphql-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/reference/graphql-api.md b/docs/src/content/docs/reference/graphql-api.md index fda5954..d0467e0 100644 --- a/docs/src/content/docs/reference/graphql-api.md +++ b/docs/src/content/docs/reference/graphql-api.md @@ -147,8 +147,8 @@ fell short of. Holding the admin role is not what the field asks for, holding that capability is, and a plugin-declared role holding it passes just as well. The `scope` extension still names what the field wanted, so a caller always -learns which area an operation acts in. A refusal about a token's scopes carries -no `capability`, so the two halves stay distinguishable. Minting a wider token +learns which area an operation acts in. An operation refused over a token's +scopes carries no `capability`, so the two halves stay distinguishable. Minting a wider token does not help here. A token cannot carry more authority than the user it acts as. From 11d3958d7671fc43c0133d71f34dc90623922447 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 23 Aug 2026 16:45:23 +0200 Subject: [PATCH 7/7] docs(whatsapp): say a failed send rather than a refused one --- plugins/whatsapp/send_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/whatsapp/send_test.go b/plugins/whatsapp/send_test.go index 8537114..b3379ec 100644 --- a/plugins/whatsapp/send_test.go +++ b/plugins/whatsapp/send_test.go @@ -98,7 +98,7 @@ func mustSend( return sent } -// sendErrorCode returns the graph code a refused send carries. +// sendErrorCode returns the graph code a failed send carries. func sendErrorCode(t *testing.T, err error) string { t.Helper() var refused sdk.GraphError