diff --git a/capacity_contract_test.go b/capacity_contract_test.go index b4f0bea..2a57370 100644 --- a/capacity_contract_test.go +++ b/capacity_contract_test.go @@ -3,8 +3,6 @@ package promptkit_test import ( "context" "errors" - "runtime" - "strings" "sync" "testing" "time" @@ -172,46 +170,6 @@ func TestUnlimitedBackendsRetainInjectedClientConcurrency(t *testing.T) { } } -func TestCancelWhileWaitingForGenerationCapacityPreservesErrorAndCapacity(t *testing.T) { - release := make(chan struct{}) - client := newCapacityGateClient(release, 3) - engine := newBackendCapacityEngine(t, client, 1, capacityInt(1), nil) - - firstResult := make(chan capacityRunResult, 1) - go runCapacityRequest(engine, context.Background(), promptkit.RunRequest{PromptID: "prompt"}, firstResult) - awaitCapacityRequest(t, client.started) - - waitingContext := newObservedCancelContext() - secondResult := make(chan capacityRunResult, 1) - go runCapacityRequest(engine, waitingContext, promptkit.RunRequest{PromptID: "prompt"}, secondResult) - awaitCapacitySignal(t, waitingContext.waiting, "generation-capacity wait") - waitingContext.cancel() - - outcome := awaitCapacityRun(t, secondResult) - if outcome.result != nil { - t.Fatalf("canceled capacity wait returned partial result: %+v", outcome.result) - } - if !errors.Is(outcome.err, context.Canceled) || !errors.Is(outcome.err, promptkit.ErrLLMGenerate) { - t.Fatalf("canceled capacity wait=%v, want context.Canceled and ErrLLMGenerate", outcome.err) - } - if _, _, calls := client.snapshot(); calls != 1 { - t.Fatalf("client calls=%d after canceled waiter, want only active call", calls) - } - - close(release) - first := awaitCapacityRun(t, firstResult) - if first.err != nil || first.result == nil { - t.Fatalf("first run=(%+v, %v), want success", first.result, first.err) - } - result, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt"}) - if err != nil || result == nil { - t.Fatalf("run after cancellation=(%+v, %v), want reusable capacity", result, err) - } - if _, _, calls := client.snapshot(); calls != 2 { - t.Fatalf("client calls=%d after reuse, want 2", calls) - } -} - func TestCapacityExceededSentinelContract(t *testing.T) { if promptkit.ErrCapacityExceeded == nil { t.Fatal("ErrCapacityExceeded is nil") @@ -366,61 +324,6 @@ func (r *capacityArtifactReader) callCount() int { return r.calls } -type observedCancelContext struct { - done chan struct{} - waiting chan struct{} - once sync.Once -} - -func newObservedCancelContext() *observedCancelContext { - return &observedCancelContext{ - done: make(chan struct{}), - waiting: make(chan struct{}, 1), - } -} - -func (c *observedCancelContext) Deadline() (time.Time, bool) { - return time.Time{}, false -} - -func (c *observedCancelContext) Done() <-chan struct{} { - var callers [8]uintptr - frames := runtime.CallersFrames(callers[:runtime.Callers(2, callers[:])]) - for { - frame, more := frames.Next() - if strings.Contains(frame.Function, "internal/capacity.(*pool).acquire") { - select { - case c.waiting <- struct{}{}: - default: - } - break - } - if !more { - break - } - } - return c.done -} - -func (c *observedCancelContext) Err() error { - select { - case <-c.done: - return context.Canceled - default: - return nil - } -} - -func (c *observedCancelContext) Value(any) any { - return nil -} - -func (c *observedCancelContext) cancel() { - c.once.Do(func() { - close(c.done) - }) -} - func awaitCapacityRequest( t *testing.T, requests <-chan promptkit.GenerateRequest, diff --git a/docs/internal/capacity.md b/docs/internal/capacity.md index 35d6f37..1556806 100644 --- a/docs/internal/capacity.md +++ b/docs/internal/capacity.md @@ -94,4 +94,7 @@ The [runner tests](../../internal/usecase/runner_test.go) own early admission, lease lifetime, failure release, and shared initial/repair scheduling. The [external package capacity tests](../../capacity_contract_test.go) own the assembled public-engine behavior for configured limits, capacity errors, -endpoint identity, engine independence, cancellation, and injected clients. +endpoint identity, engine independence, and injected clients. The +[root error-boundary tests](../../errors_internal_test.go) own preservation of +the public generation category and context identity when generation is +canceled. diff --git a/errors_internal_test.go b/errors_internal_test.go new file mode 100644 index 0000000..e3e1be6 --- /dev/null +++ b/errors_internal_test.go @@ -0,0 +1,22 @@ +package promptkit + +import ( + "context" + "errors" + "fmt" + "testing" + + "gitea.maximumdirect.net/eric/promptkit/internal/usecase" +) + +func TestMapPublicErrorPreservesGenerationCancellation(t *testing.T) { + internalErr := fmt.Errorf("%w: %w", usecase.ErrLLMGenerate, context.Canceled) + + err := mapPublicError(internalErr) + if !errors.Is(err, ErrLLMGenerate) { + t.Fatalf("mapped error=%v, want ErrLLMGenerate", err) + } + if !errors.Is(err, context.Canceled) { + t.Fatalf("mapped error=%v, want context.Canceled", err) + } +}