iop/apps/edge/internal/openai/artifact_pair_test.go

593 lines
21 KiB
Go

package openai
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sort"
"strings"
"sync"
"sync/atomic"
"testing"
"iop/packages/go/config"
)
func TestArtifactPairFrontierMatrix(t *testing.T) {
for _, endpoint := range []string{"openai", "anthropic"} {
endpoint := endpoint
t.Run(endpoint, func(t *testing.T) {
t.Run("parent-capable reversed pair becomes locally eligible once", func(t *testing.T) {
fixture := newArtifactPairFixture(t, endpoint, true)
publicIDs := fixture.issuePair()
fixture.assertPendingPayloads(publicIDs, []string{fixture.paths.PlanPath, fixture.paths.ReviewPath})
ingress, _, body, err := fixture.continueWithResult([]artifactTestResult{
{id: publicIDs[1], body: `{"written":true}`},
{id: publicIDs[0], body: `{"written":true}`},
}, nil)
if err != nil {
t.Fatalf("consume reversed pair: %v", err)
}
if ingress.Artifact.Kind != artifactDispositionLocalEligible {
t.Fatalf("local eligibility disposition = %#v", ingress.Artifact)
}
fixture.assertPhase(artifactPhaseLocalEligible)
if _, _, err := fixture.continueRaw(body); err == nil || !strings.Contains(err.Error(), "replay") {
t.Fatalf("replayed pair error = %v, want replay rejection", err)
}
fixture.assertPhase(artifactPhaseLocalEligible)
})
t.Run("prepare resumes the exact selector stage before pair", func(t *testing.T) {
fixture := newArtifactPairFixture(t, endpoint, false)
prepareIDs := fixture.issuePrepare()
if len(prepareIDs) != 1 {
t.Fatalf("prepare ids = %#v", prepareIDs)
}
fixture.assertPendingPayloads(prepareIDs, []string{fixture.paths.JobDir})
ingress, metadata, _, err := fixture.continueWithResult([]artifactTestResult{{id: prepareIDs[0], body: `{"written":true}`}}, nil)
if err != nil {
t.Fatalf("consume prepare: %v", err)
}
if metadata["iop_stage_id"] != fixture.stageID || ingress.Artifact.Kind != artifactDispositionResumeSelector {
t.Fatalf("prepare disposition = %#v, original stage = %q", metadata, fixture.stageID)
}
fixture.assertPhase(artifactPhasePairReady)
pairIDs := fixture.issuePair()
ingress, metadata, _, err = fixture.continueWithResult([]artifactTestResult{
{id: pairIDs[1], body: `{"written":true}`},
{id: pairIDs[0], body: `{"written":true}`},
}, nil)
if err != nil {
t.Fatalf("consume pair after prepare: %v", err)
}
if ingress.Artifact.Kind != artifactDispositionLocalEligible {
t.Fatalf("pair disposition = %#v", ingress.Artifact)
}
fixture.assertPhase(artifactPhaseLocalEligible)
})
t.Run("pair-ready selector cannot downgrade to direct", func(t *testing.T) {
fixture := newArtifactPairFixture(t, endpoint, false)
prepareIDs := fixture.issuePrepare()
_, metadata, _, err := fixture.continueWithResult([]artifactTestResult{{id: prepareIDs[0], body: `{"written":true}`}}, nil)
if err != nil {
t.Fatalf("consume prepare: %v", err)
}
fixture.assertPhase(artifactPhasePairReady)
recorder := httptest.NewRecorder()
err = fixture.server.dispatchPresetTurn(
recorder,
httptest.NewRequest(http.MethodPost, "/", nil),
fixture.dispatch,
fixture.endpoint,
false,
metadata,
normalizedStageOutput{ResponseID: "provider_direct", Content: "must not escape pair frontier"},
hotPathTestGate(fixture.dispatch.Preset),
)
if err == nil || recorder.Code != http.StatusBadRequest {
t.Fatalf("pair-ready direct downgrade = err %v, status %d", err, recorder.Code)
}
})
t.Run("general tool continuation bypasses artifact hook", func(t *testing.T) {
fixture := newArtifactPairFixture(t, endpoint, true)
publicID := fixture.issueGeneralTool()
metadata, _, err := fixture.continueWith([]artifactTestResult{{id: publicID, body: "general result"}}, nil)
if err != nil {
t.Fatalf("consume general continuation: %v", err)
}
if metadata["iop_stage_id"] == "" || metadata["iop_stage_id"] == fixture.stageID {
t.Fatalf("general continuation did not activate a fresh stage: %#v", metadata)
}
fixture.assertPhase(artifactPhasePinned)
})
for _, rejection := range []struct {
name string
results func([]string) []artifactTestResult
mutate func(any)
}{
{name: "missing", results: func(ids []string) []artifactTestResult {
return []artifactTestResult{{id: ids[0], body: `{"written":true}`}}
}},
{name: "extra", results: func(ids []string) []artifactTestResult {
return []artifactTestResult{{id: ids[0], body: `{"written":true}`}, {id: ids[1], body: `{"written":true}`}, {id: "call_extra", body: `{"written":true}`}}
}},
{name: "duplicate", results: func(ids []string) []artifactTestResult {
return []artifactTestResult{{id: ids[0], body: `{"written":true}`}, {id: ids[0], body: `{"written":true}`}}
}},
{name: "opaque", results: func(ids []string) []artifactTestResult {
return []artifactTestResult{{id: ids[0], body: "opaque"}, {id: ids[1], body: `{"written":true}`}}
}},
{name: "alternate public ids", results: func(ids []string) []artifactTestResult {
return []artifactTestResult{{id: "call_alternate_plan", body: `{"written":true}`}, {id: "call_alternate_review", body: `{"written":true}`}}
}, mutate: mutateArtifactAssistantIDs},
} {
rejection := rejection
t.Run("reject "+rejection.name, func(t *testing.T) {
fixture := newArtifactPairFixture(t, endpoint, true)
ids := fixture.issuePair()
before := fixture.stateSignature()
if _, _, err := fixture.continueWith(rejection.results(ids), rejection.mutate); err == nil {
t.Fatalf("%s continuation unexpectedly succeeded", rejection.name)
}
if after := fixture.stateSignature(); after != before {
t.Fatalf("%s advanced state: before=%s after=%s", rejection.name, before, after)
}
})
}
for _, emission := range []struct {
name string
planPath string
}{
{name: "traversal path", planPath: ".iop/job/../escape/plan.md"},
{name: "alternate request path", planPath: ".iop/job/other-request/plan.md"},
} {
emission := emission
t.Run("reject "+emission.name, func(t *testing.T) {
fixture := newArtifactPairFixture(t, endpoint, true)
if _, err := fixture.issue([]normalizedToolCall{
artifactProviderWrite("provider_plan", emission.planPath, "plan"),
artifactProviderWrite("provider_review", fixture.paths.ReviewPath, "review"),
}); err == nil {
t.Fatalf("%s emission unexpectedly succeeded", emission.name)
}
})
}
t.Run("concurrent duplicate consumption advances once", func(t *testing.T) {
fixture := newArtifactPairFixture(t, endpoint, true)
ids := fixture.issuePair()
body := fixture.continuationBody([]artifactTestResult{
{id: ids[1], body: `{"written":true}`},
{id: ids[0], body: `{"written":true}`},
}, nil)
var successes atomic.Int32
var wg sync.WaitGroup
for range 2 {
wg.Add(1)
go func() {
defer wg.Done()
if _, _, err := fixture.continueRaw(body); err == nil {
successes.Add(1)
}
}()
}
wg.Wait()
if got := successes.Load(); got != 1 {
t.Fatalf("concurrent successes = %d, want 1", got)
}
fixture.assertPhase(artifactPhaseLocalEligible)
})
})
}
}
type artifactPairFixture struct {
t *testing.T
endpoint string
server *Server
dispatch routeDispatch
requestID string
stageID string
ownerEdgeID string
principalRef string
paths reservedPaths
tools []any
history []any
lastAssistant any
}
type artifactTestResult struct {
id string
body string
failed bool
}
func newArtifactPairFixture(t *testing.T, endpoint string, createsParents bool) *artifactPairFixture {
t.Helper()
var sequence atomic.Int64
idSource := func() (string, error) {
return fmt.Sprintf("artifact_%03d", sequence.Add(1)), nil
}
coordinator := newLogicalRequestCoordinator(logicalRequestCoordinatorOptions{IDSource: idSource})
server := NewServer(config.EdgeOpenAIConf{}, nil, nil)
server.requestCoordinator = coordinator
server.artifactFrontiers = newArtifactFrontierStore(32)
server.SetEdgeID("edge-artifact")
alternative := workspaceAlternative("artifact-structured", "workspace", false, createsParents)
preset := config.ExecutionPreset{
ID: "artifact-preset", Selector: config.ExecutionModelBinding{Model: "selector-model"},
AllowedModes: []string{modeLight}, WorkspaceTools: []config.ExecutionWorkspaceToolAlternative{alternative},
}
dispatch := routeDispatch{IsPreset: true, PresetID: preset.ID, Preset: preset, ExternalModelID: "virtual-artifact"}
schema := map[string]any{
"type": "object",
"properties": map[string]any{"path": map[string]any{"type": "string"}, "content": map[string]any{}},
}
tools := []any{openAIChatTool("workspace", schema)}
if endpoint == "anthropic" {
tools = []any{anthropicWorkspaceTool("workspace", schema)}
}
history := []any{map[string]any{"role": "user", "content": "task"}}
body := artifactRequestBody(t, endpoint, tools, history)
metadata := map[string]string{principalMetaRef: "principal-artifact"}
var err error
if endpoint == "anthropic" {
_, err = server.joinPresetAnthropicIngress(nil, dispatch, body, metadata)
} else {
_, err = server.joinPresetChatIngress(nil, dispatch, body, metadata)
}
if err != nil {
t.Fatalf("join initial %s artifact request: %v", endpoint, err)
}
requestID := metadata["iop_logical_request_id"]
stageID := metadata["iop_stage_id"]
if requestID == "" || stageID == "" {
t.Fatalf("initial metadata = %#v", metadata)
}
return &artifactPairFixture{
t: t, endpoint: endpoint, server: server, dispatch: dispatch,
requestID: requestID, stageID: stageID, ownerEdgeID: "edge-artifact", principalRef: "principal-artifact",
paths: newReservedPaths(requestID), tools: tools, history: history,
}
}
func (f *artifactPairFixture) issuePrepare() []string {
f.t.Helper()
ids, err := f.issue([]normalizedToolCall{{
ID: "provider_prepare", Name: "workspace", Arguments: map[string]any{"path": f.paths.JobDir},
}})
if err != nil {
f.t.Fatalf("issue prepare: %v", err)
}
return ids
}
func (f *artifactPairFixture) issuePair() []string {
f.t.Helper()
ids, err := f.issue([]normalizedToolCall{
artifactProviderWrite("provider_plan", f.paths.PlanPath, "plan"),
artifactProviderWrite("provider_review", f.paths.ReviewPath, "review"),
})
if err != nil {
f.t.Fatalf("issue pair: %v", err)
}
return ids
}
func (f *artifactPairFixture) issueGeneralTool() string {
f.t.Helper()
recorder := httptest.NewRecorder()
turn := &hotPathTurn{
RequestID: f.requestID, StageID: f.stageID, CallID: "http_call", OwnerEdgeID: f.ownerEdgeID,
PrincipalRef: f.principalRef, Preset: f.dispatch.Preset, Dispatch: f.dispatch,
Protocol: f.endpoint, PublicModelID: f.dispatch.ExternalModelID,
Writer: recorder, Request: httptest.NewRequest(http.MethodPost, "/", nil),
}
output := normalizedStageOutput{
ResponseID: "provider_response", Created: 123,
ToolCalls: []normalizedToolCall{{ID: "call_general", ProviderCallID: "provider_general", Name: "search", Arguments: map[string]any{"query": "status"}}},
}
if err := f.server.runDirectTurn(turn.Request.Context(), turn, output); err != nil {
f.t.Fatalf("issue general tool: %v", err)
}
assistant, ids, err := artifactAssistantFromResponse(f.endpoint, recorder.Body.Bytes())
if err != nil || len(ids) != 1 {
f.t.Fatalf("decode general tool response: ids=%#v err=%v", ids, err)
}
f.history = append(f.history, assistant)
f.lastAssistant = assistant
return ids[0]
}
func artifactProviderWrite(id, path, content string) normalizedToolCall {
return normalizedToolCall{ID: id, Name: "workspace", Arguments: map[string]any{"path": path, "content": content}}
}
func (f *artifactPairFixture) issue(calls []normalizedToolCall) ([]string, error) {
f.t.Helper()
recorder := httptest.NewRecorder()
turn := &hotPathTurn{
RequestID: f.requestID, StageID: f.stageID, CallID: "http_call", OwnerEdgeID: f.ownerEdgeID,
PrincipalRef: f.principalRef, Preset: f.dispatch.Preset, Dispatch: f.dispatch,
Protocol: f.endpoint, PublicModelID: f.dispatch.ExternalModelID,
Writer: recorder, Request: httptest.NewRequest(http.MethodPost, "/", nil),
}
err := f.server.runArtifactPairTurn(turn, normalizedStageOutput{
ResponseID: "provider_response", Created: 123, ToolCalls: calls,
}, hotPathTestGate(turn.Preset))
if err != nil {
return nil, err
}
if recorder.Code != http.StatusOK {
return nil, fmt.Errorf("artifact response status %d: %s", recorder.Code, recorder.Body.String())
}
assistant, ids, err := artifactAssistantFromResponse(f.endpoint, recorder.Body.Bytes())
if err != nil {
return nil, err
}
f.history = append(f.history, assistant)
f.lastAssistant = assistant
return ids, nil
}
func artifactAssistantFromResponse(endpoint string, body []byte) (any, []string, error) {
if endpoint == "anthropic" {
var response struct {
Content []map[string]any `json:"content"`
}
if err := json.Unmarshal(body, &response); err != nil {
return nil, nil, err
}
ids := make([]string, 0, len(response.Content))
for _, block := range response.Content {
if block["type"] == "tool_use" {
ids = append(ids, block["id"].(string))
}
}
return map[string]any{"role": "assistant", "content": response.Content}, ids, nil
}
var response struct {
Choices []struct {
Message map[string]any `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(body, &response); err != nil || len(response.Choices) != 1 {
return nil, nil, fmt.Errorf("decode Chat artifact response: %v", err)
}
toolCalls, _ := response.Choices[0].Message["tool_calls"].([]any)
ids := make([]string, 0, len(toolCalls))
for _, value := range toolCalls {
call, _ := value.(map[string]any)
ids = append(ids, call["id"].(string))
}
return response.Choices[0].Message, ids, nil
}
func (f *artifactPairFixture) continueWith(results []artifactTestResult, mutate func(any)) (map[string]string, []byte, error) {
_, metadata, body, err := f.continueWithResult(results, mutate)
return metadata, body, err
}
func (f *artifactPairFixture) continueWithResult(results []artifactTestResult, mutate func(any)) (presetIngressResult, map[string]string, []byte, error) {
f.t.Helper()
body := f.continuationBody(results, mutate)
ingress, metadata, _, err := f.continueRawResult(body)
if err == nil {
f.history = artifactMessagesFromBody(f.t, body)
}
return ingress, metadata, body, err
}
func (f *artifactPairFixture) continueRaw(body []byte) (map[string]string, []byte, error) {
_, metadata, rawBody, err := f.continueRawResult(body)
return metadata, rawBody, err
}
func (f *artifactPairFixture) continueRawResult(body []byte) (presetIngressResult, map[string]string, []byte, error) {
metadata := map[string]string{principalMetaRef: f.principalRef}
var ingress presetIngressResult
var err error
if f.endpoint == "anthropic" {
ingress, err = f.server.joinPresetAnthropicIngress(nil, f.dispatch, body, metadata)
} else {
ingress, err = f.server.joinPresetChatIngress(nil, f.dispatch, body, metadata)
}
return ingress, metadata, body, err
}
func (f *artifactPairFixture) continuationBody(results []artifactTestResult, mutate func(any)) []byte {
f.t.Helper()
history := cloneArtifactJSON[[]any](f.t, f.history)
if mutate != nil {
mutate(history[len(history)-1])
}
if f.endpoint == "anthropic" {
blocks := make([]any, 0, len(results))
for _, result := range results {
block := map[string]any{"type": "tool_result", "tool_use_id": result.id, "content": result.body}
if result.failed {
block["is_error"] = true
}
blocks = append(blocks, block)
}
history = append(history, map[string]any{"role": "user", "content": blocks})
} else {
for _, result := range results {
content := result.body
if result.failed {
content = `{"error":{"message":"failed"}}`
}
history = append(history, map[string]any{"role": "tool", "tool_call_id": result.id, "content": content})
}
}
return artifactRequestBody(f.t, f.endpoint, f.tools, history)
}
func mutateArtifactAssistantIDs(assistant any) {
message, _ := assistant.(map[string]any)
if blocks, ok := message["content"].([]any); ok {
index := 0
for _, value := range blocks {
block, _ := value.(map[string]any)
if block["type"] == "tool_use" {
if index == 0 {
block["id"] = "call_alternate_plan"
} else {
block["id"] = "call_alternate_review"
}
index++
}
}
return
}
toolCalls, _ := message["tool_calls"].([]any)
for index, value := range toolCalls {
call, _ := value.(map[string]any)
if index == 0 {
call["id"] = "call_alternate_plan"
} else {
call["id"] = "call_alternate_review"
}
}
}
func artifactRequestBody(t *testing.T, endpoint string, tools, history []any) []byte {
t.Helper()
envelope := map[string]any{"model": "virtual-artifact", "messages": history, "tools": tools}
if endpoint == "anthropic" {
envelope["max_tokens"] = 64
}
body, err := json.Marshal(envelope)
if err != nil {
t.Fatalf("marshal artifact request: %v", err)
}
return body
}
func artifactMessagesFromBody(t *testing.T, body []byte) []any {
t.Helper()
var envelope struct {
Messages []any `json:"messages"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
t.Fatalf("decode artifact messages: %v", err)
}
return envelope.Messages
}
func cloneArtifactJSON[T any](t *testing.T, value any) T {
t.Helper()
raw, err := json.Marshal(value)
if err != nil {
t.Fatalf("marshal cloned artifact JSON: %v", err)
}
var out T
if err := json.Unmarshal(raw, &out); err != nil {
t.Fatalf("unmarshal cloned artifact JSON: %v", err)
}
return out
}
func (f *artifactPairFixture) assertPendingPayloads(ids, wantPaths []string) {
f.t.Helper()
f.server.artifactFrontiers.mu.Lock()
defer f.server.artifactFrontiers.mu.Unlock()
record := f.server.artifactFrontiers.records[f.requestID]
if record == nil || len(record.pending) != len(ids) {
f.t.Fatalf("pending frontier = %#v", record)
}
for index, id := range ids {
payload := record.pending[id]
if payload == nil || payload.safePath != wantPaths[index] {
f.t.Fatalf("payload[%q] = %#v, want path %q", id, payload, wantPaths[index])
}
if payload.publicCallID != id || payload.providerCallID == "" || payload.providerCallID == id {
f.t.Fatalf("payload identities are not public/provider correlated: %#v", payload)
}
if payload.fingerprint != record.binding.bindingFingerprint() || payload.correlationDigest == "" {
f.t.Fatalf("payload is not sealed to pinned binding: %#v", payload)
}
}
}
func (f *artifactPairFixture) assertPhase(want artifactFrontierPhase) {
f.t.Helper()
f.server.artifactFrontiers.mu.Lock()
defer f.server.artifactFrontiers.mu.Unlock()
record := f.server.artifactFrontiers.records[f.requestID]
if record == nil || record.phase != want {
f.t.Fatalf("artifact phase = %#v, want %q", record, want)
}
}
func (f *artifactPairFixture) stateSignature() string {
f.t.Helper()
snap, err := f.server.requestCoordinator.snapshot(f.requestID)
if err != nil {
f.t.Fatalf("snapshot artifact coordinator: %v", err)
}
f.server.artifactFrontiers.mu.Lock()
defer f.server.artifactFrontiers.mu.Unlock()
record := f.server.artifactFrontiers.records[f.requestID]
if record == nil {
return "missing"
}
sort.Strings(snap.ExpectedCallIDs)
return fmt.Sprintf("%s|%s|%s|%d|%s|%v", snap.State, snap.ActiveStageID, record.phase, len(record.pending), record.pendingHash, snap.ExpectedCallIDs)
}
func TestArtifactPairFailureCleanupKeepsMalformedFailClosed(t *testing.T) {
for _, endpoint := range []string{"openai", "anthropic"} {
endpoint := endpoint
t.Run(endpoint+" exact failure", func(t *testing.T) {
fixture := newScriptedLightFixture(t, endpoint, false)
prepare := fixture.request()
fixture.consumeToolResponse(prepare, []string{`{"written":true}`})
pair := fixture.request()
fixture.consumeToolResponse(pair, []string{`{"written":true}`, `{"error":"write-failed"}`})
cleanup := fixture.request()
if cleanup.Code != http.StatusOK || !strings.Contains(cleanup.Body.String(), "delete_file") {
t.Fatalf("exact failure cleanup: status=%d body=%s", cleanup.Code, cleanup.Body.String())
}
})
t.Run(endpoint+" malformed result", func(t *testing.T) {
fixture := newScriptedLightFixture(t, endpoint, false)
prepare := fixture.request()
fixture.consumeToolResponse(prepare, []string{`{"written":true}`})
pair := fixture.request()
fixture.consumeToolResponse(pair, []string{`{"written":true}`, `not-json`})
response := fixture.request()
if response.Code != http.StatusBadRequest || strings.Contains(response.Body.String(), "delete_file") {
t.Fatalf("malformed result response: status=%d body=%s", response.Code, response.Body.String())
}
if got := len(fixture.service.snapshots()); got != 2 {
t.Fatalf("malformed result dispatched provider calls=%d, want 2", got)
}
})
t.Run(endpoint+" empty result", func(t *testing.T) {
fixture := newScriptedLightFixture(t, endpoint, false)
prepare := fixture.request()
fixture.consumeToolResponse(prepare, []string{`{"written":true}`})
pair := fixture.request()
fixture.consumeToolResponse(pair, []string{`{"written":true}`, ``})
response := fixture.request()
if response.Code != http.StatusBadRequest || strings.Contains(response.Body.String(), "delete_file") {
t.Fatalf("empty result response: status=%d body=%s", response.Code, response.Body.String())
}
if got := len(fixture.service.snapshots()); got != 2 {
t.Fatalf("empty result dispatched provider calls=%d, want 2", got)
}
})
}
}