Skip to content

Conversation

@jonathanpopham
Copy link
Collaborator

@jonathanpopham jonathanpopham commented Jan 23, 2026

Summary

  • Adds async polling loop to handle the new job-based API response envelope introduced in supermodeltools/supermodel-public-api#328
  • Polls with the same idempotency key until job status is completed or failed
  • Respects retryAfter field for polling interval, with a 5-minute max timeout
  • Extracts graph data from response.result.graph (with fallback to response.graph for backward compatibility)
  • Updates integration tests to use the same polling pattern

Test plan

  • Verify build compiles cleanly (npm run build)
  • Run integration tests against staging API with SUPERMODEL_API_KEY set
  • Confirm polling logs appear during job processing
  • Confirm dead code analysis produces correct results after polling completes

Closes #6

Summary by CodeRabbit

  • Improvements
    • Graph generation now uses an asynchronous poll-based workflow for more reliable completion handling.
    • Better handling of intermediate processing states and clearer error reporting on failures.
    • Increased timeouts to accommodate longer-running analyses and reduce spurious timeouts.
    • Enhanced diagnostic logging for progress and final node/relationship counts to aid troubleshooting.

✏️ Tip: You can customize this high-level summary in your review settings.

The Supermodel API now returns async job envelopes instead of
synchronous graph responses. This adds a polling loop that waits
for job completion before extracting graph data.

Closes #6
@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

Walkthrough

Replaced synchronous graph generation with an async poll-based workflow: initial API returns a status envelope; code polls until completion, extracts graph from response.result.graph (fallback response.graph), and tests updated to poll and assert on the resolved graph. Timeouts increased.

Changes

Cohort / File(s) Summary
Async Polling Implementation
src/index.ts
Replaced direct call with poll loop: call generateSupermodelGraph, poll while status is pending/processing using retryAfter (default 5s), enforce max poll time (~5 minutes), throw on failed or unexpected status, extract graph from response.result.graph or fallback to response.graph.
Integration Tests
src/__tests__/integration.test.ts
Updated tests to call generateSupermodelGraph and loop until status === 'completed', use retryAfter delays, update assertions to validate graph.nodes and graph.relationships, increase timeouts from 60s to 120s, change idempotency key prefix to dead-code-hunter:supermodel:<hash>, and add polling/logging diagnostics.

Sequence Diagram

sequenceDiagram
    participant Client as Client/Test
    participant API as Supermodel API
    participant Poller as Poller

    Client->>API: POST /generateSupermodelGraph (job request)
    API-->>Client: {status: "pending", retryAfter: 5, id: jobId}
    Poller->>Poller: wait retryAfter seconds
    Poller->>API: GET /jobs/{jobId} (status)
    API-->>Poller: {status: "processing", retryAfter: 5}
    Poller->>Poller: wait retryAfter seconds
    Poller->>API: GET /jobs/{jobId} (status)
    API-->>Poller: {status: "completed", result: {graph: {...}}}
    Poller-->>Client: return resolved graph (nodes + relationships)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🔍 Async whispers, polling lights the way,
Jobs hum softly, retryAfter holds sway,
Nodes assemble, relationships bloom,
Timeouts stretch to give the job room,
A patient loop brings the graph to day. 🪄

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add async polling for graph generation API' accurately summarizes the main change—converting from synchronous to asynchronous polling for the graph generation API.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #6: implements async job polling, handles the new response envelope shape, provides backward compatibility via fallback to response.graph, and updates tests to validate the polling behavior.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #6 requirements: polling implementation, response shape handling, backward compatibility, and test updates. No unrelated or out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Dead Code Hunter

