Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/dead-code.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 26 additions & 3 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32607,13 +32607,36 @@ async function run() {
const api = new sdk_1.DefaultApi(config);
const zipBuffer = await fs.readFile(zipPath);
const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });
const response = await api.generateSupermodelGraph({
// Submit graph generation job (async API returns an envelope with status)
let response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
});
// Poll until the job completes or fails
const maxPollTime = 5 * 60 * 1000; // 5 minutes max
const startTime = Date.now();
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,
});
}
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}`);
}
// Step 4: Analyze for dead code
const nodes = response.graph?.nodes || [];
const relationships = response.graph?.relationships || [];
const graphData = response.result?.graph || response.graph;
const nodes = graphData?.nodes || [];
const relationships = graphData?.relationships || [];
const deadCode = (0, dead_code_1.findDeadCode)(nodes, relationships, ignorePatterns);
core.info(`Found ${deadCode.length} potentially unused functions`);
// Step 5: Set outputs
Expand Down
68 changes: 50 additions & 18 deletions src/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,72 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => {
const commitHash = execSync('git rev-parse --short HEAD', { cwd: repoRoot })
.toString()
.trim();
idempotencyKey = `dead-code-hunter:call:${commitHash}`;
idempotencyKey = `dead-code-hunter:supermodel:${commitHash}`;
});

it('should call the Supermodel API and get a call graph', async () => {
it('should call the Supermodel API and get a supermodel graph', async () => {
const zipBuffer = await fs.readFile(zipPath);
const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });

const response = await api.generateCallGraph({
let response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
});
}) as any;

// Poll until job completes (with timeout)
const startTime = Date.now();
const maxPollTime = 100_000; // 100s (within the vitest timeout)
while (response.status === 'pending' || response.status === 'processing') {
if (Date.now() - startTime > maxPollTime) {
throw new Error('Polling timed out in test');
}
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: zipBlob,
}) as any;
}

expect(response).toBeDefined();
expect(response.graph).toBeDefined();
expect(response.graph?.nodes).toBeDefined();
expect(response.graph?.relationships).toBeDefined();
expect(response.stats).toBeDefined();
expect(response.status).toBe('completed');
const graph = response.result?.graph || response.graph;
expect(graph).toBeDefined();
expect(graph?.nodes).toBeDefined();
expect(graph?.relationships).toBeDefined();

console.log('API Stats:', response.stats);
console.log('Nodes:', response.graph?.nodes?.length);
console.log('Relationships:', response.graph?.relationships?.length);
}, 60000); // 60 second timeout for API call
console.log('Nodes:', graph?.nodes?.length);
console.log('Relationships:', graph?.relationships?.length);
}, 120000); // 120 second timeout for async API

it('should find dead code in the dead-code-hunter repo itself', async () => {
const zipBuffer = await fs.readFile(zipPath);
const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });

const response = await api.generateCallGraph({
let response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
});
}) as any;

// Poll until job completes (with timeout)
const startTime = Date.now();
const maxPollTime = 100_000;
while (response.status === 'pending' || response.status === 'processing') {
if (Date.now() - startTime > maxPollTime) {
throw new Error('Polling timed out in test');
}
const waitSeconds = response.retryAfter || 5;
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
}) as any;
}

const nodes = response.graph?.nodes || [];
const relationships = response.graph?.relationships || [];
expect(response.status).toBe('completed');
const graph = response.result?.graph || response.graph;
const nodes = graph?.nodes || [];
const relationships = graph?.relationships || [];

const deadCode = findDeadCode(nodes, relationships);

Expand All @@ -80,7 +112,7 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => {

// The test passes regardless of dead code count - we just want to verify the flow works
expect(Array.isArray(deadCode)).toBe(true);
}, 60000);
}, 120000);
});

describe('Integration Test Prerequisites', () => {
Expand Down
37 changes: 33 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,43 @@ async function run(): Promise<void> {
const zipBuffer = await fs.readFile(zipPath);
const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });

const response = await api.generateSupermodelGraph({
// Submit graph generation job (async API returns an envelope with status)
let response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
});
}) as any;

// Poll until the job completes or fails
const maxPollTime = 5 * 60 * 1000; // 5 minutes max
const startTime = Date.now();

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;
}
Comment on lines +166 to +179
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.


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}`);
}

// Step 4: Analyze for dead code
const nodes = response.graph?.nodes || [];
const relationships = response.graph?.relationships || [];
const graphData = response.result?.graph || response.graph;
const nodes = graphData?.nodes || [];
const relationships = graphData?.relationships || [];

const deadCode = findDeadCode(nodes, relationships, ignorePatterns);

Expand Down
Loading