Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions go/ping_response_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package copilot

import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"time"
)

// UnmarshalJSON accepts both current ISO-8601 ping timestamps and older
// epoch-millisecond timestamps emitted by some CLI builds.
func (p *PingResponse) UnmarshalJSON(data []byte) error {
var wire struct {
Message string `json:"message"`
ProtocolVersion *int `json:"protocolVersion,omitempty"`
Timestamp json.RawMessage `json:"timestamp"`
}
if err := json.Unmarshal(data, &wire); err != nil {
return err
}

timestamp, err := parsePingTimestamp(wire.Timestamp)
if err != nil {
return err
}

p.Message = wire.Message
p.ProtocolVersion = wire.ProtocolVersion
p.Timestamp = timestamp
return nil
}

func parsePingTimestamp(raw json.RawMessage) (time.Time, error) {
raw = bytes.TrimSpace(raw)
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
return time.Time{}, nil
}

if raw[0] == '"' {
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return time.Time{}, fmt.Errorf("ping timestamp: %w", err)
}
return parsePingTimestampString(value)
}

var number json.Number
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
if err := decoder.Decode(&number); err != nil {
return time.Time{}, fmt.Errorf("ping timestamp: %w", err)
}
return parsePingTimestampNumber(number)
}

func parsePingTimestampString(value string) (time.Time, error) {
if milliseconds, err := strconv.ParseInt(value, 10, 64); err == nil {
return time.UnixMilli(milliseconds), nil
}
if timestamp, err := time.Parse(time.RFC3339Nano, value); err == nil {
return timestamp, nil
}
return time.Time{}, fmt.Errorf("ping timestamp: unsupported string value %q", value)
}

func parsePingTimestampNumber(number json.Number) (time.Time, error) {
if milliseconds, err := number.Int64(); err == nil {
return time.UnixMilli(milliseconds), nil
}
value, err := strconv.ParseFloat(number.String(), 64)
if err != nil {
return time.Time{}, fmt.Errorf("ping timestamp: %w", err)
}
return time.UnixMilli(int64(value)), nil
}
40 changes: 40 additions & 0 deletions go/rpc/generated_rpc_union_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"io"
"testing"
"time"

"github.com/github/copilot-sdk/go/internal/jsonrpc2"
)
Expand Down Expand Up @@ -46,6 +47,45 @@ func TestExternalToolResultJSONUnion(t *testing.T) {
}
}

func TestPingResultUnmarshalsIsoTimestamp(t *testing.T) {
var result PingResult
if err := json.Unmarshal([]byte(`{"message":"pong","timestamp":"2026-05-21T08:29:54.042Z","protocolVersion":3}`), &result); err != nil {
t.Fatalf("unmarshal ping result: %v", err)
}

expected := time.Date(2026, 5, 21, 8, 29, 54, 42_000_000, time.UTC)
if !result.Timestamp.Equal(expected) {
t.Fatalf("timestamp = %s, want %s", result.Timestamp, expected)
}
if result.ProtocolVersion != 3 {
t.Fatalf("protocol version = %d, want 3", result.ProtocolVersion)
}
}

func TestPingResultUnmarshalsEpochMillisecondsTimestamp(t *testing.T) {
var result PingResult
if err := json.Unmarshal([]byte(`{"message":"pong","timestamp":1779352370134,"protocolVersion":3}`), &result); err != nil {
t.Fatalf("unmarshal ping result: %v", err)
}

expected := time.UnixMilli(1779352370134)
if !result.Timestamp.Equal(expected) {
t.Fatalf("timestamp = %s, want %s", result.Timestamp, expected)
}
}

func TestPingResultUnmarshalsStringEpochMillisecondsTimestamp(t *testing.T) {
var result PingResult
if err := json.Unmarshal([]byte(`{"message":"pong","timestamp":"1779352370134","protocolVersion":3}`), &result); err != nil {
t.Fatalf("unmarshal ping result: %v", err)
}

expected := time.UnixMilli(1779352370134)
if !result.Timestamp.Equal(expected) {
t.Fatalf("timestamp = %s, want %s", result.Timestamp, expected)
}
}

func TestFilterMappingJSONUnion(t *testing.T) {
var mapping FilterMapping = FilterMappingEnumMap{"secret": ContentFilterModeHiddenCharacters}
raw, err := json.Marshal(mapping)
Expand Down
76 changes: 76 additions & 0 deletions go/rpc/ping_result_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package rpc

import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"time"
)

