Skip to content

Commit 1a81429

Browse files
jonathanpophamgreynewell
authored andcommitted
fix: handle async 202 job polling and nested graph response
The /v1/graphs/supermodel endpoint returns HTTP 202 with an async job envelope (status, jobId, retryAfter, result). The client was treating this as a completed response and unmarshalling directly into Graph, resulting in 0 nodes/relationships. Changes: - Add JobResponse and jobResult types for the async envelope - Poll with the same idempotency key until status is "completed" - Extract graph from the nested result.graph path - Extract postZip helper to avoid duplicating multipart construction Tested live against api.supermodeltools.com — correctly returns 362 files, 1527 functions, 7741 relationships for supermodel-public-api.
1 parent cc523eb commit 1a81429

File tree

2 files changed

+65
-5
lines changed

2 files changed

+65
-5
lines changed

internal/api/client.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,52 @@ func New(cfg *config.Config) *Client {
3333
}
3434
}
3535

36+
// analyzeEndpoint is the API path for the supermodel graph analysis.
37+
const analyzeEndpoint = "/v1/graphs/supermodel"
38+
3639
// Analyze uploads a repository ZIP and runs the full analysis pipeline,
37-
// returning the DisplayGraphResponse.
40+
// polling until the async job completes and returning the Graph.
3841
func (c *Client) Analyze(ctx context.Context, zipPath, idempotencyKey string) (*Graph, error) {
42+
job, err := c.postZip(ctx, zipPath, idempotencyKey)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
// Poll until the job completes.
48+
for job.Status == "pending" || job.Status == "processing" {
49+
wait := time.Duration(job.RetryAfter) * time.Second
50+
if wait <= 0 {
51+
wait = 5 * time.Second
52+
}
53+
select {
54+
case <-ctx.Done():
55+
return nil, ctx.Err()
56+
case <-time.After(wait):
57+
}
58+
59+
job, err = c.postZip(ctx, zipPath, idempotencyKey)
60+
if err != nil {
61+
return nil, err
62+
}
63+
}
64+
65+
if job.Error != nil {
66+
return nil, fmt.Errorf("analysis failed: %s", *job.Error)
67+
}
68+
if job.Status != "completed" {
69+
return nil, fmt.Errorf("unexpected job status: %s", job.Status)
70+
}
71+
72+
var result jobResult
73+
if err := json.Unmarshal(job.Result, &result); err != nil {
74+
return nil, fmt.Errorf("decode graph result: %w", err)
75+
}
76+
return &result.Graph, nil
77+
}
78+
79+
// postZip sends the repository ZIP to the analyze endpoint and returns the
80+
// raw job response (which may be pending, processing, or completed).
81+
func (c *Client) postZip(ctx context.Context, zipPath, idempotencyKey string) (*JobResponse, error) {
3982
f, err := os.Open(zipPath)
4083
if err != nil {
4184
return nil, err
@@ -53,11 +96,11 @@ func (c *Client) Analyze(ctx context.Context, zipPath, idempotencyKey string) (*
5396
}
5497
mw.Close()
5598

56-
var g Graph
57-
if err := c.request(ctx, http.MethodPost, "/v1/supermodel", mw.FormDataContentType(), &buf, idempotencyKey, &g); err != nil {
99+
var job JobResponse
100+
if err := c.request(ctx, http.MethodPost, analyzeEndpoint, mw.FormDataContentType(), &buf, idempotencyKey, &job); err != nil {
58101
return nil, err
59102
}
60-
return &g, nil
103+
return &job, nil
61104
}
62105

63106
// DisplayGraph fetches the composed display graph for an already-analyzed repo.

internal/api/types.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package api
22

3-
import "fmt"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
47

58
// Node represents a graph node returned by the Supermodel API.
69
type Node struct {
@@ -90,6 +93,20 @@ func (g *Graph) NodeByID(id string) (Node, bool) {
9093
return Node{}, false
9194
}
9295

96+
// JobResponse is the async envelope returned by the API for long-running jobs.
97+
type JobResponse struct {
98+
Status string `json:"status"`
99+
JobID string `json:"jobId"`
100+
RetryAfter int `json:"retryAfter"`
101+
Error *string `json:"error"`
102+
Result json.RawMessage `json:"result"`
103+
}
104+
105+
// jobResult is the inner result object containing the graph.
106+
type jobResult struct {
107+
Graph Graph `json:"graph"`
108+
}
109+
93110
// Error represents a non-2xx response from the API.
94111
type Error struct {
95112
StatusCode int `json:"-"`

0 commit comments

Comments
 (0)