Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.25.5

require (
github.com/bluele/gcache v0.0.2
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260510070250-0340ac6a5a33
github.com/flashcatcloud/flashduty-sdk v0.9.1
github.com/google/go-github/v72 v72.0.0
github.com/josephburnett/jd v1.9.2
github.com/mark3labs/mcp-go v0.52.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260510070250-0340ac6a5a33 h1:HnB++VulEnF+oIsNwKK8QBv4CCdG+ztdFKLScI4bXlc=
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260510070250-0340ac6a5a33/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
github.com/flashcatcloud/flashduty-sdk v0.9.1 h1:vDTkSjAJJD6Ex5r7S+VCxPi4yxSFNw1bU/SfoRCvk+k=
github.com/flashcatcloud/flashduty-sdk v0.9.1/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
Expand Down
93 changes: 93 additions & 0 deletions pkg/flashduty/marshal_time_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package flashduty

import (
"strconv"
"strings"
"testing"
"time"

sdk "github.com/flashcatcloud/flashduty-sdk"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// timeFixture is a small struct exercising both SDK timestamp types the way an
// SDK response object would carry them.
type timeFixture struct {
CreatedAt sdk.Timestamp `json:"created_at"`
UpdatedAt sdk.TimestampMilli `json:"updated_at"`
}

// resultText pulls the single text payload out of an MCP CallToolResult.
func resultText(t *testing.T, res *mcp.CallToolResult) string {
t.Helper()
require.NotNil(t, res)
require.False(t, res.IsError, "tool result reported an error: %+v", res.Content)
require.Len(t, res.Content, 1)
tc, ok := res.Content[0].(mcp.TextContent)
require.True(t, ok, "expected TextContent, got %T", res.Content[0])
return tc.Text
}

// TestMarshalResultRendersTimestampsAsRFC3339 proves the transparent win from
// the typed-timestamp SDK: mcp marshals SDK results straight through and gets
// RFC3339 strings out, never raw epoch integers — with no mcp-side code change.
func TestMarshalResultRendersTimestampsAsRFC3339(t *testing.T) {
t.Parallel()

// A concrete instant well clear of the epoch so the year is unambiguous.
secs := int64(1748487600) // 2025-ish; exact day depends on TZ, year does not.
wantYear := strconv.Itoa(time.Unix(secs, 0).Year())

fixture := timeFixture{
CreatedAt: sdk.Timestamp(secs),
UpdatedAt: sdk.TimestampMilli(secs * 1000),
}

formats := []struct {
name string
format OutputFormat
// JSON wraps the RFC3339 value in quotes; TOON leaves it bare. The
// year + 'T' separator are present in both.
text string
}{
{name: "default-json", format: GetOutputFormat()},
{name: "json", format: OutputFormatJSON},
{name: "toon", format: OutputFormatTOON},
}

for _, f := range formats {
f := f
t.Run(f.name, func(t *testing.T) {
t.Parallel()

out := resultText(t, MarshalResultWithFormat(fixture, f.format))

// RFC3339 shape: contains the date/time 'T' separator and the year.
assert.Contains(t, out, "T", "expected RFC3339 'T' separator in %q", out)
assert.Contains(t, out, wantYear, "expected RFC3339 year in %q", out)

// Negative assertion: the raw epoch integers must NOT appear.
assert.NotContains(t, out, strconv.FormatInt(secs, 10),
"raw epoch-seconds leaked into output %q", out)
assert.NotContains(t, out, strconv.FormatInt(secs*1000, 10),
"raw epoch-millis leaked into output %q", out)
})
}
}

// TestMarshalResultUsesGlobalFormat covers the no-arg MarshalResult path so the
// default-format wrapper is exercised too.
func TestMarshalResultUsesGlobalFormat(t *testing.T) {
secs := int64(1748487600)
wantYear := strconv.Itoa(time.Unix(secs, 0).Year())

fixture := timeFixture{CreatedAt: sdk.Timestamp(secs)}

out := resultText(t, MarshalResult(fixture))
assert.True(t, strings.Contains(out, wantYear) && strings.Contains(out, "T"),
"expected RFC3339 timestamp in default-format output %q", out)
assert.NotContains(t, out, strconv.FormatInt(secs, 10),
"raw epoch-seconds leaked into default-format output %q", out)
}
Loading