init
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
func TestPlatformClientJobMethodsPostJSONAndDecodeResponses(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
seen[r.URL.Path] = true
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/run/jobs/claim":
|
||||
var request protocol.RunJobClaimRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.RunEndpointID != "run-local" || request.SessionToken != "session-token" {
|
||||
t.Fatalf("unexpected claim request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, validRunJobClaimResponse())
|
||||
case "/api/v1/run/jobs/ack":
|
||||
var request protocol.RunJobAckRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.JobID != "job-1" || request.LeaseToken != "lease-1" {
|
||||
t.Fatalf("unexpected ack request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.RunJobAckResponse{Accepted: true, Job: validRunJobAssignment(), ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/progress":
|
||||
var request protocol.RunJobProgressRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.Progress.Percent != 40 {
|
||||
t.Fatalf("unexpected progress request: %+v", request)
|
||||
}
|
||||
assignment := validRunJobAssignment()
|
||||
assignment.Progress.Percent = 40
|
||||
writeTestJSON(t, w, protocol.RunJobProgressResponse{Accepted: true, Job: assignment, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/result":
|
||||
var request protocol.RunJobResultRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.State != "succeeded" || request.ResultRef == "" {
|
||||
t.Fatalf("unexpected result request: %+v", request)
|
||||
}
|
||||
assignment := validRunJobAssignment()
|
||||
assignment.State = "succeeded"
|
||||
assignment.ResultRef = request.ResultRef
|
||||
writeTestJSON(t, w, protocol.RunJobResultResponse{Accepted: true, Job: assignment, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/cancel":
|
||||
var request protocol.RunJobCancelPollRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.JobID != "job-1" {
|
||||
t.Fatalf("unexpected cancel request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.RunJobCancelPollResponse{Accepted: true, RunEndpointID: "run-local", HasCancel: true, JobID: "job-1", Reason: "stop", ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/reconcile":
|
||||
var request protocol.RunJobReconcileRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if len(request.ActiveJobs) != 1 || request.ActiveJobs[0].JobID != "job-1" || request.ActiveJobs[0].Attempt != 1 {
|
||||
t.Fatalf("unexpected reconcile request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.RunJobReconcileResponse{Accepted: true, RunEndpointID: "run-local", ConfirmedJobs: []protocol.RunJobAssignment{validRunJobAssignment()}, ServerTime: fixedClientTestTime()})
|
||||
default:
|
||||
t.Fatalf("unexpected request path %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewPlatformClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
claim, err := client.ClaimJob(ctx, validRunJobClaimRequest())
|
||||
if err != nil || !claim.HasJob {
|
||||
t.Fatalf("claim job response=%+v err=%v", claim, err)
|
||||
}
|
||||
if _, err := client.AckJob(ctx, validRunJobAckRequest()); err != nil {
|
||||
t.Fatalf("ack job: %v", err)
|
||||
}
|
||||
if _, err := client.UpdateJobProgress(ctx, validRunJobProgressRequest()); err != nil {
|
||||
t.Fatalf("progress job: %v", err)
|
||||
}
|
||||
if _, err := client.CompleteJob(ctx, validRunJobResultRequest()); err != nil {
|
||||
t.Fatalf("complete job: %v", err)
|
||||
}
|
||||
if _, err := client.PollJobCancel(ctx, validRunJobCancelPollRequest()); err != nil {
|
||||
t.Fatalf("poll cancel: %v", err)
|
||||
}
|
||||
if _, err := client.ReconcileJobs(ctx, validRunJobReconcileRequest()); err != nil {
|
||||
t.Fatalf("reconcile jobs: %v", err)
|
||||
}
|
||||
|
||||
for _, path := range []string{"/api/v1/run/jobs/claim", "/api/v1/run/jobs/ack", "/api/v1/run/jobs/progress", "/api/v1/run/jobs/result", "/api/v1/run/jobs/cancel", "/api/v1/run/jobs/reconcile"} {
|
||||
if !seen[path] {
|
||||
t.Fatalf("expected request to %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientJobMethodReturnsErrorForPlatformFailure(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"code":"validation_failed"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewPlatformClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
if _, err := client.ClaimJob(context.Background(), validRunJobClaimRequest()); err == nil {
|
||||
t.Fatal("expected platform error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientJobLifecycleFlow(t *testing.T) {
|
||||
assignment := validRunJobAssignment()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/run/jobs/claim":
|
||||
assignment.State = "accepted"
|
||||
writeTestJSON(t, w, protocol.RunJobClaimResponse{Accepted: true, RunEndpointID: "run-local", HasJob: true, Job: &assignment, NextPollSeconds: 2, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/ack":
|
||||
assignment.State = "running"
|
||||
writeTestJSON(t, w, protocol.RunJobAckResponse{Accepted: true, Job: assignment, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/progress":
|
||||
assignment.Progress.Percent = 70
|
||||
writeTestJSON(t, w, protocol.RunJobProgressResponse{Accepted: true, Job: assignment, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/result":
|
||||
assignment.State = "succeeded"
|
||||
assignment.Progress.Percent = 100
|
||||
assignment.ResultRef = "artifact://jobs/job-1/result"
|
||||
writeTestJSON(t, w, protocol.RunJobResultResponse{Accepted: true, Job: assignment, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/reconcile":
|
||||
writeTestJSON(t, w, protocol.RunJobReconcileResponse{Accepted: true, RunEndpointID: "run-local", DiscardJobIDs: []string{"local-only"}, ServerTime: fixedClientTestTime()})
|
||||
default:
|
||||
t.Fatalf("unexpected request path %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewPlatformClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
claim, err := client.ClaimJob(ctx, validRunJobClaimRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("claim job: %v", err)
|
||||
}
|
||||
ack, err := client.AckJob(ctx, protocol.RunJobAckRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("ack job: %v", err)
|
||||
}
|
||||
progress, err := client.UpdateJobProgress(ctx, protocol.RunJobProgressRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: ack.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, Progress: protocol.RunJobProgressReport{Percent: 70}})
|
||||
if err != nil {
|
||||
t.Fatalf("progress job: %v", err)
|
||||
}
|
||||
result, err := client.CompleteJob(ctx, protocol.RunJobResultRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: progress.Job.JobID, LeaseToken: progress.Job.LeaseToken, Attempt: progress.Job.Attempt, State: "succeeded", Progress: protocol.RunJobProgressReport{Percent: 100}, ResultRef: "artifact://jobs/job-1/result"})
|
||||
if err != nil {
|
||||
t.Fatalf("complete job: %v", err)
|
||||
}
|
||||
reconcile, err := client.ReconcileJobs(ctx, protocol.RunJobReconcileRequest{RunEndpointID: "run-local", SessionToken: "session-token", ActiveJobs: []protocol.RunJobReconcileEntry{{JobID: "local-only", LeaseToken: "local-lease", Attempt: 1}}})
|
||||
if err != nil {
|
||||
t.Fatalf("reconcile jobs: %v", err)
|
||||
}
|
||||
if claim.Job.State != "accepted" || ack.Job.State != "running" || progress.Job.Progress.Percent != 70 || result.Job.State != "succeeded" || len(reconcile.DiscardJobIDs) != 1 {
|
||||
t.Fatalf("unexpected lifecycle responses: claim=%+v ack=%+v progress=%+v result=%+v reconcile=%+v", claim, ack, progress, result, reconcile)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeTestRequest(t *testing.T, r *http.Request, target any) {
|
||||
t.Helper()
|
||||
if r.Method != http.MethodPost {
|
||||
t.Fatalf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
|
||||
t.Fatalf("expected JSON content type, got %q", contentType)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(target); err != nil {
|
||||
t.Fatalf("decode request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fixedClientTestTime() time.Time {
|
||||
return time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func validRunJobClaimRequest() protocol.RunJobClaimRequest {
|
||||
return protocol.RunJobClaimRequest{RunEndpointID: "run-local", SessionToken: "session-token", Capabilities: []string{"process.start"}, Capacity: protocol.RunCapacityReport{MaxJobs: 4}}
|
||||
}
|
||||
|
||||
func validRunJobAckRequest() protocol.RunJobAckRequest {
|
||||
return protocol.RunJobAckRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: "job-1", LeaseToken: "lease-1", Attempt: 1, Message: "started"}
|
||||
}
|
||||
|
||||
func validRunJobProgressRequest() protocol.RunJobProgressRequest {
|
||||
return protocol.RunJobProgressRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: "job-1", LeaseToken: "lease-1", Attempt: 1, Progress: protocol.RunJobProgressReport{Percent: 40, Message: "working"}}
|
||||
}
|
||||
|
||||
func validRunJobResultRequest() protocol.RunJobResultRequest {
|
||||
return protocol.RunJobResultRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: "job-1", LeaseToken: "lease-1", Attempt: 1, State: "succeeded", Progress: protocol.RunJobProgressReport{Percent: 100}, ResultRef: "artifact://jobs/job-1/result", Message: "done"}
|
||||
}
|
||||
|
||||
func validRunJobCancelPollRequest() protocol.RunJobCancelPollRequest {
|
||||
return protocol.RunJobCancelPollRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: "job-1", LeaseToken: "lease-1", Attempt: 1}
|
||||
}
|
||||
|
||||
func validRunJobReconcileRequest() protocol.RunJobReconcileRequest {
|
||||
return protocol.RunJobReconcileRequest{RunEndpointID: "run-local", SessionToken: "session-token", ActiveJobs: []protocol.RunJobReconcileEntry{{JobID: "job-1", LeaseToken: "lease-1", Attempt: 1}}}
|
||||
}
|
||||
|
||||
func validRunJobClaimResponse() protocol.RunJobClaimResponse {
|
||||
job := validRunJobAssignment()
|
||||
return protocol.RunJobClaimResponse{Accepted: true, RunEndpointID: "run-local", HasJob: true, Job: &job, NextPollSeconds: 2, ServerTime: fixedClientTestTime()}
|
||||
}
|
||||
|
||||
func validRunJobAssignment() protocol.RunJobAssignment {
|
||||
return protocol.RunJobAssignment{
|
||||
JobID: "job-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: "process.start",
|
||||
IdempotencyKey: "idem-1",
|
||||
State: "accepted",
|
||||
LeaseToken: "lease-1",
|
||||
Attempt: 1,
|
||||
CreatedAt: fixedClientTestTime(),
|
||||
UpdatedAt: fixedClientTestTime(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user