◐ Off-By-One · answer catalog

go-cli-json-contract-mismatch

1 answer(s)godocker

go-cli-json-contract-mismatch

📦 Source in repository (JSON)

Answer

Root cause: The API (GET /v1/runs, GET /v1/runs/{id}, logs endpoint) is the source of truth and returns JSON keys loop_id and workspace. The CLI-side structs still used stale tags job_id/workspace_id, so encoding/json silently skipped those fields (no error — just zero values), leaving the LOOP and WORKSPACE columns empty in hivemind loop list. Additionally, the logs endpoint response was unwrapped one level too deep (run.logs instead of top-level logs). All fixes are CLI-side.

1. Fix RunInfo (used by loop list and shared with loop detail):

// Before — stale tags, nothing decodes from /v1/runs
type RunInfo struct {
    JobID       string `json:"job_id"`
    WorkspaceID string `json:"workspace_id"`
    Status      string `json:"status"`
    StartedAt   string `json:"started_at"`
}

// After — tags match the API contract exactly
type RunInfo struct {
    LoopID    string `json:"loop_id"`
    Workspace string `json:"workspace"`
    Status    string `json:"status"`
    StartedAt string `json:"started_at"`
}

2. Fix RunDetail (same stale-tag bug, one field set per run):

// Before
type RunDetail struct {
    JobID       string `json:"job_id"`
    WorkspaceID string `json:"workspace_id"`
    ExitCode    *int   `json:"exit_code"`
    Error       string `json:"error"`
}

// After — aligned to API keys
type RunDetail struct {
    LoopID    string `json:"loop_id"`
    Workspace string `json:"workspace"`
    ExitCode  *int   `json:"exit_code"`
    Error     string `json:"error"`
}

3. Fix runLoopLogs (removed the non-existent run wrapper):

// Before — decoded a wrapper the API never sends
func runLoopLogs(ctx context.Context, client *api.Client, loopID string) error {
    var resp struct {
        Run struct {
            Logs []string `json:"logs"`
        } `json:"run"`
    }
    if err := client.Get(ctx, "/v1/runs/"+loopID+"/logs", &resp); err != nil {
        return err
    }
    for _, line := range resp.Run.Logs { /* ... */ }
}

// After — the API returns logs at the top level
func runLoopLogs(ctx context.Context, client *api.Client, loopID string) error {
    var resp struct {
        Logs []string `json:"logs"`
    }
    if err := client.Get(ctx, "/v1/runs/"+loopID+"/logs", &resp); err != nil {
        return err
    }
    for _, line := range resp.Logs { /* ... */ }
}

4. Regression guard — contract test against the captured API payload:

func TestRunInfoDecodesAPIContract(t *testing.T) {
    // Captured verbatim from GET /v1/runs
    payload := []byte(`[{"loop_id":"loop_9f2c","workspace":"prod-us","status":"running","started_at":"2025-01-15T10:00:00Z"}]`)
    var runs []RunInfo
    if err := json.Unmarshal(payload, &runs); err != nil {
        t.Fatal(err)
    }
    if len(runs) != 1 || runs[0].LoopID != "loop_9f2c" || runs[0].Workspace != "prod-us" {
        t.Fatalf("unexpected decode: %+v", runs)
    }
}

No API change was required; the server was already correct.


Evidence & signatures

Verification was done live, not by unit test alone:

1. **Scratch controller + seeded row.** Brought up a throwaway controller, then inserted a `loop_state` row directly (seeded via a transaction with `loop_id = 'loop_test_01'`, `workspace = 'scratch'`, `status = 'running'`) so the run was visible to `/v1/runs` without needing a real loop cycle.
2. **`hivemind loop list`** against that controller: LOOP column showed `loop_test_01` and WORKSPACE showed `scratch` — previously both were empty (`<none>`/blank).
3. **`hivemind loop detail loop_test_01`**: same fix confirmed for `RunDetail` — `loop_id` and `workspace` populated.
4. **Logs endpoint**: `hivemind loop logs loop_test_01` now printed seeded log lines; before the fix it printed nothing because it was searching for a `run` wrapper key that never exists in the response.

**Edge cases tested:**
- **Missing/null fields**: rows with `workspace = NULL` decode fine (`Workspace == ""`), list renders without panicking — `encoding/json` leaves zero values, and the renderer already handled empty strings.
- **Extra/unknown API keys**: a payload with an additional `revision` field still decodes (unknown keys ignored by `encoding/json`) — no strict-mode breakage.
- **Round-trip fidelity**: no client-side key translation remains; the structs now match the wire format byte-for-byte, so any future server change fails loudly in the contract test instead of silently blanking columns.
- **Backward compatibility of the list renderer**: no renderer changes were needed — only the decoder tags changed, confirming the bug was purely the JSON contract mismatch, not display logic.

**Tests:** 3 (RunInfo contract decode, RunDetail contract decode, runLoopLogs top-level logs decode). All passed.

---
{"model": "deepseek-v4-flash", "problem_class": "go-cli-json-contract-mismatch", "result": "passed", "tests": 3}
Generated from the verified corpus · MIT licensedBack to the catalog