Skip to content
Open
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
216 changes: 216 additions & 0 deletions docs/superpowers/plans/2026-07-30-qoder-organization-quota.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Qoder Organization Quota Visibility Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Show meaningful Qoder organization quota rows and finite allocations even when Qoder reports a zero total.

**Architecture:** Keep the server response and generic quota-table rendering unchanged. In the Qoder-specific normalization path, suppress only all-zero organization placeholders and infer a missing total from absolute used plus remaining credits.

**Tech Stack:** JavaScript ESM, Vitest, existing Next.js dashboard utilities

## Global Constraints

- Must use the existing Qoder quota normalization path.
- Must preserve the existing JavaScript and Vitest toolchain.
- Must not add third-party dependencies.
- Test coverage for the changed behavior must be complete.

---

### Task 1: Preserve Meaningful Organization Quotas

**Files:**
- Create: `tests/unit/qoder-quota.test.js`
- Modify: `src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js:406`

**Interfaces:**
- Consumes: `parseQuotaData(provider, data)` from the existing quota normalization utility.
- Produces: Qoder normalized rows where a non-zero `total`, `used`, or `remaining` value makes the organization row visible.

- [ ] **Step 1: Write the failing regression test**

```js
import { describe, expect, it } from "vitest";
import { parseQuotaData } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";

describe("Qoder organization quota visibility", () => {
const resetAt = "2026-07-31T16:00:00.000Z";

it.each([
["total", { total: 1, used: 0, remaining: 0 }],
["used", { total: 0, used: 20000, remaining: 0 }],
["remaining", { total: 0, used: 0, remaining: 1 }],
])("keeps organization quota when %s is non-zero", (_field, organization) => {
const data = {
quotas: {
user: {
total: 3000,
used: 3000,
remaining: 0,
unit: "credits",
resetAt,
},
organization: {
...organization,
unit: "credits",
resetAt,
},
},
};

expect(parseQuotaData("qoder", data)).toContainEqual({
name: "Organization",
used: organization.used,
total: organization.total,
unit: "credits",
resetAt,
});
});

it("still hides an all-zero organization placeholder", () => {
const data = {
quotas: {
user: { total: 3000, used: 0, remaining: 3000, unit: "credits" },
organization: { total: 0, used: 0, remaining: 0, unit: "credits" },
},
};

expect(parseQuotaData("qoder", data).map((quota) => quota.name)).toEqual([
"Personal",
]);
});

it("keeps personal quota normalization unchanged", () => {
const data = {
quotas: {
user: { total: 3000, used: 1200, remaining: 1800, unit: "credits", resetAt },
},
};

expect(parseQuotaData("qoder", data)).toEqual([{
name: "Personal",
used: 1200,
total: 3000,
unit: "credits",
resetAt,
}]);
});
});
```

- [ ] **Step 2: Run the focused test and verify the regression is red**

Run: `cd tests && npx vitest run unit/qoder-quota.test.js`

Expected: the used-only and remaining-only cases fail because the output lacks the `Organization` row; the total-only, all-zero placeholder, and personal cases pass.

- [ ] **Step 3: Narrow the organization filter**

```js
// Skip only the empty organization placeholder synthesized for personal accounts.
if (
quotaType === "organization"
&& (!quota || [quota.total, quota.used, quota.remaining]
.every((value) => (Number(value) || 0) === 0))
) {
return;
}
```

- [ ] **Step 4: Run focused and adjacent tests**

Run: `cd tests && npx vitest run unit/qoder-quota.test.js unit/provider-quota-visibility.test.js unit/usage-dispatch.test.js`

Expected: all selected test files pass with zero failures.

- [ ] **Step 5: Run repository quality gates**

Run: `npx eslint 'src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js' tests/unit/qoder-quota.test.js`

Expected: ESLint exits successfully with zero errors.

Run: `node --check 'src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js'`

Expected: Node syntax checking exits successfully.

Run from `tests/`: `npx vitest run --reporter=json --outputFile=/tmp/9router-results.json`

Expected: Vitest writes complete JSON results; the command may exit non-zero only for failures already catalogued by the repository.

Run from the repository root to normalize the checkout prefix expected by the baseline verifier:

```bash
node --input-type=module -e 'import fs from "node:fs"; import path from "node:path"; const file="/tmp/9router-results.json"; const data=JSON.parse(fs.readFileSync(file,"utf8")); for (const result of data.testResults) result.name=`/app/${path.relative(process.cwd(),result.name).split(path.sep).join("/")}`; fs.writeFileSync(file,JSON.stringify(data));'
```

