Skip to content

fix(api): restore v1 skills list compatibility#961

Closed
Alix-007 wants to merge 4 commits intoopenclaw:mainfrom
Alix-007:fix-958-skills-v1-list-compat
Closed

fix(api): restore v1 skills list compatibility#961
Alix-007 wants to merge 4 commits intoopenclaw:mainfrom
Alix-007:fix-958-skills-v1-list-compat

Conversation

@Alix-007
Copy link
Copy Markdown

Summary

  • restore skills.listPublicPage as a compatibility shim for HTTP API v1 callers
  • preserve legacy updated, stat-sorted, and trending behaviors instead of returning an empty page
  • replace the deprecated-stub test with compatibility coverage for filtering and cached trending fallbacks

Testing

  • git diff --check
  • could not run vitest in this environment because bun/repo dependencies are not installed locally

Closes #958

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 17, 2026

@Alix-007 is attempting to deploy a commit to the Amantus Machina Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR restores listPublicPage from a no-op stub back to a fully functional query, providing a backward-compatibility shim for HTTP API v1 and older CLI consumers. The handler now correctly handles updated, stat-sorted (downloads, stars, installsCurrent, installsAllTime), and trending sorts, with soft-delete and nonSuspiciousOnly filtering applied consistently across all paths.

Key observations:

  • The updated and stat-sorted code paths are correct and consistent with how the rest of the codebase paginates and filters skills.
  • The trending path has a cold-start gap: when nonSuspiciousOnly: true, getTrendingEntries queries only the trending_non_suspicious leaderboard and returns [] if it doesn't exist yet — no fallback to the general trending leaderboard with runtime filtering. Clients requesting safe trending results will receive an empty list during any window where only the trending leaderboard has been populated.
  • The double isSkillSuspicious check in the trending loop (once via the leaderboard kind, once per-skill) is intentional defense-in-depth against stale leaderboard data — not a bug.
  • The new test suite covers updated and trending paths well, but the four stat-sorted paths through sortToIndex have no coverage.

Confidence Score: 3/5

  • Safe to merge with low risk, but the trending cold-start gap should be addressed before the leaderboard jobs are confirmed to populate both leaderboard kinds.
  • The core updated and stat-sorted restore is correct and well-tested. The trending path has a real behavioral gap (empty results when trending_non_suspicious leaderboard is absent and nonSuspiciousOnly: true) that could affect API consumers. The previous behavior was always empty, so this is strictly better, but the gap is worth fixing before considering the compatibility fully restored.
  • convex/skills.ts — specifically the getTrendingEntries function and its lack of a fallback to the general trending leaderboard.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: convex/skills.ts
Line: 2955-2968

Comment:
**No fallback when `trending_non_suspicious` leaderboard doesn't exist yet**

When `nonSuspiciousOnly: true`, `getTrendingEntries` queries the `trending_non_suspicious` leaderboard exclusively. If that leaderboard hasn't been populated yet (e.g., before the first scheduled run of the leaderboard job, or if the job has only ever populated `trending`), it returns `[]`, so the handler sends back an empty items list.

This is a cold-start regression: callers asking for safe trending results get nothing even though a `trending` leaderboard exists that could be filtered at runtime. The PR description says it's "restoring" the trending behavior, but this edge case regresses to empty-page behavior.

A safer approach would fall back to the `trending` leaderboard and apply runtime filtering when the specialized leaderboard is absent:

