first commit
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
func TestPlatformClientArtifactMethodsPostJSONAndDecodeResponses(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/artifacts/open":
|
||||
var request protocol.ArtifactTransferOpenRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.ArtifactID != "artifact-1" || request.ChunkSizeBytes != 8 {
|
||||
t.Fatalf("unexpected artifact open request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, validArtifactOpenResponse())
|
||||
case "/api/v1/run/artifacts/chunks":
|
||||
var request protocol.ArtifactChunkUploadRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.TransferID != "transfer-1" || request.ChunkIndex != 0 || string(request.Payload) != "payload" {
|
||||
t.Fatalf("unexpected artifact chunk request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.ArtifactChunkUploadResponse{Accepted: true, TransferID: "transfer-1", ArtifactID: "artifact-1", ChunkIndex: 0, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/artifacts/status":
|
||||
var request protocol.ArtifactTransferStatusRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.TransferID != "transfer-1" {
|
||||
t.Fatalf("unexpected artifact status request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.ArtifactTransferStatusResponse{Accepted: true, TransferID: "transfer-1", ArtifactID: "artifact-1", Direction: "upload", TotalChunks: 2, ChunkSizeBytes: 8, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/artifacts/complete":
|
||||
var request protocol.ArtifactTransferCompleteRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.Checksum == "" || request.SizeBytes != 7 {
|
||||
t.Fatalf("unexpected artifact complete request: %+v", request)
|
||||
}
|
||||
response := protocol.ArtifactTransferCompleteResponse{Accepted: true, TransferID: "transfer-1", Artifact: validArtifactMetadata("available"), Completed: true, ServerTime: fixedClientTestTime()}
|
||||
writeTestJSON(t, w, response)
|
||||
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()
|
||||
open, err := client.OpenArtifactTransfer(ctx, validClientArtifactOpen())
|
||||
if err != nil || !open.Accepted || open.TransferID != "transfer-1" {
|
||||
t.Fatalf("open artifact response=%+v err=%v", open, err)
|
||||
}
|
||||
chunk, err := client.UploadArtifactChunk(ctx, validClientArtifactChunk())
|
||||
if err != nil || chunk.NextMissingChunkIndex != 1 {
|
||||
t.Fatalf("upload artifact chunk response=%+v err=%v", chunk, err)
|
||||
}
|
||||
status, err := client.QueryArtifactTransferStatus(ctx, validClientArtifactStatus())
|
||||
if err != nil || len(status.ReceivedChunkIndexes) != 1 {
|
||||
t.Fatalf("artifact status response=%+v err=%v", status, err)
|
||||
}
|
||||
complete, err := client.CompleteArtifactTransfer(ctx, validClientArtifactComplete())
|
||||
if err != nil || !complete.Completed || complete.Artifact.State != "available" {
|
||||
t.Fatalf("complete artifact response=%+v err=%v", complete, err)
|
||||
}
|
||||
|
||||
for _, path := range []string{"/api/v1/run/artifacts/open", "/api/v1/run/artifacts/chunks", "/api/v1/run/artifacts/status", "/api/v1/run/artifacts/complete"} {
|
||||
if !seen[path] {
|
||||
t.Fatalf("expected request to %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientArtifactMethodReturnsErrorForPlatformFailure(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.OpenArtifactTransfer(context.Background(), validClientArtifactOpen()); err == nil {
|
||||
t.Fatal("expected platform error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactChunkPayloadUsesJSONBase64Encoding(t *testing.T) {
|
||||
encoded, err := json.Marshal(validClientArtifactChunk())
|
||||
if err != nil {
|
||||
t.Fatalf("marshal artifact chunk: %v", err)
|
||||
}
|
||||
if !json.Valid(encoded) || !containsJSONPayloadField(encoded) {
|
||||
t.Fatalf("expected JSON encoded payload field, got %s", string(encoded))
|
||||
}
|
||||
}
|
||||
|
||||
func validClientArtifactOpen() protocol.ArtifactTransferOpenRequest {
|
||||
return protocol.ArtifactTransferOpenRequest{
|
||||
RunEndpointID: "run-local",
|
||||
SessionToken: "session-token",
|
||||
ArtifactID: "artifact-1",
|
||||
Direction: "upload",
|
||||
OwnerKind: "job",
|
||||
OwnerID: "job-1",
|
||||
SizeBytes: 7,
|
||||
ChunkSizeBytes: 8,
|
||||
Checksum: validSHA256Checksum(),
|
||||
IdempotencyKey: "artifact-upload-1",
|
||||
}
|
||||
}
|
||||
|
||||
func validClientArtifactChunk() protocol.ArtifactChunkUploadRequest {
|
||||
return protocol.ArtifactChunkUploadRequest{RunEndpointID: "run-local", SessionToken: "session-token", TransferID: "transfer-1", ArtifactID: "artifact-1", ChunkIndex: 0, Offset: 0, SizeBytes: 7, Checksum: validSHA256Checksum(), Payload: []byte("payload")}
|
||||
}
|
||||
|
||||
func validClientArtifactStatus() protocol.ArtifactTransferStatusRequest {
|
||||
return protocol.ArtifactTransferStatusRequest{RunEndpointID: "run-local", SessionToken: "session-token", TransferID: "transfer-1", ArtifactID: "artifact-1"}
|
||||
}
|
||||
|
||||
func validClientArtifactComplete() protocol.ArtifactTransferCompleteRequest {
|
||||
return protocol.ArtifactTransferCompleteRequest{RunEndpointID: "run-local", SessionToken: "session-token", TransferID: "transfer-1", ArtifactID: "artifact-1", Checksum: validSHA256Checksum(), SizeBytes: 7}
|
||||
}
|
||||
|
||||
func validArtifactOpenResponse() protocol.ArtifactTransferOpenResponse {
|
||||
return protocol.ArtifactTransferOpenResponse{Accepted: true, TransferID: "transfer-1", Direction: "upload", Artifact: validArtifactMetadata("uploading"), TotalChunks: 2, ChunkSizeBytes: 8, NextMissingChunkIndex: 0, ServerTime: fixedClientTestTime()}
|
||||
}
|
||||
|
||||
func validArtifactMetadata(state string) protocol.ArtifactMetadata {
|
||||
return protocol.ArtifactMetadata{ID: "artifact-1", OwnerKind: "job", OwnerID: "job-1", SizeBytes: 7, Checksum: validSHA256Checksum(), State: state, CreatedAt: fixedClientTestTime(), UpdatedAt: fixedClientTestTime()}
|
||||
}
|
||||
|
||||
func validSHA256Checksum() string {
|
||||
return "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
}
|
||||
|
||||
func containsJSONPayloadField(encoded []byte) bool {
|
||||
var body map[string]any
|
||||
if err := json.Unmarshal(encoded, &body); err != nil {
|
||||
return false
|
||||
}
|
||||
_, exists := body["payload"]
|
||||
return exists
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
func TestPlatformClientLightweightChannelsCompleteWhileArtifactChunkIsBlocked(t *testing.T) {
|
||||
artifactStarted := make(chan struct{})
|
||||
releaseArtifact := make(chan struct{})
|
||||
artifactDone := make(chan struct{})
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/run/artifacts/chunks":
|
||||
var request protocol.ArtifactChunkUploadRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.ChunkIndex != 0 || len(request.Payload) == 0 {
|
||||
t.Fatalf("unexpected artifact payload: %+v", request)
|
||||
}
|
||||
close(artifactStarted)
|
||||
<-releaseArtifact
|
||||
writeTestJSON(t, w, protocol.ArtifactChunkUploadResponse{Accepted: true, TransferID: request.TransferID, ArtifactID: request.ArtifactID, ChunkIndex: request.ChunkIndex, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
||||
close(artifactDone)
|
||||
case "/api/v1/run/control/heartbeat":
|
||||
var request protocol.RunHeartbeatRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
writeTestJSON(t, w, protocol.RunHeartbeatResponse{Accepted: true, RunEndpointID: request.RunEndpointID, NextHeartbeatSeconds: 15, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/jobs/result":
|
||||
var request protocol.RunJobResultRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
encoded, _ := json.Marshal(request)
|
||||
for _, forbidden := range []string{"payload", "entries", "/Users/", "unix://", "tcp://", "Bearer ", "sk-", "password="} {
|
||||
if strings.Contains(string(encoded), forbidden) {
|
||||
t.Fatalf("job result carried forbidden transfer content %q: %s", forbidden, string(encoded))
|
||||
}
|
||||
}
|
||||
job := validRunJobAssignment()
|
||||
job.State = request.State
|
||||
job.ResultRef = request.ResultRef
|
||||
writeTestJSON(t, w, protocol.RunJobResultResponse{Accepted: true, Job: job, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/logs/batches":
|
||||
var request protocol.LogBatchIngestRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if len(request.Entries) != 1 || request.FirstSeq != 1 || request.LastSeq != 1 {
|
||||
t.Fatalf("unexpected log batch: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.LogBatchIngestResponse{Accepted: true, LogStreamID: request.LogStreamID, AcceptedFrom: 1, AcceptedTo: 1, LatestSeq: 1, 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)
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := client.UploadArtifactChunk(context.Background(), validClientArtifactChunk())
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-artifactStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("artifact request did not start")
|
||||
}
|
||||
|
||||
lightCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if _, err := client.Heartbeat(lightCtx, validRunHeartbeatRequest("session-token")); err != nil {
|
||||
t.Fatalf("heartbeat should not wait for artifact chunk: %v", err)
|
||||
}
|
||||
if _, err := client.CompleteJob(lightCtx, validRunJobResultRequest()); err != nil {
|
||||
t.Fatalf("job result should not wait for artifact chunk: %v", err)
|
||||
}
|
||||
if _, err := client.IngestLogBatch(lightCtx, validClientLogBatch()); err != nil {
|
||||
t.Fatalf("log ingest should not wait for artifact chunk: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-artifactDone:
|
||||
t.Fatal("artifact chunk completed before release")
|
||||
default:
|
||||
}
|
||||
close(releaseArtifact)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("artifact chunk upload: %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("artifact chunk did not finish after release")
|
||||
}
|
||||
}
|
||||
@@ -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.ActiveJobIDs) != 1 || request.ActiveJobIDs[0] != "job-1" {
|
||||
t.Fatalf("unexpected reconcile request: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.RunJobReconcileResponse{Accepted: true, RunEndpointID: "run-local", ActiveJobs: []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", UnknownJobIDs: []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", ActiveJobIDs: []string{"local-only"}})
|
||||
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.UnknownJobIDs) != 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"}
|
||||
}
|
||||
|
||||
func validRunJobReconcileRequest() protocol.RunJobReconcileRequest {
|
||||
return protocol.RunJobReconcileRequest{RunEndpointID: "run-local", SessionToken: "session-token", ActiveJobIDs: []string{"job-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(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
func TestPlatformClientIngestLogBatchPostsJSONAndDecodesResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/run/logs/batches" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
|
||||
t.Fatalf("expected JSON content type, got %q", contentType)
|
||||
}
|
||||
var request protocol.LogBatchIngestRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatalf("decode log ingest request: %v", err)
|
||||
}
|
||||
if request.LogStreamID != "log-1" || request.FirstSeq != 1 || request.LastSeq != 1 || len(request.Entries) != 1 {
|
||||
t.Fatalf("unexpected log ingest payload: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.LogBatchIngestResponse{Accepted: true, LogStreamID: "log-1", AcceptedFrom: 1, AcceptedTo: 1, LatestSeq: 1, ServerTime: fixedClientTestTime()})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewPlatformClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
response, err := client.IngestLogBatch(context.Background(), validClientLogBatch())
|
||||
if err != nil {
|
||||
t.Fatalf("ingest log batch: %v", err)
|
||||
}
|
||||
if !response.Accepted || response.LatestSeq != 1 {
|
||||
t.Fatalf("unexpected log ingest response: %+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientIngestLogBatchReturnsErrorForPlatformFailure(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.IngestLogBatch(context.Background(), validClientLogBatch()); err == nil {
|
||||
t.Fatal("expected platform error")
|
||||
}
|
||||
}
|
||||
|
||||
func validClientLogBatch() protocol.LogBatchIngestRequest {
|
||||
return protocol.LogBatchIngestRequest{
|
||||
RunEndpointID: "run-local",
|
||||
SessionToken: "session-token",
|
||||
LogStreamID: "log-1",
|
||||
ServerInstanceID: "server-1",
|
||||
StreamKey: "stdout",
|
||||
Source: "process",
|
||||
FirstSeq: 1,
|
||||
LastSeq: 1,
|
||||
Compression: "none",
|
||||
Checksum: "sha256:test",
|
||||
Entries: []protocol.LogEntry{{
|
||||
Seq: 1,
|
||||
Timestamp: time.Date(2026, 7, 3, 12, 0, 1, 0, time.UTC),
|
||||
Level: "info",
|
||||
Line: "line",
|
||||
}},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
type PlatformClient struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewPlatformClient(rawURL string) (PlatformClient, error) {
|
||||
return NewPlatformClientWithHTTPClient(rawURL, http.DefaultClient)
|
||||
}
|
||||
|
||||
func NewPlatformClientWithHTTPClient(rawURL string, httpClient *http.Client) (PlatformClient, error) {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return PlatformClient{}, err
|
||||
}
|
||||
if parsed.Scheme == "" || parsed.Host == "" {
|
||||
return PlatformClient{}, fmt.Errorf("platform URL must include scheme and host")
|
||||
}
|
||||
if httpClient == nil {
|
||||
httpClient = http.DefaultClient
|
||||
}
|
||||
|
||||
return PlatformClient{baseURL: strings.TrimRight(parsed.String(), "/"), httpClient: httpClient}, nil
|
||||
}
|
||||
|
||||
func (c PlatformClient) BaseURL() string {
|
||||
return c.baseURL
|
||||
}
|
||||
|
||||
func (c PlatformClient) Hello(ctx context.Context, request protocol.RunHelloRequest) (protocol.RunHelloResponse, error) {
|
||||
return postPlatformJSON[protocol.RunHelloRequest, protocol.RunHelloResponse](ctx, c, "/api/v1/run/control/hello", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) Heartbeat(ctx context.Context, request protocol.RunHeartbeatRequest) (protocol.RunHeartbeatResponse, error) {
|
||||
return postPlatformJSON[protocol.RunHeartbeatRequest, protocol.RunHeartbeatResponse](ctx, c, "/api/v1/run/control/heartbeat", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) ClaimJob(ctx context.Context, request protocol.RunJobClaimRequest) (protocol.RunJobClaimResponse, error) {
|
||||
return postPlatformJSON[protocol.RunJobClaimRequest, protocol.RunJobClaimResponse](ctx, c, "/api/v1/run/jobs/claim", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) AckJob(ctx context.Context, request protocol.RunJobAckRequest) (protocol.RunJobAckResponse, error) {
|
||||
return postPlatformJSON[protocol.RunJobAckRequest, protocol.RunJobAckResponse](ctx, c, "/api/v1/run/jobs/ack", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) UpdateJobProgress(ctx context.Context, request protocol.RunJobProgressRequest) (protocol.RunJobProgressResponse, error) {
|
||||
return postPlatformJSON[protocol.RunJobProgressRequest, protocol.RunJobProgressResponse](ctx, c, "/api/v1/run/jobs/progress", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) CompleteJob(ctx context.Context, request protocol.RunJobResultRequest) (protocol.RunJobResultResponse, error) {
|
||||
return postPlatformJSON[protocol.RunJobResultRequest, protocol.RunJobResultResponse](ctx, c, "/api/v1/run/jobs/result", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) PollJobCancel(ctx context.Context, request protocol.RunJobCancelPollRequest) (protocol.RunJobCancelPollResponse, error) {
|
||||
return postPlatformJSON[protocol.RunJobCancelPollRequest, protocol.RunJobCancelPollResponse](ctx, c, "/api/v1/run/jobs/cancel", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) ReconcileJobs(ctx context.Context, request protocol.RunJobReconcileRequest) (protocol.RunJobReconcileResponse, error) {
|
||||
return postPlatformJSON[protocol.RunJobReconcileRequest, protocol.RunJobReconcileResponse](ctx, c, "/api/v1/run/jobs/reconcile", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) IngestLogBatch(ctx context.Context, request protocol.LogBatchIngestRequest) (protocol.LogBatchIngestResponse, error) {
|
||||
return postPlatformJSON[protocol.LogBatchIngestRequest, protocol.LogBatchIngestResponse](ctx, c, "/api/v1/run/logs/batches", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) OpenArtifactTransfer(ctx context.Context, request protocol.ArtifactTransferOpenRequest) (protocol.ArtifactTransferOpenResponse, error) {
|
||||
return postPlatformJSON[protocol.ArtifactTransferOpenRequest, protocol.ArtifactTransferOpenResponse](ctx, c, "/api/v1/run/artifacts/open", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) UploadArtifactChunk(ctx context.Context, request protocol.ArtifactChunkUploadRequest) (protocol.ArtifactChunkUploadResponse, error) {
|
||||
return postPlatformJSON[protocol.ArtifactChunkUploadRequest, protocol.ArtifactChunkUploadResponse](ctx, c, "/api/v1/run/artifacts/chunks", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) QueryArtifactTransferStatus(ctx context.Context, request protocol.ArtifactTransferStatusRequest) (protocol.ArtifactTransferStatusResponse, error) {
|
||||
return postPlatformJSON[protocol.ArtifactTransferStatusRequest, protocol.ArtifactTransferStatusResponse](ctx, c, "/api/v1/run/artifacts/status", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) CompleteArtifactTransfer(ctx context.Context, request protocol.ArtifactTransferCompleteRequest) (protocol.ArtifactTransferCompleteResponse, error) {
|
||||
return postPlatformJSON[protocol.ArtifactTransferCompleteRequest, protocol.ArtifactTransferCompleteResponse](ctx, c, "/api/v1/run/artifacts/complete", request)
|
||||
}
|
||||
|
||||
func postPlatformJSON[Request any, Response any](ctx context.Context, client PlatformClient, path string, request Request) (Response, error) {
|
||||
var response Response
|
||||
|
||||
var body bytes.Buffer
|
||||
if err := json.NewEncoder(&body).Encode(request); err != nil {
|
||||
return response, fmt.Errorf("encode platform request: %w", err)
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL+path, &body)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("build platform request: %w", err)
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResponse, err := client.httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("send platform request: %w", err)
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode < http.StatusOK || httpResponse.StatusCode >= http.StatusMultipleChoices {
|
||||
message, _ := io.ReadAll(io.LimitReader(httpResponse.Body, 4096))
|
||||
return response, fmt.Errorf("platform request failed: status=%d body=%s", httpResponse.StatusCode, strings.TrimSpace(string(message)))
|
||||
}
|
||||
if err := json.NewDecoder(httpResponse.Body).Decode(&response); err != nil {
|
||||
return response, fmt.Errorf("decode platform response: %w", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
func TestNewPlatformClientNormalizesBaseURL(t *testing.T) {
|
||||
client, err := NewPlatformClient("http://platform.test/")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if client.BaseURL() != "http://platform.test" {
|
||||
t.Fatalf("expected normalized base URL, got %q", client.BaseURL())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPlatformClientRequiresAbsoluteURL(t *testing.T) {
|
||||
if _, err := NewPlatformClient("platform.local"); err == nil {
|
||||
t.Fatal("expected error for URL without scheme and host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientHelloPostsJSONAndDecodesResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/run/control/hello" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
|
||||
t.Fatalf("expected JSON content type, got %q", contentType)
|
||||
}
|
||||
var request protocol.RunHelloRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatalf("decode hello request: %v", err)
|
||||
}
|
||||
if request.RunEndpointID != "run-local" || request.CapabilityReport.Fingerprint != "cap-v1" {
|
||||
t.Fatalf("unexpected hello payload: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.RunHelloResponse{
|
||||
Accepted: true,
|
||||
RunEndpointID: "run-local",
|
||||
SessionToken: "session-token",
|
||||
ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC),
|
||||
HeartbeatIntervalSeconds: 15,
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewPlatformClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
response, err := client.Hello(context.Background(), validRunHelloRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("hello: %v", err)
|
||||
}
|
||||
if !response.Accepted || response.SessionToken != "session-token" || response.HeartbeatIntervalSeconds != 15 {
|
||||
t.Fatalf("unexpected hello response: %+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientHeartbeatPostsJSONAndDecodesResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/run/control/heartbeat" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var request protocol.RunHeartbeatRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatalf("decode heartbeat request: %v", err)
|
||||
}
|
||||
if request.SessionToken != "session-token" || request.Capacity.RunningJobs != 1 {
|
||||
t.Fatalf("unexpected heartbeat payload: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.RunHeartbeatResponse{
|
||||
Accepted: true,
|
||||
RunEndpointID: "run-local",
|
||||
NextHeartbeatSeconds: 15,
|
||||
RefreshCapabilities: true,
|
||||
ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC),
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewPlatformClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
response, err := client.Heartbeat(context.Background(), validRunHeartbeatRequest("session-token"))
|
||||
if err != nil {
|
||||
t.Fatalf("heartbeat: %v", err)
|
||||
}
|
||||
if !response.Accepted || !response.RefreshCapabilities || response.NextHeartbeatSeconds != 15 {
|
||||
t.Fatalf("unexpected heartbeat response: %+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientReturnsErrorForPlatformFailure(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.Hello(context.Background(), validRunHelloRequest()); err == nil {
|
||||
t.Fatal("expected platform error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformClientHelloThenHeartbeatFlow(t *testing.T) {
|
||||
var activeSessionToken string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/run/control/hello":
|
||||
var request protocol.RunHelloRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatalf("decode hello request: %v", err)
|
||||
}
|
||||
if request.RegistrationToken == "" || request.RunEndpointID != "run-local" {
|
||||
t.Fatalf("unexpected hello request: %+v", request)
|
||||
}
|
||||
activeSessionToken = "session-token"
|
||||
writeTestJSON(t, w, protocol.RunHelloResponse{
|
||||
Accepted: true,
|
||||
RunEndpointID: request.RunEndpointID,
|
||||
SessionToken: activeSessionToken,
|
||||
ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC),
|
||||
HeartbeatIntervalSeconds: 15,
|
||||
})
|
||||
case "/api/v1/run/control/heartbeat":
|
||||
var request protocol.RunHeartbeatRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatalf("decode heartbeat request: %v", err)
|
||||
}
|
||||
if request.SessionToken != activeSessionToken {
|
||||
t.Fatalf("heartbeat did not use active session token: %+v", request)
|
||||
}
|
||||
writeTestJSON(t, w, protocol.RunHeartbeatResponse{
|
||||
Accepted: true,
|
||||
RunEndpointID: request.RunEndpointID,
|
||||
NextHeartbeatSeconds: 15,
|
||||
ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC),
|
||||
})
|
||||
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)
|
||||
}
|
||||
hello, err := client.Hello(context.Background(), validRunHelloRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("hello: %v", err)
|
||||
}
|
||||
heartbeat, err := client.Heartbeat(context.Background(), validRunHeartbeatRequest(hello.SessionToken))
|
||||
if err != nil {
|
||||
t.Fatalf("heartbeat: %v", err)
|
||||
}
|
||||
if !hello.Accepted || !heartbeat.Accepted {
|
||||
t.Fatalf("expected accepted hello and heartbeat, got %+v %+v", hello, heartbeat)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTestJSON(t *testing.T, w http.ResponseWriter, value any) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(value); err != nil {
|
||||
t.Fatalf("encode response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validRunHelloRequest() protocol.RunHelloRequest {
|
||||
return protocol.RunHelloRequest{
|
||||
RegistrationToken: "registration-token",
|
||||
RunEndpointID: "run-local",
|
||||
DisplayName: "Local Run",
|
||||
Version: "0.1.0",
|
||||
Status: "online",
|
||||
Platform: "darwin/arm64",
|
||||
CapabilityReport: protocol.RunCapabilityReport{
|
||||
Capabilities: []string{"control.hello", "control.heartbeat"},
|
||||
Fingerprint: "cap-v1",
|
||||
},
|
||||
Capacity: protocol.RunCapacityReport{MaxJobs: 4},
|
||||
}
|
||||
}
|
||||
|
||||
func validRunHeartbeatRequest(sessionToken string) protocol.RunHeartbeatRequest {
|
||||
return protocol.RunHeartbeatRequest{
|
||||
RunEndpointID: "run-local",
|
||||
SessionToken: sessionToken,
|
||||
Version: "0.1.0",
|
||||
Status: "online",
|
||||
CapabilityFingerprint: "cap-v1",
|
||||
Capacity: protocol.RunCapacityReport{
|
||||
MaxJobs: 4,
|
||||
RunningJobs: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user