Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Prove a Convex feature works — seed, drive as multiple mocked users via convex-test, assert behavior including the negative authz cases (wrong user refused, data-scope enforced).
.claude/skills/get-convex-convex-verify/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-09 | ✗→✓ | ▲ Improved | -28% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 65% | 0% |
| case-06 | ✓→✗ | ▼ Worse | -42% | 0% |
| case-22 | ✓→✗ | ▼ Worse | 249% | 0% |
<!-- GENERATED from convex-agents content/capabilities/convex-verify.json — do not edit by hand. -->
A green typecheck proves the code parses; it does not prove a non-owner is actually denied, that a query returns the right rows, or that a mutation has the effect it claims. This capability closes that gap with the loop the whole field is missing: seed → drive → assert, run in-process with convex-test so it needs no deployment. Its highest-value assertions are the NEGATIVE ones — the caller who should be refused — because those are exactly the authz defects the 30-app corpus shows are the #1 real bug and the ones a happy-path demo never catches.
convex-test: ensure convex-test + vitest are dev deps AND a vitest.config.ts sets test.environment: "edge-runtime" with server.deps.inline: ["convex-test"] — WITHOUT that config, convexTest(schema) fails at runtime with import.meta.glob is not a function (verified). Also install @edge-runtime/vm. Then convexTest(schema) gives a t handle. Reuse the project's existing test setup if present (compose with the test capability, don't fork it).t.run(async (ctx) => ctx.db.insert(...)) for fixtures the public API can't create. Seed at least: the caller's own rows AND a second user's rows, so cross-user access is testable.t.withIdentity({ subject, tokenIdentifier, ... }): call the function as (a) the legitimate owner, (b) a different authenticated user, and (c) unauthenticated (t with no identity). Use the real identity shape the app's auth uses (subject/tokenIdentifier), matching how ownership is resolved.expect(await t.withIdentity(owner).query(api.x.y, args)).toEqual(...)).await expect(t.withIdentity(other).mutation(api.x.cancel, {id})).rejects.toThrow(/forbidden|not authorized|403/) — and an unauthenticated caller is refused where auth is required. A feature is not proven until the wrong caller is shown to be blocked.npx vitest run) and report: what was proven (each positive + negative assertion that passed), and — critically — any assertion that FAILED, because a failed negative assertion is a real authz hole found before ship. Emit findings on the bus (specs/finding.schema.json, class authz/correctness, evidence kind probe-result with the exact failing call) for anything that didn't behave.test capability's setup rather than forking it.test capability.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-20 | pass→fail | 11,561 | 12,774 | +10% | 1 | 1 | 0% | 2,216 | 3,659 | +65% | 0 | 0 | — |
case-01 | fail→fail | 4,411 | 8,349 | +89% | 1 | 1 | 0% | 263 | 1,597 | +507% | 0 | 0 | — |
case-02 | fail→fail | 5,237 | 8,177 | +56% | 1 | 1 | 0% | 325 | 1,740 | +435% | 0 | 0 | — |
case-03 | fail→fail | 6,319 | 10,245 | +62% | 1 | 1 | 0% | 345 | 1,553 | +350% | 0 | 0 | — |
case-04 | pass→pass | 11,471 | 7,462 | -35% | 1 | 1 | 0% | 1,943 | 2,280 | +17% | 0 | 0 | — |
case-05 | pass→pass | 29,911 | 5,625 | -81% | 1 | 1 | 0% | 4,310 | 1,917 | -56% | 0 | 0 | — |
case-06 | pass→fail | 14,309 | 8,241 | -42% | 1 | 1 | 0% | 2,684 | 1,561 | -42% | 0 | 0 | — |
case-07 | pass→pass | 10,624 | 11,367 | +7% | 1 | 1 | 0% | 2,097 | 3,459 | +65% | 0 | 0 | — |
case-08 | pass→pass | 12,992 | 9,664 | -26% | 1 | 1 | 0% | 1,896 | 2,504 | +32% | 0 | 0 | — |
case-09 | fail→pass | 14,006 | 3,545 | -75% | 1 | 1 | 0% | 2,371 | 1,700 | -28% | 0 | 0 | — |
case-10 | fail→fail | 5,577 | 4,751 | -15% | 1 | 1 | 0% | 894 | 1,299 | +45% | 0 | 0 | — |
case-11 | fail→pass | 11,529 | 10,167 | -12% | 1 | 1 | 0% | 2,216 | 2,631 | +19% | 0 | 0 | — |
case-12 | pass→pass | 10,884 | 12,613 | +16% | 1 | 1 | 0% | 1,914 | 2,745 | +43% | 0 | 0 | — |
case-13 | pass→pass | 9,635 | 11,500 | +19% | 1 | 1 | 0% | 1,825 | 2,895 | +59% | 0 | 0 | — |
case-14 | pass→pass | 13,169 | 12,281 | -7% | 1 | 1 | 0% | 2,343 | 3,031 | +29% | 0 | 0 | — |
case-15 | pass→pass | 8,874 | 7,975 | -10% | 1 | 1 | 0% | 1,511 | 2,239 | +48% | 0 | 0 | — |
case-16 | pass→pass | 11,148 | 4,814 | -57% | 1 | 1 | 0% | 1,751 | 1,872 | +7% | 0 | 0 | — |
case-17 | pass→pass | 10,700 | 9,490 | -11% | 1 | 1 | 0% | 1,487 | 2,477 | +67% | 0 | 0 | — |
case-18 | pass→pass | 13,850 | 6,301 | -55% | 1 | 1 | 0% | 2,134 | 2,200 | +3% | 0 | 0 | — |
case-19 | pass→pass | 12,705 | 10,763 | -15% | 1 | 1 | 0% | 2,197 | 2,993 | +36% | 0 | 0 | — |
case-21 | pass→pass | 31,683 | 26,127 | -18% | 1 | 1 | 0% | 6,388 | 5,785 | -9% | 0 | 0 | — |
case-22 | pass→fail | 7,722 | 19,521 | +153% | 1 | 1 | 0% | 1,381 | 4,818 | +249% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 17 counted toward the lift figure. The other 5 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of -5 percentage points is the difference between those two pass rates over the 17 comparable cases. 3 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.