Present native PromptKit operation failures
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
# Invocation reporting audit remediation
|
# Invocation reporting audit remediation
|
||||||
|
|
||||||
Status: five open remediation findings and two efficiency opportunities for
|
Status: remediation in progress; 1 of 7 findings complete.
|
||||||
consideration. Findings 4 and 5 came from the interaction audit; findings 6 and 7
|
|
||||||
came from the algorithmic audit.
|
|
||||||
|
|
||||||
## Review scope
|
## Review scope
|
||||||
|
|
||||||
@@ -65,7 +63,13 @@ findings 1–5. It made no implementation changes and ran no tests or benchmarks
|
|||||||
|
|
||||||
## 1. Preserve native PromptKit operation failure explanations
|
## 1. Preserve native PromptKit operation failure explanations
|
||||||
|
|
||||||
Priority: P2. Status: open.
|
Priority: P2. Status: complete.
|
||||||
|
|
||||||
|
- [x] Completed: bundled native operation failures now supply terminal explanations
|
||||||
|
without printing arbitrary application causes. Explicit provider context and
|
||||||
|
independent warnings retain their presentation paths. Native-error forwarding
|
||||||
|
and single-presentation regressions pass in ordinary and quiet modes.
|
||||||
|
Validation: `GOTOOLCHAIN=local go test ./...` in `integrations/ckprompt`.
|
||||||
|
|
||||||
### Finding and evidence
|
### Finding and evidence
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ type Suite[I, S any] struct {
|
|||||||
// PromptKit unchanged; the loader receives the seed's original configuration
|
// PromptKit unchanged; the loader receives the seed's original configuration
|
||||||
// selection. A nil builder forwards the seed request unchanged. A builder
|
// selection. A nil builder forwards the seed request unchanged. A builder
|
||||||
// failure prevents engine loading and preserves its supported error identity.
|
// failure prevents engine loading and preserves its supported error identity.
|
||||||
// Native validation errors are returned with supported identity, and
|
// Native validation errors supply terminal explanations and are returned with
|
||||||
|
// supported identity, and
|
||||||
// configuration diagnostics and terminal context from engine loading remain
|
// configuration diagnostics and terminal context from engine loading remain
|
||||||
// separate in the acquisition. A nil loader is retained as a suite wiring
|
// separate in the acquisition. A nil loader is retained as a suite wiring
|
||||||
// issue and does not install a validation callback. Callback references and
|
// issue and does not install a validation callback. Callback references and
|
||||||
@@ -192,6 +193,7 @@ func WithRequestValidation[I, S any](
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := engine.ValidateRequest(ctx, concreteRequest); err != nil {
|
if err := engine.ValidateRequest(ctx, concreteRequest); err != nil {
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
if err := promptKitContextError(ctx); err != nil {
|
if err := promptKitContextError(ctx); err != nil {
|
||||||
@@ -209,6 +211,22 @@ type promptKitAcquisition struct {
|
|||||||
explanation commandkit.Explanation
|
explanation commandkit.Explanation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withOperationError deliberately presents a native PromptKit operation error.
|
||||||
|
// Loader and request-builder errors remain application-owned. Native structured
|
||||||
|
// context takes precedence when an acquisition already supplies it.
|
||||||
|
func (messages promptKitAcquisition) withOperationError(err error) promptKitAcquisition {
|
||||||
|
if err == nil || messages.explanation.Message != "" {
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
for _, detail := range messages.explanation.Details {
|
||||||
|
if detail != nil {
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages.explanation.Message = err.Error()
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigScope adapts PromptKit's captured configuration inspection to the
|
// ConfigScope adapts PromptKit's captured configuration inspection to the
|
||||||
// standard configuration commands, including native comparison when the scope
|
// standard configuration commands, including native comparison when the scope
|
||||||
// declares Diff. The loader is not called during declaration construction or
|
// declares Diff. The loader is not called during declaration construction or
|
||||||
@@ -266,6 +284,7 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
|
|||||||
}
|
}
|
||||||
|
|
||||||
differences, err := leftInspection.Diff(rightInspection)
|
differences, err := leftInspection.Diff(rightInspection)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err)
|
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err)
|
||||||
}
|
}
|
||||||
@@ -294,7 +313,8 @@ func promptKitContextError(ctx context.Context) error {
|
|||||||
// listing or inspection method. Applications can narrow, wrap, or replace these
|
// listing or inspection method. Applications can narrow, wrap, or replace these
|
||||||
// callbacks before Command captures them. A nil loader is retained as an
|
// callbacks before Command captures them. A nil loader is retained as an
|
||||||
// adapter issue. Each callback returns independent loader diagnostics and
|
// adapter issue. Each callback returns independent loader diagnostics and
|
||||||
// terminal context alongside the native operation's value and error.
|
// terminal context alongside the native operation's value and error. Native
|
||||||
|
// operation errors supply terminal explanations when structured context is absent.
|
||||||
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
|
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
|
||||||
if load == nil {
|
if load == nil {
|
||||||
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
|
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
|
||||||
@@ -307,6 +327,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
prompts, err := engine.ListPrompts(ctx, request.Filter)
|
prompts, err := engine.ListPrompts(ctx, request.Filter)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
ListProfiles: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
ListProfiles: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
||||||
@@ -315,6 +336,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
profiles, err := engine.ListProfiles(ctx, request.Filter)
|
profiles, err := engine.ListProfiles(ctx, request.Filter)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
ListBackends: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
ListBackends: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
||||||
@@ -323,6 +345,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
backends, err := engine.ListBackends(ctx, request.Filter)
|
backends, err := engine.ListBackends(ctx, request.Filter)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[[]string]{Value: backends, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]string]{Value: backends, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
ListNamespaces: func(ctx context.Context, request NamespaceRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
ListNamespaces: func(ctx context.Context, request NamespaceRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
||||||
@@ -331,6 +354,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
namespaces, err := engine.ListNamespaces(ctx, request.Kinds...)
|
namespaces, err := engine.ListNamespaces(ctx, request.Kinds...)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
ListCollections: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
|
ListCollections: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
|
||||||
@@ -339,6 +363,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
collections, err := engine.ListCollections(ctx)
|
collections, err := engine.ListCollections(ctx)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||||
@@ -347,6 +372,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
inspection, err := engine.InspectPrompt(ctx, request.ID)
|
inspection, err := engine.InspectPrompt(ctx, request.ID)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||||
@@ -355,6 +381,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
inspection, err := engine.InspectProfile(ctx, request.ID)
|
inspection, err := engine.InspectProfile(ctx, request.ID)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
InspectRequirements: func(ctx context.Context, request RequirementsRequest, input I, state S) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
|
InspectRequirements: func(ctx context.Context, request RequirementsRequest, input I, state S) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
|
||||||
@@ -363,6 +390,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
inspection, err := engine.InspectRequirements(ctx, request.Inspection)
|
inspection, err := engine.InspectRequirements(ctx, request.Inspection)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -390,6 +418,7 @@ func loadPromptKitEngine[I, S any](
|
|||||||
}
|
}
|
||||||
inspection, err := engine.InspectConfiguration()
|
inspection, err := engine.InspectConfiguration()
|
||||||
messages := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
|
messages := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, inspection, messages, err
|
return nil, inspection, messages, err
|
||||||
}
|
}
|
||||||
|
|||||||
80
integrations/ckprompt/native_explanation_test.go
Normal file
80
integrations/ckprompt/native_explanation_test.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package ckprompt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||||
|
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBundledNativeFailuresReachTerminalExplanation(t *testing.T) {
|
||||||
|
engine := adapterEngine(t)
|
||||||
|
load := func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) {
|
||||||
|
return engine, nil
|
||||||
|
}
|
||||||
|
for _, operation := range []string{"validation", "inspection"} {
|
||||||
|
for _, mode := range []string{"ordinary", "quiet"} {
|
||||||
|
t.Run(operation+"/"+mode, func(t *testing.T) {
|
||||||
|
suite := FromEngine(load)
|
||||||
|
suite = WithRequestValidation(suite, load, nil)
|
||||||
|
var nativeErr error
|
||||||
|
args := []string{"promptkit", "requirements", "validate", "briefing"}
|
||||||
|
if operation == "validation" {
|
||||||
|
original := suite.ValidateRequest
|
||||||
|
suite.ValidateRequest = func(ctx context.Context, request ValidationRequest, input, state struct{}) (commandkit.Acquired[struct{}], error) {
|
||||||
|
acquired, err := original(ctx, request, input, state)
|
||||||
|
nativeErr = err
|
||||||
|
acquired.Diagnostics = append(acquired.Diagnostics, commandkit.MessageDiagnostic(commandkit.SeverityWarning, "independent fixture warning"))
|
||||||
|
return acquired, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = []string{"promptkit", "show", "prompt", "missing"}
|
||||||
|
original := suite.InspectPrompt
|
||||||
|
suite.InspectPrompt = func(ctx context.Context, request PromptRequest, input, state struct{}) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||||
|
acquired, err := original(ctx, request, input, state)
|
||||||
|
nativeErr = err
|
||||||
|
return acquired, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mode == "quiet" {
|
||||||
|
args = append(args, "--quiet")
|
||||||
|
}
|
||||||
|
var diagnostics bytes.Buffer
|
||||||
|
got := newPromptTestApp(t, Command(suite)).Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: &diagnostics})
|
||||||
|
if nativeErr == nil {
|
||||||
|
t.Fatal("fixture did not supply a native failure")
|
||||||
|
}
|
||||||
|
if got.Status != 1 || !errors.Is(got.Cause, nativeErr) || got.Explanation.Message != nativeErr.Error() {
|
||||||
|
t.Fatalf("native error was not preserved in outcome: %#v, error=%v", got, nativeErr)
|
||||||
|
}
|
||||||
|
if strings.Count(diagnostics.String(), nativeErr.Error()) != 1 || strings.Contains(diagnostics.String(), "independent fixture warning") {
|
||||||
|
t.Fatalf("terminal adaptation or warning visibility: %q", diagnostics.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNativeOperationExplanationPreservesSuppliedContext(t *testing.T) {
|
||||||
|
nativeErr := errors.New("native operation detail")
|
||||||
|
for _, explanation := range []commandkit.Explanation{
|
||||||
|
{Message: "selected explanation"},
|
||||||
|
{Details: []commandkit.Detail{func(w io.Writer) error { _, err := io.WriteString(w, "native structured explanation\n"); return err }}},
|
||||||
|
} {
|
||||||
|
messages := (promptKitAcquisition{explanation: explanation}).withOperationError(nativeErr)
|
||||||
|
suite := Suite[struct{}, struct{}]{InspectPrompt: func(context.Context, PromptRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||||
|
return commandkit.Acquired[*promptkit.PromptInspection]{Explanation: messages.explanation}, nativeErr
|
||||||
|
}}
|
||||||
|
var diagnostics bytes.Buffer
|
||||||
|
got := newPromptTestApp(t, Command(suite)).Execute(context.Background(), []string{"promptkit", "show", "prompt", "fixture", "--quiet"}, commandkit.Streams{Err: &diagnostics})
|
||||||
|
if !errors.Is(got.Cause, nativeErr) || strings.Contains(diagnostics.String(), nativeErr.Error()) || strings.Count(diagnostics.String(), "explanation") != 1 {
|
||||||
|
t.Fatalf("supplied explanation was replaced or duplicated: %#v, %q", got, diagnostics.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,8 @@ type SmokeRequest struct {
|
|||||||
// without a builder, the seed is forwarded unchanged. Reports and errors from
|
// without a builder, the seed is forwarded unchanged. Reports and errors from
|
||||||
// PromptKit are both retained. A nil loader is kept as a suite wiring issue and
|
// PromptKit are both retained. A nil loader is kept as a suite wiring issue and
|
||||||
// does not install a smoke-test callback. The callback also retains independent
|
// does not install a smoke-test callback. The callback also retains independent
|
||||||
// engine diagnostics and terminal context with the report and error.
|
// engine diagnostics and terminal context with the report and error. Native
|
||||||
|
// operation errors supply terminal explanations when structured context is absent.
|
||||||
func WithSmokeTesting[I, S any](
|
func WithSmokeTesting[I, S any](
|
||||||
suite Suite[I, S],
|
suite Suite[I, S],
|
||||||
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
|
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
|
||||||
@@ -65,6 +66,7 @@ func WithSmokeTesting[I, S any](
|
|||||||
}
|
}
|
||||||
|
|
||||||
report, err := engine.SmokeTest(ctx, concreteRequest)
|
report, err := engine.SmokeTest(ctx, concreteRequest)
|
||||||
|
messages = messages.withOperationError(err)
|
||||||
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||||
}
|
}
|
||||||
return suite
|
return suite
|
||||||
|
|||||||
Reference in New Issue
Block a user