Skip to content

Commit fce24ab

Browse files
Dumbrisclaude
andcommitted
fix(status): mirror checked_at/is_prerelease in status update object; mark FR-002 delta follow-up
Review follow-ups on the US1 status slice (specs/079-upgrade-nudge): - StatusUpdateInfo now carries checked_at (RFC 3339 string, staleness signal for consumers of `status -o json`) and is_prerelease, so it is an accurate mirror of the /api/v1/info update object as the code comment and docs claim (FR-019/FR-021). Extraction + JSON wire-format tests extended; docs example updated. - Added explicit TODO(spec-079/FR-002) markers at the two surfaces that will grow the "N releases / M weeks behind" delta (statusVersionSuffix and the checker's one-shot Info log). The delta needs the release list + publish dates, which the checker does not fetch yet — owned by a later 079 slice, intentionally not in this one. - FR-004 once-per-version (vs once-per-process) Info announce is kept as-is: the operative clause is "MUST NOT repeatedly log the same availability on a timer", and per-version re-announce matches the Web UI banner's per-version dismissal semantics (FR-005); a genuinely newer release is new information. Locked in by checker_test.go. Testing: go test -race ./internal/updatecheck/... ./cmd/mcpproxy/...; golangci-lint v2 (.github/.golangci.yml) on touched packages: clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2eeabd7 commit fce24ab

4 files changed

Lines changed: 47 additions & 10 deletions

File tree

cmd/mcpproxy/status_cmd.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ type StatusUpdateInfo struct {
4545
Available bool `json:"available"`
4646
LatestVersion string `json:"latest_version,omitempty"`
4747
ReleaseURL string `json:"release_url,omitempty"`
48+
CheckedAt string `json:"checked_at,omitempty"` // RFC 3339, as serialized by the daemon
49+
IsPrerelease bool `json:"is_prerelease,omitempty"`
4850
CheckError string `json:"check_error,omitempty"`
4951
}
5052

@@ -300,6 +302,12 @@ func extractStatusUpdate(infoData map[string]interface{}) *StatusUpdateInfo {
300302
if v, ok := updateData["release_url"].(string); ok {
301303
u.ReleaseURL = v
302304
}
305+
if v, ok := updateData["checked_at"].(string); ok {
306+
u.CheckedAt = v
307+
}
308+
if v, ok := updateData["is_prerelease"].(bool); ok {
309+
u.IsPrerelease = v
310+
}
303311
if v, ok := updateData["check_error"].(string); ok {
304312
u.CheckError = v
305313
}
@@ -309,6 +317,11 @@ func extractStatusUpdate(infoData map[string]interface{}) *StatusUpdateInfo {
309317
// statusVersionSuffix renders the update annotation appended to the Version
310318
// line, mirroring doctor's presentation. A failed or not-yet-completed check
311319
// renders nothing (quiet on failure; the error stays in JSON for diagnostics).
320+
//
321+
// TODO(spec-079/FR-002): extend the annotation with the human-readable
322+
// "N releases / M weeks behind" delta once internal/updatecheck computes it
323+
// (requires the release list + publish dates, not just the latest release;
324+
// additive per FR-021). This function is the single rendering point.
312325
func statusVersionSuffix(u *StatusUpdateInfo) string {
313326
if u == nil || u.CheckError != "" {
314327
return ""

cmd/mcpproxy/status_update_test.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,10 @@ func TestExtractStatusUpdate(t *testing.T) {
153153
"version": "v0.45.0",
154154
"update": map[string]interface{}{
155155
"available": true,
156-
"latest_version": "v0.46.0",
157-
"release_url": "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/tag/v0.46.0",
158-
"is_prerelease": false,
156+
"latest_version": "v0.46.0-rc.1",
157+
"release_url": "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/tag/v0.46.0-rc.1",
158+
"checked_at": "2026-07-02T10:00:00Z",
159+
"is_prerelease": true,
159160
},
160161
}
161162

@@ -166,12 +167,18 @@ func TestExtractStatusUpdate(t *testing.T) {
166167
if !u.Available {
167168
t.Error("expected Available=true")
168169
}
169-
if u.LatestVersion != "v0.46.0" {
170-
t.Errorf("expected LatestVersion v0.46.0, got %q", u.LatestVersion)
170+
if u.LatestVersion != "v0.46.0-rc.1" {
171+
t.Errorf("expected LatestVersion v0.46.0-rc.1, got %q", u.LatestVersion)
171172
}
172-
if u.ReleaseURL != "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/tag/v0.46.0" {
173+
if u.ReleaseURL != "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/tag/v0.46.0-rc.1" {
173174
t.Errorf("unexpected ReleaseURL %q", u.ReleaseURL)
174175
}
176+
if u.CheckedAt != "2026-07-02T10:00:00Z" {
177+
t.Errorf("expected CheckedAt to be preserved, got %q", u.CheckedAt)
178+
}
179+
if !u.IsPrerelease {
180+
t.Error("expected IsPrerelease=true")
181+
}
175182
})
176183

177184
t.Run("check error preserved for machine output", func(t *testing.T) {
@@ -210,6 +217,8 @@ func TestStatusJSONIncludesUpdate(t *testing.T) {
210217
Available: true,
211218
LatestVersion: "v0.46.0",
212219
ReleaseURL: "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/tag/v0.46.0",
220+
CheckedAt: "2026-07-02T10:00:00Z",
221+
IsPrerelease: true,
213222
},
214223
}
215224

@@ -234,13 +243,21 @@ func TestStatusJSONIncludesUpdate(t *testing.T) {
234243
t.Errorf("expected update.latest_version v0.46.0, got %q", result.Update.LatestVersion)
235244
}
236245

237-
// Field name in the wire format must be "update" (snake_case contract).
246+
// Field names in the wire format must match the /api/v1/info update
247+
// object (snake_case contract, FR-021).
238248
var raw map[string]interface{}
239249
if err := json.Unmarshal([]byte(output), &raw); err != nil {
240250
t.Fatalf("invalid JSON: %v", err)
241251
}
242-
if _, ok := raw["update"]; !ok {
243-
t.Error("expected raw JSON key 'update'")
252+
rawUpdate, ok := raw["update"].(map[string]interface{})
253+
if !ok {
254+
t.Fatal("expected raw JSON key 'update'")
255+
}
256+
if v, ok := rawUpdate["checked_at"].(string); !ok || v != "2026-07-02T10:00:00Z" {
257+
t.Errorf("expected raw JSON key 'update.checked_at', got %v", rawUpdate["checked_at"])
258+
}
259+
if v, ok := rawUpdate["is_prerelease"].(bool); !ok || !v {
260+
t.Errorf("expected raw JSON key 'update.is_prerelease'=true, got %v", rawUpdate["is_prerelease"])
244261
}
245262
}
246263

docs/cli/status-command.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ mcpproxy status -o json
138138
"update": {
139139
"available": true,
140140
"latest_version": "v1.3.0",
141-
"release_url": "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/tag/v1.3.0"
141+
"release_url": "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/tag/v1.3.0",
142+
"checked_at": "2026-07-02T10:00:00Z"
142143
}
143144
}
144145
```
@@ -151,6 +152,8 @@ When the daemon is running, `status` surfaces the result of the background updat
151152
- **Up to date**: `Version: v1.3.0 (latest)`
152153
- **Check failed or not yet completed** (offline, rate-limited): the version is shown without any annotation. In JSON output the `update.check_error` field retains the failure reason for diagnostics.
153154

155+
In machine-readable output (`-o json`/`-o yaml`) the `update` object also carries `checked_at` (when the last successful check ran, so consumers can judge staleness) and `is_prerelease` (whether the offered version is a prerelease), matching the `/api/v1/info` contract.
156+
154157
## API Key Masking
155158

156159
By default, the API key is masked showing only the first 4 and last 4 characters:

internal/updatecheck/checker.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ func (c *Checker) updateVersionInfo(release *GitHubRelease, checkError string) {
167167
case updateAvailable && latestVersion != c.announcedVersion:
168168
// Announce each newly detected version exactly once per process;
169169
// subsequent ticks for the same version log at Debug only.
170+
// TODO(spec-079/FR-002): include the "N releases / M weeks behind"
171+
// delta here once the checker fetches the release list + publish
172+
// dates (a later 079 slice extending VersionInfo, additive per
173+
// FR-021).
170174
c.announcedVersion = latestVersion
171175
c.logger.Info("Update available",
172176
zap.String("current", c.version),

0 commit comments

Comments
 (0)