package ckprompt import ( "bytes" "context" "encoding/json" "errors" "io" "reflect" "testing" "go.maximumdirect.net/ckconfig" commandkit "go.maximumdirect.net/commandkit" promptkit "go.maximumdirect.net/promptkit" ) func TestPreparationCommandForwardsOrderedInputsAndNativeReport(t *testing.T) { type key struct{} ctx := context.WithValue(context.Background(), key{}, "context") var received PreparationRequest calls := 0 native := &promptkit.PreparedRun{PromptID: "native", Messages: []promptkit.RenderedMessage{{Content: "rendered"}}} app := newPromptTestApp(t, Command(Suite[struct{}, struct{}]{PrepareRequest: func(got context.Context, req PreparationRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.PreparedRun], error) { if got.Value(key{}) != "context" { t.Fatal("invocation context lost") } calls++ received = req return commandkit.Acquired[*promptkit.PreparedRun]{Value: native}, nil }})) var output bytes.Buffer result := app.Execute(ctx, []string{"promptkit", "show", "prepared", "briefing", "--config=chosen", "--llm-profile=editorial", "--llm-input-file=x=first.txt", "--llm-input-text=x=a=b", "--llm-input-text=y=", "--llm-input-file=z=path", "--llm-input-text=z=last", "--llm-input-file=z=final", "--format=json"}, commandkit.Streams{Out: &output, Err: io.Discard}) if result.Kind != commandkit.OutcomeSuccess || calls != 1 || received.Request.PromptID != "briefing" || received.Request.ProfileID != "editorial" || received.Selection.Config.Value != "chosen" || !received.Selection.Config.Set { t.Fatalf("outcome/request = %#v %#v", result, received) } want := map[string]promptkit.ArtifactRef{"x": promptkit.Inline("a=b"), "y": promptkit.Inline(""), "z": promptkit.File("final")} if !reflect.DeepEqual(received.Request.Inputs, want) { t.Fatalf("inputs = %#v", received.Request.Inputs) } var report, expected any encoded, err := json.Marshal(native) if err != nil { t.Fatal(err) } if err := json.Unmarshal(encoded, &expected); err != nil { t.Fatal(err) } if err := json.Unmarshal(output.Bytes(), &report); err != nil || !reflect.DeepEqual(report, expected) { t.Fatalf("report = %s (%v)", output.String(), err) } output.Reset() result = app.Execute(ctx, []string{"promptkit", "show", "prepared", "fresh", "--format=json"}, commandkit.Streams{Out: &output, Err: io.Discard}) if result.Kind != commandkit.OutcomeSuccess || received.Request.Inputs != nil { t.Fatalf("fresh request = %#v %#v", result, received) } } func TestPreparationErrorsAndHelpDoNotInvokeUnrelatedCallbacks(t *testing.T) { cause := errors.New("preparation failure") calls := 0 suite := Suite[struct{}, struct{}]{PrepareRequest: func(context.Context, PreparationRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.PreparedRun], error) { calls++ return commandkit.Acquired[*promptkit.PreparedRun]{}, cause }} app := newPromptTestApp(t, Command(suite)) var help bytes.Buffer result := app.Execute(context.Background(), []string{"promptkit", "show", "prepared", "--help"}, commandkit.Streams{Out: &help, Err: io.Discard}) if result.Kind != commandkit.OutcomeSuccess || calls != 0 || !bytes.Contains(help.Bytes(), []byte("resource I/O")) || !bytes.Contains(help.Bytes(), []byte("rendered application content")) { t.Fatalf("help = %#v %q", result, help.String()) } result = app.Execute(context.Background(), []string{"promptkit", "show", "prepared", "missing"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) if result.Kind != commandkit.OutcomeOperational || !errors.Is(result.Cause, cause) || calls != 1 { t.Fatalf("failure = %#v", result) } } func TestNativePreparationBuilderAndErrorIdentity(t *testing.T) { client := &validationFailOnGenerate{} engine := validationTestEngine(t, adapterPrompt, promptkit.WithLLMClient(client)) built := promptkit.RunRequest{PromptID: "briefing", ProfileID: "default", Inputs: map[string]promptkit.ArtifactRef{"topic": promptkit.Inline("builder value")}} ctx := context.Background() want, err := engine.Prepare(ctx, built) if err != nil { t.Fatal(err) } calls := 0 suite := WithRequestPreparation(Suite[struct{}, struct{}]{}, func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) { return engine, nil }, func(_ context.Context, seed PreparationRequest, _, _ struct{}) (promptkit.RunRequest, error) { calls++ if seed.Request.PromptID != "seed" { t.Fatalf("seed = %#v", seed) } return built, nil }) got, err := suite.PrepareRequest(ctx, PreparationRequest{Request: promptkit.RunRequest{PromptID: "seed"}}, struct{}{}, struct{}{}) if err != nil || calls != 1 || got.Value == nil || got.Value.PromptID != want.PromptID || !reflect.DeepEqual(got.Value.Messages, want.Messages) || client.calls != 0 { t.Fatalf("prepared = %#v, err = %v, calls = %d, model calls = %d", got, err, calls, client.calls) } withoutBuilder := WithRequestPreparation(Suite[struct{}, struct{}]{}, func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) { return engine, nil }, nil) _, err = withoutBuilder.PrepareRequest(ctx, PreparationRequest{Request: promptkit.RunRequest{PromptID: "missing"}}, struct{}{}, struct{}{}) if !errors.Is(err, promptkit.ErrPromptNotFound) { t.Fatalf("native error = %v", err) } }