Run: `node tests/__baseline__/verify-no-regression.mjs /tmp/9router-results.json`

Expected: the baseline verifier reports `No regression` and exits successfully.

Coverage gate: the repository has no Vitest coverage provider or configured threshold. The focused parameterized test mechanically exercises all four visibility decisions (non-zero total, used, or remaining; and all-zero), providing 100% behavioral coverage of the changed predicate without adding an unaudited dependency.

- [ ] **Step 6: Obtain independent code and test review**

Compare the implementation with the Spec and this plan. Reject Critical or Important findings; stop and escalate if more than two review rounds are required.

- [ ] **Step 7: Commit the verified change**

```bash
git add -f docs/superpowers/specs/2026-07-30-qoder-organization-quota-design.md \
docs/superpowers/plans/2026-07-30-qoder-organization-quota.md \
tests/unit/qoder-quota.test.js \
'src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js'
git commit -m "fix(qoder): show organization quota usage"
```

- [ ] **Step 8: Verify and deliver the branch**

```bash
git ls-files --error-unmatch \
docs/superpowers/specs/2026-07-30-qoder-organization-quota-design.md \
docs/superpowers/plans/2026-07-30-qoder-organization-quota.md
git push -u origin fix/qoder-organization-quota
gh pr create --repo decolua/9router --base master --head Beants:fix/qoder-organization-quota
```

Expected: both governance documents are tracked, the branch is present on the fork, and the upstream pull request URL is returned.

---

### Task 2: Render Zero-Total Qoder Allocations as Finite

**Files:**
- Modify: `tests/unit/qoder-quota.test.js`
- Modify: `src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js`

**Interfaces:**
- Consumes: Qoder quota rows shaped as `{ total, used, remaining, unit, resetAt }`.
- Produces: normalized quota rows whose total is the positive reported total, or `used + remaining` when the reported total is zero.

- [x] **Step 1: Extend the regression tests**

Add coverage for `total: 0`, `used: 3804`, and `remaining: 6196`. Assert a normalized total of `10000` and a calculated remaining percentage of `62`. Update the existing zero-total cases to expect the inferred total.

- [x] **Step 2: Verify the regression is red**

Run: `cd tests && npx vitest run unit/qoder-quota.test.js`

Expected: the inferred-total assertions fail because the current parser preserves `total: 0`.

- [x] **Step 3: Implement the Qoder-only total fallback**

Use the positive reported total when available. Otherwise, set the normalized total to the sum of numeric used and remaining credits. Continue omitting the absolute `remaining` field so generic percentage handling is unchanged.

- [x] **Step 4: Verify focused and adjacent behavior**

Run: `cd tests && npx vitest run unit/qoder-quota.test.js unit/provider-quota-visibility.test.js unit/usage-dispatch.test.js`

Expected: all selected tests pass with zero failures.

- [x] **Step 5: Run quality gates and independent review**

Run lint, syntax checking, the repository differential suite, and an independent review. Update the existing branch and pull request only after all gates pass.

- [x] **Step 6: Rebuild and reinstall locally**

Build the CLI tarball, install it globally, restart the launch agent, and verify the health endpoint plus the Qoder quota page behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Qoder Organization Quota Visibility Spec

## Problem Statement

The quota tracker omits Qoder organization quota records whenever their reported total is zero. Qoder can report meaningful organization usage and remaining credits in that state, so users either cannot see the data or see a finite allocation rendered as unlimited.

## Success Metrics

- 100% of Qoder organization quota records with a non-zero `total`, `used`, or `remaining` value appear as quota tracker rows.
- 100% of Qoder quota records with a zero reported total and meaningful used or remaining credits display a finite inferred total equal to `used + remaining`.
- 100% of all-zero Qoder organization placeholder records remain hidden from the quota tracker.
- Existing focused quota-parser tests pass with zero failures.

## User Stories

- As a Qoder user, I want organization usage returned by 9Router to appear in the quota tracker so that I can see usage beyond my personal allocation.
- As a Qoder user, I want a finite organization allocation to show its actual used, total, and remaining percentage instead of an infinity symbol.
- As a Qoder user without an organization allocation, I do not want an empty organization row cluttering the quota tracker.

## Acceptance Criteria

- [x] Given an organization quota with `total: 0`, `used: 20000`, and `remaining: 0`, the quota tracker includes an `Organization` row with an inferred total of `20000`.
- [x] Given an organization quota with `total: 0`, `used: 3804`, and `remaining: 6196`, the quota tracker displays `3804 / 10000` and `62%` remaining.
- [x] A positive reported total remains authoritative and is not replaced by the inferred total.
- [x] The included organization row preserves its reported usage, unit, and reset time.
- [x] Given an organization quota whose `total`, `used`, and `remaining` values are all zero, the normalized quota output omits the organization row.
- [x] Personal Qoder quota normalization remains unchanged.
- [x] Automated regression tests cover non-zero total, used, and remaining values plus the all-zero placeholder case.

