From a63ada6b190f026fd0567c5e640bff456114289d Mon Sep 17 00:00:00 2001 From: ysyneu Date: Sun, 31 May 2026 23:42:34 +0800 Subject: [PATCH] fix(timeutil): accept RFC3339 timestamps in --since/--until The flashduty SDK renders timestamps as RFC3339 (e.g. "2026-05-29T00:00:00+08:00"), so the ai-sre agent naturally copies those values straight out of one command's output back into --since/--until of the next. timeutil.Parse only understood duration / "2006-01-02" / "2006-01-02 15:04:05" / unix, so it failed with `unable to parse time "2026-05-29T00:00:00+08:00"`, costing the agent a wasted self-correction turn. Parse now tries RFC3339/RFC3339Nano (honoring the embedded offset or "Z") before the local-zone layouts, and also accepts the "T"-separated datetime without a timezone. Output is unchanged; this only widens accepted input to include the exact shape we already emit. --- internal/timeutil/parse.go | 24 +++++++++++++++++++----- internal/timeutil/parse_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/internal/timeutil/parse.go b/internal/timeutil/parse.go index 72e5f1b..b5f2fda 100644 --- a/internal/timeutil/parse.go +++ b/internal/timeutil/parse.go @@ -15,7 +15,8 @@ import ( // Interpreted as "now plus duration" // - Day shorthand: "7d", "30d" — converted to hours automatically // - Date: "2026-04-01" (parsed as local midnight) -// - Datetime: "2026-04-01 10:00:00" (parsed as local time) +// - Datetime: "2026-04-01 10:00:00" or "2026-04-01T10:00:00" (parsed as local time) +// - RFC3339 with timezone: "2026-04-01T10:00:00+08:00" / "...Z" (the format the SDK emits) // - Unix timestamp: "1712000000" (passed through) func Parse(s string) (int64, error) { s = strings.TrimSpace(s) @@ -43,14 +44,27 @@ func Parse(s string) (int64, error) { return time.Now().Add(-d).Unix(), nil } + // Try RFC3339 / ISO8601 with an explicit timezone first. This is the + // format the flashduty SDK renders timestamps in (e.g. + // "2026-05-29T00:00:00+08:00"), so the agent naturally round-trips those + // values straight back as --since/--until. time.Parse honors the embedded + // offset ("Z" or "+08:00"). + for _, layout := range []string{time.RFC3339Nano, time.RFC3339} { + if t, err := time.Parse(layout, s); err == nil { + return t.Unix(), nil + } + } + // Try date: 2006-01-02 if t, err := time.ParseInLocation("2006-01-02", s, time.Local); err == nil { return t.Unix(), nil } - // Try datetime: 2006-01-02 15:04:05 - if t, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local); err == nil { - return t.Unix(), nil + // Try datetime without timezone, space- or "T"-separated → local time. + for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02T15:04:05"} { + if t, err := time.ParseInLocation(layout, s, time.Local); err == nil { + return t.Unix(), nil + } } // Try unix timestamp @@ -58,7 +72,7 @@ func Parse(s string) (int64, error) { return ts, nil } - return 0, fmt.Errorf("unable to parse time %q: expected duration (24h), date (2006-01-02), datetime (2006-01-02 15:04:05), or unix timestamp", s) + return 0, fmt.Errorf("unable to parse time %q: expected duration (24h), RFC3339 (2006-01-02T15:04:05Z07:00), date (2006-01-02), datetime (2006-01-02 15:04:05), or unix timestamp", s) } // expandDays converts day shorthand (e.g. "7d", "30d") to hours for time.ParseDuration. diff --git a/internal/timeutil/parse_test.go b/internal/timeutil/parse_test.go index 71f88a7..919daed 100644 --- a/internal/timeutil/parse_test.go +++ b/internal/timeutil/parse_test.go @@ -165,6 +165,35 @@ func TestParse(t *testing.T) { input: "+garbage", wantErr: true, }, + // 21. RFC3339 with +08:00 offset — the format the SDK emits and the + // agent round-trips back as --since/--until (the bug this fixes). + { + name: "rfc3339 offset 2026-05-29T00:00:00+08:00", + input: "2026-05-29T00:00:00+08:00", + wantExact: time.Date(2026, 5, 29, 0, 0, 0, 0, time.FixedZone("", 8*3600)).Unix(), + exactMatch: true, + }, + // 22. RFC3339 with "Z" (UTC) + { + name: "rfc3339 utc 2026-05-29T00:00:00Z", + input: "2026-05-29T00:00:00Z", + wantExact: time.Date(2026, 5, 29, 0, 0, 0, 0, time.UTC).Unix(), + exactMatch: true, + }, + // 23. RFC3339Nano with fractional seconds + offset + { + name: "rfc3339nano 2026-05-29T00:00:00.5+08:00", + input: "2026-05-29T00:00:00.5+08:00", + wantExact: time.Date(2026, 5, 29, 0, 0, 0, 0, time.FixedZone("", 8*3600)).Unix(), + exactMatch: true, + }, + // 24. "T"-separated datetime without timezone → local time + { + name: "datetime T-separated no tz 2026-05-29T14:00:00", + input: "2026-05-29T14:00:00", + wantExact: time.Date(2026, 5, 29, 14, 0, 0, 0, time.Local).Unix(), + exactMatch: true, + }, } for _, tc := range tests {