No dead code found! Your codebase is clean.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/__tests__/integration.test.ts`:
- Around line 39-42: Tests are calling the wrong SDK method: replace all uses of
api.generateCallGraph with api.generateSupermodelGraph in the integration tests
(both the initial request and the polling/retry calls) so the tests exercise the
same production code path; also add the same 5-minute polling timeout/safety
check used in production to the test polling loops to prevent infinite waits
during retries.

In `@src/index.ts`:
- Around line 166-179: The polling loop exits on any non-pending/processing
status but doesn't verify success; update the code after the loop (the block
using response from api.generateSupermodelGraph with idempotencyKey and zipBlob)
to explicitly check that response.status === 'completed' and treat any other
status (e.g., 'failed', 'cancelled', or unknown values) as an error: log/throw a
clear error including response.status and response.jobId (and any
response.message) so failures/unknown statuses don't get treated as successful;
keep existing handling for 'failed' but ensure all non-'completed' cases are
handled consistently.
🧹 Nitpick comments (3)
src/index.ts (1)

156-160: The as any type cast bypasses TypeScript's safety.

Hey! So this as any cast works, but it's basically telling TypeScript "trust me, I know what I'm doing" - which means you lose all the nice autocomplete and type checking for the response.

This is probably happening because the SDK types (v0.4.1) don't match the new async job envelope from the API (v0.6.0). That's totally understandable, but worth noting for later cleanup once the SDK types are updated.

Consider defining an interface for the expected response shape. Something like:

interface AsyncJobResponse {
  status: 'pending' | 'processing' | 'completed' | 'failed';
  jobId?: string;
  retryAfter?: number;
  error?: string;
  result?: { graph?: GraphData };
  graph?: GraphData;
}

This way you still get some type safety even if the SDK doesn't provide it yet.

src/__tests__/integration.test.ts (2)

45-53: Test polling loops have no timeout protection - could hang forever.

So in the main code (src/index.ts), you have this nice 5-minute timeout:

const maxPollTime = 5 * 60 * 1000;
if (Date.now() - startTime > maxPollTime) {
  throw new Error('Graph generation timed out...');
}

But in the tests... nothing stops the while loop if the API never returns completed or failed. The test would just hang until Vitest's 120-second timeout kills it - which isn't as clean and doesn't give you a helpful error message.

♻️ Add timeout protection to test polling
+ const maxPollTime = 90 * 1000; // 90 seconds for tests
+ const startTime = Date.now();
+
  while (response.status === 'pending' || response.status === 'processing') {
+   if (Date.now() - startTime > maxPollTime) {
+     throw new Error(`Test timed out waiting for job ${response.jobId}`);
+   }
    const waitSeconds = response.retryAfter || 5;
    console.log(`Job ${response.jobId} is ${response.status}, waiting ${waitSeconds}s...`);
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
    response = await api.generateCallGraph({
      idempotencyKey,
      file: zipBlob,
    }) as any;
  }

Also applies to: 75-82


44-53: Duplicate polling logic in both tests - consider extracting a helper.

You've got basically the same polling loop copy-pasted in both tests. This is fine for now (2 occurrences), but if you add more tests or the polling logic changes, you'll have to update multiple places.

♻️ Extract a reusable polling helper
async function pollUntilComplete(
  api: DefaultApi, 
  idempotencyKey: string, 
  file: Blob,
  timeoutMs = 90000
): Promise<any> {
  const startTime = Date.now();
  let response = await api.generateSupermodelGraph({ idempotencyKey, file }) as any;
  
  while (response.status === 'pending' || response.status === 'processing') {
    if (Date.now() - startTime > timeoutMs) {
      throw new Error(`Polling timed out after ${timeoutMs}ms`);
    }
    const waitSeconds = response.retryAfter || 5;
    console.log(`Job ${response.jobId} is ${response.status}, waiting ${waitSeconds}s...`);
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
    response = await api.generateSupermodelGraph({ idempotencyKey, file }) as any;
  }
  
  return response;
}

Then your tests become much cleaner:

const response = await pollUntilComplete(api, idempotencyKey, zipBlob);
expect(response.status).toBe('completed');

Also applies to: 74-82

Comment on lines +166 to +179
while (response.status === 'pending' || response.status === 'processing') {
if (Date.now() - startTime > maxPollTime) {
throw new Error('Graph generation timed out after 5 minutes');
}

const waitSeconds = response.retryAfter || 5;
core.info(`Job ${response.jobId} is ${response.status}, retrying in ${waitSeconds}s...`);
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));

response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
}) as any;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing explicit check for 'completed' status after the polling loop exits.

So here's the thing - your while loop exits when status is NOT pending or processing. That means it could exit with:

  • completed ✅ (what you want)
  • failed ✅ (you handle this on line 181)
  • cancelled ❓ (not handled)
  • unknown_future_status ❓ (not handled)

The code assumes "if not pending/processing/failed, we're good" - but that's a bit risky. If the API adds a new status like cancelled or expired, your code would silently treat it as success.

🔧 Suggested fix: Add explicit completed check
  if (response.status === 'failed') {
    throw new Error(`Graph generation failed: ${response.error || 'Unknown error'}`);
  }

+ if (response.status !== 'completed') {
+   throw new Error(`Unexpected job status: ${response.status}`);
+ }
🤖 Prompt for AI Agents
In `@src/index.ts` around lines 166 - 179, The polling loop exits on any
non-pending/processing status but doesn't verify success; update the code after
the loop (the block using response from api.generateSupermodelGraph with
idempotencyKey and zipBlob) to explicitly check that response.status ===
'completed' and treat any other status (e.g., 'failed', 'cancelled', or unknown
values) as an error: log/throw a clear error including response.status and
response.jobId (and any response.message) so failures/unknown statuses don't get
treated as successful; keep existing handling for 'failed' but ensure all
non-'completed' cases are handled consistently.

- Add explicit 'completed' status check after polling loop to guard
  against unexpected statuses (e.g. 'cancelled', 'expired')
- Change integration tests to use generateSupermodelGraph (matches
  production code path) instead of generateCallGraph
- Add polling timeout protection to test loops
- Update idempotency key prefix from 'call' to 'supermodel'
@jonathanpopham jonathanpopham marked this pull request as ready for review January 23, 2026 23:08
@jonathanpopham jonathanpopham marked this pull request as draft January 23, 2026 23:35
@jonathanpopham
Copy link
Collaborator Author

Closing - SDK 0.6.0 will handle async polling internally, so no changes needed in dead-code-hunter beyond a version bump once the SDK is published.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: Breaking change to Supermodel api/sdk coming in v0.6.0

2 participants