## Non-Goals

- Changing the Qoder upstream request or server-side usage response.
- Changing generic quota percentage calculation or unlimited-quota display behavior.
- Changing normalization for providers other than Qoder.
- Adding dependencies or modifying provider authentication.

## Constraints

- Must use the existing Qoder quota normalization path.
- Must infer a missing Qoder total without forwarding absolute remaining credits through the generic percentage field.
- Must preserve the existing JavaScript and Vitest toolchain.
- Must not add third-party dependencies.
- Test coverage for the changed behavior must be complete.
Original file line number Diff line number Diff line change
Expand Up @@ -395,21 +395,26 @@ export function parseQuotaData(provider, data) {
case "qoder":
// Qoder ships a `user` quota and (optionally) an `organization`
// quota, both with same shape: {total, used, remaining, unit, resetAt}.
// Skip an organization bucket when its total is 0 — most personal
// Qoder accounts won't have one and rendering "0/0" is misleading.
// Skip only the empty organization placeholder synthesized for
// personal accounts; a zero total can still carry meaningful usage.
// Don't forward Qoder's `remaining` field: it's an absolute credit
// count, but getRemainingPercentage / QuotaTable interpret
// `remaining` as a 0-100 percentage and would render 348 credits
// as "348%". The percentage is computed from used/total instead.
if (data.quotas) {
Object.entries(data.quotas).forEach(([quotaType, quota]) => {
if (quotaType === "organization" && (!quota || (Number(quota.total) || 0) === 0)) {
if (
quotaType === "organization"
&& (!quota || [quota.total, quota.used, quota.remaining]
.every((value) => (Number(value) || 0) === 0))
) {
return;
}
normalizedQuotas.push({
name: quotaType === "user" ? "Personal" : quotaType === "organization" ? "Organization" : quotaType,
used: quota.used || 0,
total: quota.total || 0,
total: Math.max(0, Number(quota.total) || 0)
|| ((Number(quota.used) || 0) + (Number(quota.remaining) || 0)),
unit: quota.unit,
resetAt: quota.resetAt || null,
});
Expand Down
95 changes: 95 additions & 0 deletions tests/unit/qoder-quota.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import {
getRemainingPercentage,
parseQuotaData,
} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";

describe("Qoder organization quota visibility", () => {
const resetAt = "2026-07-31T16:00:00.000Z";

it.each([
["total", { total: 1, used: 0, remaining: 0 }, 1],
["used", { total: 0, used: 20000, remaining: 0 }, 20000],
["remaining", { total: 0, used: 0, remaining: 1 }, 1],
["invalid total", { total: -1, used: 3804, remaining: 6196 }, 10000],
])("keeps organization quota when %s is non-zero", (_field, organization, expectedTotal) => {
const data = {
quotas: {
user: {
total: 3000,
used: 3000,
remaining: 0,
unit: "credits",
resetAt,
},
organization: {
...organization,
unit: "credits",
resetAt,
},
},
};

expect(parseQuotaData("qoder", data)).toContainEqual({
name: "Organization",
used: organization.used,
total: expectedTotal,
unit: "credits",
resetAt,
});
});

it("infers a finite organization total from used and remaining credits", () => {
const data = {
quotas: {
organization: {
total: 0,
used: 3804,
remaining: 6196,
unit: "credits",
resetAt,
},
},
};

const [organization] = parseQuotaData("qoder", data);

expect(organization).toEqual({
name: "Organization",
used: 3804,
total: 10000,
unit: "credits",
resetAt,
});
expect(getRemainingPercentage(organization)).toBe(62);
});

it("still hides an all-zero organization placeholder", () => {
const data = {
quotas: {
user: { total: 3000, used: 0, remaining: 3000, unit: "credits" },
organization: { total: 0, used: 0, remaining: 0, unit: "credits" },
},
};

expect(parseQuotaData("qoder", data).map((quota) => quota.name)).toEqual([
"Personal",
]);
});

it("keeps personal quota normalization unchanged", () => {
const data = {
quotas: {
user: { total: 3000, used: 1200, remaining: 1800, unit: "credits", resetAt },
},
};

expect(parseQuotaData("qoder", data)).toEqual([{
name: "Personal",
used: 1200,
total: 3000,
unit: "credits",
resetAt,
}]);
});
});