```typescript
async function getTrendingEntries(
  ctx: QueryCtx,
  limit: number,
  args?: { nonSuspiciousOnly?: boolean },
) {
  if (args?.nonSuspiciousOnly) {
    const nonSuspicious = await ctx.db
      .query('skillLeaderboards')
      .withIndex('by_kind', (q) => q.eq('kind', TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND))
      .order('desc')
      .take(1)
    if (nonSuspicious[0]) return nonSuspicious[0].items.slice(0, limit)
    // Fall back to the general leaderboard; the caller already filters per-skill.
  }

  const latest = await ctx.db
    .query('skillLeaderboards')
    .withIndex('by_kind', (q) => q.eq('kind', TRENDING_LEADERBOARD_KIND))
    .order('desc')
    .take(1)

  return latest[0] ? latest[0].items.slice(0, limit) : []
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: convex/skills.listPublicPage.test.ts
Line: 155-188

Comment:
**No test coverage for stat-sorted paths**

`makeCtx` only wires up `by_updated` — calling the handler with `sort: 'downloads'`, `'stars'`, `'installsCurrent'`, or `'installsAllTime'` would throw `unexpected index` from the mock. The four `sortToIndex` branches and their pagination / filtering logic have zero test coverage in this suite.

Consider adding at least one parameterised case for a stat-sorted path (e.g. `sort: 'downloads'`) to guard against a wrong index name or a logic divergence from the `updated` path.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 30a4e76

convex/skills.ts Outdated
Comment on lines +2955 to +2968
const kind = args?.nonSuspiciousOnly
? TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND
: TRENDING_LEADERBOARD_KIND
const latest = await ctx.db
.query('skillLeaderboards')
.withIndex('by_kind', (q) => q.eq('kind', kind))
.order('desc')
.take(1)

if (latest[0]) {
return latest[0].items.slice(0, limit)
}

return []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 No fallback when trending_non_suspicious leaderboard doesn't exist yet

When nonSuspiciousOnly: true, getTrendingEntries queries the trending_non_suspicious leaderboard exclusively. If that leaderboard hasn't been populated yet (e.g., before the first scheduled run of the leaderboard job, or if the job has only ever populated trending), it returns [], so the handler sends back an empty items list.

This is a cold-start regression: callers asking for safe trending results get nothing even though a trending leaderboard exists that could be filtered at runtime. The PR description says it's "restoring" the trending behavior, but this edge case regresses to empty-page behavior.

A safer approach would fall back to the trending leaderboard and apply runtime filtering when the specialized leaderboard is absent:

async function getTrendingEntries(
  ctx: QueryCtx,
  limit: number,
  args?: { nonSuspiciousOnly?: boolean },
) {
  if (args?.nonSuspiciousOnly) {
    const nonSuspicious = await ctx.db
      .query('skillLeaderboards')
      .withIndex('by_kind', (q) => q.eq('kind', TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND))
      .order('desc')
      .take(1)
    if (nonSuspicious[0]) return nonSuspicious[0].items.slice(0, limit)
    // Fall back to the general leaderboard; the caller already filters per-skill.
  }

  const latest = await ctx.db
    .query('skillLeaderboards')
    .withIndex('by_kind', (q) => q.eq('kind', TRENDING_LEADERBOARD_KIND))
    .order('desc')
    .take(1)

  return latest[0] ? latest[0].items.slice(0, limit) : []
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: convex/skills.ts
Line: 2955-2968

Comment:
**No fallback when `trending_non_suspicious` leaderboard doesn't exist yet**

When `nonSuspiciousOnly: true`, `getTrendingEntries` queries the `trending_non_suspicious` leaderboard exclusively. If that leaderboard hasn't been populated yet (e.g., before the first scheduled run of the leaderboard job, or if the job has only ever populated `trending`), it returns `[]`, so the handler sends back an empty items list.

This is a cold-start regression: callers asking for safe trending results get nothing even though a `trending` leaderboard exists that could be filtered at runtime. The PR description says it's "restoring" the trending behavior, but this edge case regresses to empty-page behavior.

A safer approach would fall back to the `trending` leaderboard and apply runtime filtering when the specialized leaderboard is absent:

```typescript
async function getTrendingEntries(
  ctx: QueryCtx,
  limit: number,
  args?: { nonSuspiciousOnly?: boolean },
) {
  if (args?.nonSuspiciousOnly) {
    const nonSuspicious = await ctx.db
      .query('skillLeaderboards')
      .withIndex('by_kind', (q) => q.eq('kind', TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND))
      .order('desc')
      .take(1)
    if (nonSuspicious[0]) return nonSuspicious[0].items.slice(0, limit)
    // Fall back to the general leaderboard; the caller already filters per-skill.
  }

  const latest = await ctx.db
    .query('skillLeaderboards')
    .withIndex('by_kind', (q) => q.eq('kind', TRENDING_LEADERBOARD_KIND))
    .order('desc')
    .take(1)

  return latest[0] ? latest[0].items.slice(0, limit) : []
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +155 to +188
function makeCtx({
by_updated,
users,
versions,
}: {
by_updated: ReturnType<typeof vi.fn>
users: Array<ReturnType<typeof makeUser>>
versions: Array<ReturnType<typeof makeVersion>>
}) {
const userMap = new Map(users.map((user) => [user._id, user]))
const versionMap = new Map(versions.map((version) => [version._id, version]))
return {
db: {
query: vi.fn((table: string) => {
if (table !== 'skills') throw new Error(`unexpected table ${table}`)
return {
withIndex: vi.fn((index: string, _builder: unknown) => {
if (index !== 'by_updated') throw new Error(`unexpected index ${index}`)
return {
order: vi.fn((dir: string) => {
if (dir !== 'desc') throw new Error(`unexpected order ${dir}`)
return { paginate: by_updated }
}),
}
}),
}
}),
get: vi.fn(async (id: string) => {
if (id.startsWith('users:')) return userMap.get(id) ?? null
if (id.startsWith('skillVersions:')) return versionMap.get(id) ?? null
return null
}),
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 No test coverage for stat-sorted paths

makeCtx only wires up by_updated — calling the handler with sort: 'downloads', 'stars', 'installsCurrent', or 'installsAllTime' would throw unexpected index from the mock. The four sortToIndex branches and their pagination / filtering logic have zero test coverage in this suite.

Consider adding at least one parameterised case for a stat-sorted path (e.g. sort: 'downloads') to guard against a wrong index name or a logic divergence from the updated path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: convex/skills.listPublicPage.test.ts
Line: 155-188

Comment:
**No test coverage for stat-sorted paths**

`makeCtx` only wires up `by_updated` — calling the handler with `sort: 'downloads'`, `'stars'`, `'installsCurrent'`, or `'installsAllTime'` would throw `unexpected index` from the mock. The four `sortToIndex` branches and their pagination / filtering logic have zero test coverage in this suite.

Consider adding at least one parameterised case for a stat-sorted path (e.g. `sort: 'downloads'`) to guard against a wrong index name or a logic divergence from the `updated` path.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30a4e76801

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

convex/skills.ts Outdated
.take(1)

if (latest[0]) {
return latest[0].items.slice(0, limit)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Defer trending limit until after filtering invalid skills

The trending path slices cached entries to limit inside getTrendingEntries, but listPublicPage then drops entries that are soft-deleted, suspicious, or otherwise non-public. That means valid lower-ranked entries are never considered, so callers can get underfilled (or empty) pages even when enough eligible trending skills exist; for example, limit=1 returns nothing if rank #1 was deleted after cache generation while rank #2 is still valid.

Useful? React with 👍 / 👎.

convex/skills.ts Outdated
return latest[0].items.slice(0, limit)
}

return []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fallback to main trending cache when clean cache is missing

When nonSuspiciousOnly is enabled, getTrendingEntries queries only trending_non_suspicious and returns an empty list if that cache row is absent, even if the regular trending cache exists. Since leaderboard rebuild writes trending and trending_non_suspicious in separate mutations (convex/leaderboards.ts), a partial failure can leave non-suspicious API requests empty until the next successful rebuild instead of falling back to filtering the main trending entries.

Useful? React with 👍 / 👎.

@Alix-007
Copy link
Copy Markdown
Author

Addressing the review notes in the latest head:

  • added a cold-start fallback from trending_non_suspicious to the general trending leaderboard, with per-skill filtering still applied by the caller
  • added compatibility coverage for the four stat-sorted v1 paths plus the missing trending fallback case

Static check in this environment: git diff --check.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d71ca17b0a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

.order('desc')
.take(1)

return latest[0] ? latest[0].items.slice(0, limit) : []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter trending entries before applying requested limit

getTrendingEntries slices the cached leaderboard to limit before listPublicPage filters out non-public/suspicious skills, so lower-ranked valid skills are never considered. In the fallback path (nonSuspiciousOnly=true with only trending cache present), a leaderboard like [suspicious, clean] and limit=1 returns an empty page because only the first entry is fetched and then discarded; this underfills v1 API responses even when enough eligible skills exist.

Useful? React with 👍 / 👎.

@Alix-007
Copy link
Copy Markdown
Author

Closing this to keep the active queue on items I can actually unblock. The code changes are in place, but this PR is currently gated by Vercel team authorization on the repository side, so I am redirecting effort to unblocked issues instead.

@Alix-007 Alix-007 closed this Mar 17, 2026
@xiaoshitou1
Copy link
Copy Markdown

Is there any plan to merge the fix from PR #961?

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.

HTTP API /api/v1/skills returns empty after V4 migration

2 participants