// UnmarshalJSON accepts both current ISO-8601 ping timestamps and older
// epoch-millisecond timestamps emitted by some CLI builds.
func (p *PingResult) UnmarshalJSON(data []byte) error {
var wire struct {
Message string `json:"message"`
ProtocolVersion int64 `json:"protocolVersion"`
Timestamp json.RawMessage `json:"timestamp"`
}
if err := json.Unmarshal(data, &wire); err != nil {
return err
}

timestamp, err := parsePingTimestamp(wire.Timestamp)
if err != nil {
return err
}

p.Message = wire.Message
p.ProtocolVersion = wire.ProtocolVersion
p.Timestamp = timestamp
return nil
}

func parsePingTimestamp(raw json.RawMessage) (time.Time, error) {
raw = bytes.TrimSpace(raw)
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
return time.Time{}, nil
}

if raw[0] == '"' {
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return time.Time{}, fmt.Errorf("ping timestamp: %w", err)
}
return parsePingTimestampString(value)
}

var number json.Number
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
if err := decoder.Decode(&number); err != nil {
return time.Time{}, fmt.Errorf("ping timestamp: %w", err)
}
return parsePingTimestampNumber(number)
}

func parsePingTimestampString(value string) (time.Time, error) {
if milliseconds, err := strconv.ParseInt(value, 10, 64); err == nil {
return time.UnixMilli(milliseconds), nil
}
if timestamp, err := time.Parse(time.RFC3339Nano, value); err == nil {
return timestamp, nil
}
return time.Time{}, fmt.Errorf("ping timestamp: unsupported string value %q", value)
}

func parsePingTimestampNumber(number json.Number) (time.Time, error) {
if milliseconds, err := number.Int64(); err == nil {
return time.UnixMilli(milliseconds), nil
}
value, err := strconv.ParseFloat(number.String(), 64)
if err != nil {
return time.Time{}, fmt.Errorf("ping timestamp: %w", err)
}
return time.UnixMilli(int64(value)), nil
}
40 changes: 40 additions & 0 deletions go/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package copilot
import (
"encoding/json"
"testing"
"time"
)

func TestProviderConfig_JSONIncludesHeaders(t *testing.T) {
Expand Down Expand Up @@ -33,6 +34,45 @@ func TestProviderConfig_JSONIncludesHeaders(t *testing.T) {
}
}

func TestPingResponseUnmarshalsIsoTimestamp(t *testing.T) {
var response PingResponse
if err := json.Unmarshal([]byte(`{"message":"pong","timestamp":"2026-05-21T08:29:54.042Z","protocolVersion":3}`), &response); err != nil {
t.Fatalf("unmarshal ping response: %v", err)
}

expected := time.Date(2026, 5, 21, 8, 29, 54, 42_000_000, time.UTC)
if !response.Timestamp.Equal(expected) {
t.Fatalf("timestamp = %s, want %s", response.Timestamp, expected)
}
if response.ProtocolVersion == nil || *response.ProtocolVersion != 3 {
t.Fatalf("protocol version = %v, want 3", response.ProtocolVersion)
}
}

func TestPingResponseUnmarshalsEpochMillisecondsTimestamp(t *testing.T) {
var response PingResponse
if err := json.Unmarshal([]byte(`{"message":"pong","timestamp":1779352370134,"protocolVersion":3}`), &response); err != nil {
t.Fatalf("unmarshal ping response: %v", err)
}

expected := time.UnixMilli(1779352370134)
if !response.Timestamp.Equal(expected) {
t.Fatalf("timestamp = %s, want %s", response.Timestamp, expected)
}
}

func TestPingResponseUnmarshalsStringEpochMillisecondsTimestamp(t *testing.T) {
var response PingResponse
if err := json.Unmarshal([]byte(`{"message":"pong","timestamp":"1779352370134","protocolVersion":3}`), &response); err != nil {
t.Fatalf("unmarshal ping response: %v", err)
}

expected := time.UnixMilli(1779352370134)
if !response.Timestamp.Equal(expected) {
t.Fatalf("timestamp = %s, want %s", response.Timestamp, expected)
}
}

func TestSessionSendRequest_JSONIncludesRequestHeaders(t *testing.T) {
req := sessionSendRequest{
SessionID: "session-1",
Expand Down