Skip to content
Merged
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
415 changes: 314 additions & 101 deletions .github/workflows/tests.yml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions cli/src/bundle/auto-bump-ai.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SupabaseClient } from '@supabase/supabase-js'
import type { Database } from '../types/supabase.types.ts'
import type { AutoBumpLevel } from '../versionHelpers.ts'
import type { Database } from '../types/supabase.types'
import type { AutoBumpLevel } from '../versionHelpers'
import { log } from '@clack/prompts'
import { generateManifest, invokeCapgoCliApi } from '../utils'

Expand Down
4 changes: 2 additions & 2 deletions cli/test/prescan/request-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ describe('permission backstop fires before the POST on the prescan-skipped and -
true,
)
expect(result.success).toBe(false)
expect(result.error).toMatch(/insufficient permissions to request a native build/i)
expect(result.error).toMatch(/missing app\.build_native permission/i)
expect(probe.postedBuildRequest).toBe(false)
})

Expand All @@ -233,7 +233,7 @@ describe('permission backstop fires before the POST on the prescan-skipped and -
true,
)
expect(result.success).toBe(false)
expect(result.error).toMatch(/insufficient permissions to request a native build/i)
expect(result.error).toMatch(/missing app\.build_native permission/i)
expect(probe.postedBuildRequest).toBe(false)
})
})
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@
"test:all:local": "bun run test:all",
"test:backend": "bun run supabase:with-env -- bunx vitest run --exclude=tests/cli*",
"test:plugin": "bun run supabase:with-env -- bunx vitest run tests/updates*.test.ts tests/stats*.test.ts tests/channel_self*.test.ts",
"test:api": "bun run supabase:with-env -- bunx vitest run --exclude=tests/cli* --exclude=tests/updates* --exclude=tests/stats* --exclude=tests/channel_self*",
"test:api": "bun run supabase:with-env -- bunx vitest run --exclude=tests/cli* --exclude=tests/updates* --exclude=tests/stats* --exclude=tests/channel_self* --exclude=tests/channel-rate-limit.test.ts",
"test:channel_devices": "bun run supabase:with-env -- bunx vitest run tests/channel_devices/*.test.ts",
"test:cli": "bun run cli:build && bun run supabase:with-env -- bunx vitest run tests/cli*",
"test:cli:local": "bun run test:cli",
"test:cloudflare:all": "vitest run tests/* --config vitest.config.cloudflare.ts",
"test:cloudflare:backend": "vitest run --exclude=tests/cli* --config vitest.config.cloudflare.ts",
"test:cloudflare:plugin": "USE_CLOUDFLARE_WORKERS=true vitest run tests/updates*.test.ts tests/stats*.test.ts tests/channel_self*.test.ts --config vitest.config.cloudflare-plugin.ts",
"test:cloudflare:api": "vitest run --exclude=tests/cli* --exclude=tests/updates* --exclude=tests/stats* --exclude=tests/channel_self* --config vitest.config.cloudflare.ts",
"test:cloudflare:plugin": "USE_CLOUDFLARE_WORKERS=true vitest run tests/updates*.test.ts tests/stats*.test.ts tests/channel_self*.test.ts tests/channel-rate-limit.test.ts --exclude=tests/*.unit.test.ts --exclude=tests/stats-export-cors.test.ts --config vitest.config.cloudflare-plugin.ts",
"test:cloudflare:api": "vitest run --exclude=tests/cli* --exclude=tests/updates* --exclude=tests/stats* --exclude=tests/channel_self* --exclude=tests/channel-rate-limit.test.ts --config vitest.config.cloudflare.ts",
"test:cloudflare:updates": "vitest run tests/updates* --config vitest.config.cloudflare.ts",
"bench": "vitest bench --config vitest.config.bench.ts --run",
"bench:plugin-worker-runtime": "bun scripts/bench_plugin_worker_runtime.ts --compare scripts/bench/plugin_worker_runtime_baseline.json --save scripts/bench/plugin_worker_runtime_results.json",
Expand Down
8 changes: 4 additions & 4 deletions playwright/e2e/observe-tabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ test.describe('Observe sections', () => {
await expect(updaterTab).toHaveAttribute('aria-current', 'page')

await logsTab.click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/logs$/)
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/logs(?:\?|$)/)
await expect(logsTab).toHaveAttribute('aria-current', 'page')

await compatibilityTab.click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/compatibility$/)
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/compatibility(?:\?|$)/)
await expect(compatibilityTab).toHaveAttribute('aria-current', 'page')

await pluginsTab.click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/plugins$/)
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/plugins(?:\?|$)/)
await expect(pluginsTab).toHaveAttribute('aria-current', 'page')
await expect(page.locator('[data-test="observe-plugin-insights"]')).toBeVisible()
await expect(page.locator('[data-test="observe-plugin-insights"] table').getByText('4.15.3', { exact: true })).toBeVisible()
Expand All @@ -45,7 +45,7 @@ test.describe('Observe sections', () => {
expect((pluginsBox?.x ?? 0) + (pluginsBox?.width ?? 0)).toBeLessThanOrEqual(375)

await nativeTab.click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/native$/)
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/native(?:\?|$)/)
await expect(page.getByRole('heading', { name: 'All versions summary', exact: true })).toBeVisible()
})
})
113 changes: 107 additions & 6 deletions scripts/supabase-worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ function hasSupabaseCli(): boolean {
function getLocalSupabaseCli(repoRoot: string): string | null {
const binName = process.platform === 'win32' ? 'supabase.exe' : 'supabase'
// Prefer the package shim Bun/npm expose on PATH (.bin), then legacy bin/ layouts.
// Do NOT use dist/supabase.js — that is the installer stub and throws
// "No matching Supabase CLI binary package" when the platform binary is not
// extracted yet (common while `bun install` still runs in parallel with start).
const candidates = [
resolve(repoRoot, 'node_modules', '.bin', binName),
resolve(repoRoot, 'node_modules', 'supabase', 'bin', binName),
resolve(repoRoot, 'node_modules', 'supabase', 'dist', 'supabase.js'),
]
return candidates.find(candidate => existsSync(candidate)) ?? null
}
Expand Down Expand Up @@ -322,6 +324,28 @@ function isTransientDockerPortBindFailure(output: string): boolean {
|| /failed to bind host port/i.test(output)
}

function getCloudflareWorkerPorts(): number[] {
const raw = process.env.CLOUDFLARE_WORKER_PORT_OFFSET
if (!raw || !/^\d+$/.test(raw))
return []

const offset = Number(raw)
// Match scripts/start-cloudflare-workers.sh / cloudflare-test-config.ts so we never
// freeHostPorts() derived ports from an invalid offset before the launcher rejects it.
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 50_000)
Comment on lines +333 to +335

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reserve the default Cloudflare port band.

Line 333 states that this logic matches the launcher, but an unset CLOUDFLARE_WORKER_PORT_OFFSET returns [] here. The launcher treats an unset value as offset 0 and binds ports 8787 through 8789 and 9230 through 9232. A default worker run can therefore collide with an existing process that this cleanup did not reserve or release.

Normalize an unset or empty value to 0. Keep [] for malformed or out-of-range values.

Proposed fix
 function getCloudflareWorkerPorts(): number[] {
   const raw = process.env.CLOUDFLARE_WORKER_PORT_OFFSET
-  if (!raw || !/^\d+$/.test(raw))
+  if (raw && !/^\d+$/.test(raw))
     return []

-  const offset = Number(raw)
+  const offset = raw ? Number(raw) : 0
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/supabase-worktree.ts` around lines 333 - 335, Update the Cloudflare
port-offset parsing in the surrounding cleanup logic to normalize an unset or
empty CLOUDFLARE_WORKER_PORT_OFFSET value to offset 0, reserving the default
worker port bands 8787–8789 and 9230–9232. Continue returning [] for malformed,
unsafe, negative, or greater-than-50,000 offsets, and keep the existing
freeHostPorts behavior for valid offsets.

return []

// wrangler worker ports + inspector ports from scripts/start-cloudflare-workers.sh
return [
8787 + offset,
8788 + offset,
8789 + offset,
9230 + offset,
9231 + offset,
9232 + offset,
]
Comment on lines +338 to +346

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the wrangler worker and inspector base ports used by the startup script.
fd -t f 'start-cloudflare-workers.sh' -x cat -n {}
fd -t f 'cloudflare-test-config.ts' -x rg -n '87[0-9]{2}|92[0-9]{2}|PORT' {}

Repository: Cap-go/capgo.app

Length of output: 9245


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- scripts/cloudflare-test-config.ts ---'
cat -n scripts/cloudflare-test-config.ts | sed -n '1,90p'
printf '%s\n' '--- scripts/supabase-worktree.ts ---'
cat -n scripts/supabase-worktree.ts | sed -n '300,355p'
printf '%s\n' '--- port helper usages ---'
rg -n -C 3 'getCloudflareWorkerPorts|cloudflareWorkerPortOffset|API_INSPECTOR_PORT|PLUGIN_INSPECTOR_PORT|FILES_INSPECTOR_PORT' scripts

Repository: Cap-go/capgo.app

Length of output: 8465


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- cloudflareWorkerUrl call sites ---'
rg -n -C 2 'cloudflareWorkerUrl\(' --glob '!node_modules/**'
printf '%s\n' '--- all Cloudflare worker port literals ---'
rg -n -g '*.ts' -g '*.tsx' -g '*.vue' -g '*.sh' '8787|8788|8789|9230|9231|9232'
printf '%s\n' '--- offset bounds in port reservation ---'
cat -n scripts/supabase-worktree.ts | sed -n '354,380p'
cat -n scripts/supabase-worktree.ts | sed -n '450,475p'

Repository: Cap-go/capgo.app

Length of output: 193


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- all Cloudflare worker port literals ---'
rg -n -g '*.ts' -g '*.tsx' -g '*.vue' -g '*.sh' '8787|8788|8789|9230|9231|9232' . || true
printf '%s\n' '--- port reservation call sites ---'
cat -n scripts/supabase-worktree.ts | sed -n '354,380p'
cat -n scripts/supabase-worktree.ts | sed -n '450,475p'

Repository: Cap-go/capgo.app

Length of output: 4659


Centralize the Cloudflare port constants.

The ports match scripts/start-cloudflare-workers.sh, but the base values are duplicated across startup, test, and worktree scripts. Use one shared source to prevent drift.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/supabase-worktree.ts` around lines 336 - 344, Centralize the
Cloudflare worker and inspector base port values used by the returned array in
the worktree port logic, startup script, and test script. Introduce or reuse one
shared constants source, then update each consumer to derive its ports from
those constants while preserving the existing offset behavior and port
assignments.

}

/**
* Keep worktree host ports out of the kernel ephemeral pool.
*
Expand All @@ -336,11 +360,14 @@ function reserveWorktreePortsFromEphemeralPool(repoRoot: string): void {
return

const { cfg } = ensureWorktreeSupabaseDir(repoRoot)
const ports = Object.values(cfg.ports).filter(port => Number.isFinite(port)).sort((a, b) => a - b)
const ports = [
...Object.values(cfg.ports).filter(port => Number.isFinite(port)),
...getCloudflareWorkerPorts(),
].sort((a, b) => a - b)
if (ports.length === 0)
return

const reserved = ports.join(',')
const reserved = [...new Set(ports)].join(',')
const result = spawnSync('sudo', ['sysctl', '-w', `net.ipv4.ip_local_reserved_ports=${reserved}`], {
encoding: 'utf8',
})
Expand All @@ -356,24 +383,92 @@ function reserveWorktreePortsFromEphemeralPool(repoRoot: string): void {
console.error(`Reserved Supabase worktree ports from ephemeral pool: ${reserved}`)
}

/**
* Drop whatever still holds worktree host ports after a partial Docker start.
*
* `fuser` alone is not enough on GitHub runners: docker-proxy / leftover
* containers from a failed bind can keep the port until removed explicitly.
*/
function freeHostPorts(ports: number[]): void {
if (process.platform === 'win32' || ports.length === 0)
return

for (const port of ports) {
const uniquePorts = [...new Set(ports.filter(port => Number.isFinite(port)))]
const holders = new Set<string>()

for (const port of uniquePorts) {
const byPublish = spawnSync('docker', ['ps', '-aq', '--filter', `publish=${port}`], {
encoding: 'utf8',
})
for (const id of (byPublish.stdout ?? '').split(/\s+/).filter(Boolean))
holders.add(id)
}

// Match host-port publish strings docker prints (0.0.0.0:58722->5432/tcp).
const listed = spawnSync('docker', ['ps', '-a', '--format', '{{.ID}} {{.Ports}}'], {
encoding: 'utf8',
})
if ((listed.status ?? 1) === 0) {
for (const line of (listed.stdout ?? '').split('\n')) {
const trimmed = line.trim()
if (!trimmed)
continue
const spaceIdx = trimmed.indexOf(' ')
const id = spaceIdx >= 0 ? trimmed.slice(0, spaceIdx) : trimmed
const published = spaceIdx >= 0 ? trimmed.slice(spaceIdx + 1) : ''
if (uniquePorts.some(port => published.includes(`:${port}->`) || published.includes(`:${port}/`)))
holders.add(id)
}
}

if (holders.size > 0) {
console.error(`Removing Docker containers still publishing worktree ports: ${[...holders].join(', ')}`)
spawnSync('docker', ['rm', '-f', ...holders], { stdio: 'inherit' })

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: Starting a worktree can destroy an unrelated Docker container that happens to publish one of these ports because this cleanup force-removes all matching container IDs without checking the current projectId. Scoping removal to this worktree's project/labels, or leaving unrelated holders for the bind error, would avoid destructive cleanup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/supabase-worktree.ts, line 424:

<comment>Starting a worktree can destroy an unrelated Docker container that happens to publish one of these ports because this cleanup force-removes all matching container IDs without checking the current `projectId`. Scoping removal to this worktree's project/labels, or leaving unrelated holders for the bind error, would avoid destructive cleanup.</comment>

<file context>
@@ -381,12 +381,53 @@ function reserveWorktreePortsFromEphemeralPool(repoRoot: string): void {
+
+  if (holders.size > 0) {
+    console.error(`Removing Docker containers still publishing worktree ports: ${[...holders].join(', ')}`)
+    spawnSync('docker', ['rm', '-f', ...holders], { stdio: 'inherit' })
+  }
+
</file context>

}

for (const port of uniquePorts) {
spawnSync('fuser', ['-k', `${port}/tcp`], { stdio: 'ignore' })
// Close lingering sockets that still occupy the port after docker-proxy dies.
spawnSync('ss', ['-K', 'sport', '=', `:${port}`], { stdio: 'ignore' })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The newly added ss -K sport = :NNNN cleanup step (Close lingering sockets ... after docker-proxy dies) is likely a silent no-op on GitHub Actions: destroying sockets via ss -K needs CAP_NET_ADMIN (root), which the default non-root runner user does not have, and the failure is swallowed by stdio: 'ignore'. Also, ss -K cannot kill TIME_WAIT sockets, which is the exact lingering state after docker-proxy exits. The effective cleanup here is the docker ps/rm -f block just above it; consider dropping the ss call or confirming it actually runs (e.g. run it with sudo / capture and log the exit status) so the intended port release is not silently skipped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/supabase-worktree.ts, line 430:

<comment>The newly added `ss -K sport = :NNNN` cleanup step (`Close lingering sockets ... after docker-proxy dies`) is likely a silent no-op on GitHub Actions: destroying sockets via `ss -K` needs CAP_NET_ADMIN (root), which the default non-root `runner` user does not have, and the failure is swallowed by `stdio: 'ignore'`. Also, `ss -K` cannot kill TIME_WAIT sockets, which is the exact lingering state after docker-proxy exits. The effective cleanup here is the docker `ps`/`rm -f` block just above it; consider dropping the `ss` call or confirming it actually runs (e.g. run it with `sudo` / capture and log the exit status) so the intended port release is not silently skipped.</comment>

<file context>
@@ -381,12 +381,53 @@ function reserveWorktreePortsFromEphemeralPool(repoRoot: string): void {
+  for (const port of uniquePorts) {
     spawnSync('fuser', ['-k', `${port}/tcp`], { stdio: 'ignore' })
+    // Close lingering sockets that still occupy the port after docker-proxy dies.
+    spawnSync('ss', ['-K', 'sport', '=', `:${port}`], { stdio: 'ignore' })
   }
 }
</file context>

}
}
Comment on lines 392 to 434

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Restrict container removal to CI or to this worktree's containers.

freeHostPorts runs on every supabase start through runSupabaseStartWithRetry, including local developer runs. It force-removes any Docker container that publishes one of the target ports. The Cloudflare ports (8787, 8788, 8789) and inspector ports (9230-9232) are common local ports, so an unrelated developer container can be deleted without warning.

removeLeftoverWorktreeContainers already handles this worktree safely by filtering on projectId. Gate the broad port-based removal behind CI, or filter the discovered containers by the worktree project name before removal.

🛡️ Proposed guard
-function freeHostPorts(ports: number[]): void {
+function freeHostPorts(ports: number[]): void {
   if (process.platform === 'win32' || ports.length === 0)
     return
+  // Removing containers by host port can hit unrelated local stacks; only do it in CI.
+  const allowContainerRemoval = Boolean(process.env.CI)
 
   const uniquePorts = [...new Set(ports.filter(port => Number.isFinite(port)))]
   const holders = new Set<string>()
 
+  if (allowContainerRemoval) {
   for (const port of uniquePorts) {
     ...
   }
+  }
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/supabase-worktree.ts` around lines 390 - 432, Restrict the
container-removal block in freeHostPorts to CI runs or containers belonging to
this worktree, using the same projectId/project-name filtering as
removeLeftoverWorktreeContainers. Do not force-remove unrelated local Docker
containers that happen to publish the target ports; retain port cleanup only for
approved containers.


/**
* Cancelled CI jobs can leave named Supabase containers that still hold host
* ports after `supabase stop`. Force-remove anything matching this worktree.
*/
function removeLeftoverWorktreeContainers(projectId: string): void {
if (process.platform === 'win32')
return

const listed = spawnSync('docker', ['ps', '-aq', '--filter', `name=${projectId}`], {
encoding: 'utf8',
})
if ((listed.status ?? 1) !== 0)
return

const ids = (listed.stdout ?? '').split(/\s+/).filter(Boolean)
if (ids.length === 0)
return

console.error(`Removing leftover Docker containers for ${projectId}: ${ids.join(', ')}`)
spawnSync('docker', ['rm', '-f', ...ids], { stdio: 'inherit' })
}

/**
* `supabase start` can fail on GitHub runners with a transient Docker port bind
* (`address already in use`) after a partial start/stop. Retry only that class of
* failure so permanent start errors fail fast.
*/
function runSupabaseStartWithRetry(args: string[], repoRoot: string): number {
const { cfg } = ensureWorktreeSupabaseDir(repoRoot)
const ports = Object.values(cfg.ports).filter(port => Number.isFinite(port))
const ports = [
...Object.values(cfg.ports).filter(port => Number.isFinite(port)),
...getCloudflareWorkerPorts(),
]
reserveWorktreePortsFromEphemeralPool(repoRoot)
removeLeftoverWorktreeContainers(cfg.projectId)
freeHostPorts(ports)

const maxAttempts = 5
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
Expand All @@ -385,8 +480,14 @@ function runSupabaseStartWithRetry(args: string[], repoRoot: string): number {
return status
console.error(`Supabase start hit a transient Docker port bind (attempt ${attempt}/${maxAttempts}); stopping and retrying...`)
runSupabase(['stop', '--no-backup'], repoRoot)
removeLeftoverWorktreeContainers(cfg.projectId)
freeHostPorts(ports)
spawnSync(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/T', '2', '/NOBREAK'] : ['2'])
// Back off so docker-proxy / TIME_WAIT can release before the next bind.
const sleepSeconds = String(Math.min(2 ** attempt, 8))
spawnSync(
process.platform === 'win32' ? 'timeout' : 'sleep',
process.platform === 'win32' ? ['/T', sleepSeconds, '/NOBREAK'] : [sleepSeconds],
)
}
return 1
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/admin/AdminFilterBar.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { LocationQueryRaw } from 'vue-router'
import type { DateRangeMode } from '~/stores/adminDashboard'
import { useMutationObserver } from '@vueuse/core'
import { onMounted, onUnmounted, ref, watch } from 'vue'
Expand Down Expand Up @@ -102,7 +103,7 @@ function syncStoreToQuery() {
return
}

const nextQuery: Record<string, string | string[] | null | undefined> = { ...route.query }
const nextQuery: LocationQueryRaw = { ...route.query }
nextQuery.range = serialized.range
if (serialized.start && serialized.end) {
nextQuery.start = serialized.start
Expand Down
2 changes: 2 additions & 0 deletions tests/admin-stats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,8 @@ describe('/private/admin_stats', () => {
expect(payload.success).toBe(true)
expect(payload.data.trend).toHaveLength(1)
expect(payload.data.trend[0]?.date).toBe('2026-02-01')
// Counts all trial orgs created that day: ONBOARDING_ORG, ONBOARDING_NO_BUNDLE_ORG,
// ONBOARDING_LATE_SUBSCRIPTION_ORG, and ONBOARDING_INVITE_ORG (invite orgs are included).
expect(payload.data.trend[0]?.total).toBe(4)
expect(payload.data.trend[0]?.plans[soloPlan?.name ?? 'Solo']).toBe(4)
expect(payload.data.totals.find(plan => plan.plan_name === (soloPlan?.name ?? 'Solo'))?.total).toBe(4)
Expand Down
1 change: 1 addition & 0 deletions tests/cli-app-permission-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const hasCliPermissionMock = vi.hoisted(() => vi.fn())

vi.mock('../cli/src/utils', () => ({
getPMAndCommand: () => ({ runner: 'bunx' }),
appAddHintMessage: (appId: string) => `App ${appId} does not exist, run first \`bunx @capgo/cli app add ${appId}\` to create it`,
hasCliPermission: hasCliPermissionMock,
show2FADeniedError: vi.fn(() => {
throw new Error('2FA required')
Expand Down
8 changes: 7 additions & 1 deletion tests/cron_stat_app.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { BASE_URL, ORG_ID_CRON_APP, STRIPE_CUSTOMER_ID_CRON_APP, getSupabaseClient, resetAndSeedAppData, resetAndSeedAppDataStats, resetAppData, resetAppDataStats } from './test-utils.ts'
import { BASE_URL, ORG_ID_CRON_APP, STRIPE_CUSTOMER_ID_CRON_APP, getSupabaseClient, resetAndSeedAppData, resetAndSeedAppDataStats, resetAppData, resetAppDataStats, warmEdgeEndpoint } from './test-utils.ts'

const appId = `com.cron.${randomUUID().slice(0, 8)}`

Expand All @@ -24,6 +24,12 @@ describe('[POST] /triggers/cron_stat_app', () => {
.eq('id', ORG_ID_CRON_APP)
if (error)
throw error

await warmEdgeEndpoint('/triggers/cron_stat_app', {
method: 'POST',
headers: triggerHeaders,
body: JSON.stringify({ appId, orgId: ORG_ID_CRON_APP }),
})
Comment on lines +28 to +32

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the cron_stat_app test setup and the trigger's write behavior.
fd -t f 'cron_stat_app.test.ts' -x sed -n '1,80p' {}
fd -t f -g '*cron_stat_app*' supabase | head -20

Repository: Cap-go/capgo.app

Length of output: 2678


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- test file ---'
sed -n '1,220p' tests/cron_stat_app.test.ts
printf '%s\n' '--- trigger outline/source ---'
wc -l supabase/functions/_backend/triggers/cron_stat_app.ts
sed -n '1,260p' supabase/functions/_backend/triggers/cron_stat_app.ts
printf '%s\n' '--- reset helpers ---'
rg -n -A35 -B10 'resetAndSeedAppDataStats|resetAppDataStats|resetAndSeedAppData|warmEdgeEndpoint' tests/test-utils.ts tests

Repository: Cap-go/capgo.app

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- cron_stat_app.test.ts ---'
nl -ba tests/cron_stat_app.test.ts | sed -n '1,180p'
printf '%s\n' '--- cron_stat_app.ts symbols and writes ---'
rg -n '^(export )?(async )?function|from\\(|insert|update|upsert|delete|rpc|queue|stats|return' supabase/functions/_backend/triggers/cron_stat_app.ts
printf '%s\n' '--- cron_stat_app.ts relevant sections ---'
nl -ba supabase/functions/_backend/triggers/cron_stat_app.ts | sed -n '1,240p'
printf '%s\n' '--- helper definitions ---'
rg -n '^(export )?async function (resetAndSeedAppDataStats|resetAppDataStats|resetAndSeedAppData|resetAppData|warmEdgeEndpoint)' tests/test-utils.ts

Repository: Cap-go/capgo.app

Length of output: 411


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- cron_stat_app.test.ts ---'
awk '{printf "%5d %s\n", NR, $0}' tests/cron_stat_app.test.ts | sed -n '1,180p'
printf '%s\n' '--- cron_stat_app.ts symbols and writes ---'
rg -n 'function|from\(|insert|update|upsert|delete|rpc|queue|stats|return' supabase/functions/_backend/triggers/cron_stat_app.ts || true
printf '%s\n' '--- cron_stat_app.ts relevant sections ---'
awk '{printf "%5d %s\n", NR, $0}' supabase/functions/_backend/triggers/cron_stat_app.ts | sed -n '1,260p'
printf '%s\n' '--- helper definitions ---'
rg -n 'resetAndSeedAppDataStats|resetAppDataStats|resetAndSeedAppData|resetAppData|warmEdgeEndpoint' tests/test-utils.ts | head -80

Repository: Cap-go/capgo.app

Length of output: 20695


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- trigger handler ---'
sed -n '480,690p' supabase/functions/_backend/triggers/cron_stat_app.ts
printf '%s\n' '--- warm-up helper ---'
sed -n '490,535p' tests/test-utils.ts
printf '%s\n' '--- seed and reset helpers ---'
sed -n '650,750p' tests/test-utils.ts
printf '%s\n' '--- stats table references in helpers and migrations ---'
rg -n 'daily_mau|daily_bandwidth|daily_storage|daily_version|daily_storage_hourly|app_metrics_cache|stats_updated_at|plan_calculated_at' tests/test-utils.ts supabase/migrations | head -160

Repository: Cap-go/capgo.app

Length of output: 40219


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- seed/reset SQL definitions ---'
rg -n -A90 -B10 'reset_and_seed_app_stats_data|reset_app_stats_data|reset_and_seed_app_data\\(' supabase/migrations | head -420
printf '%s\n' '--- queue and stats-refresh SQL definitions ---'
rg -n -A80 -B15 'queue_cron_stat_org_for_org|mark_app_stats_refreshed|sync.*stats|plan_calculated_at' supabase/migrations | head -500
printf '%s\n' '--- assertions or reads of affected stats rows in the test ---'
rg -n 'daily_|storage_hourly|stats_updated_at|plan_calculated_at|queue_cron|rpc\\(' tests/cron_stat_app.test.ts

Repository: Cap-go/capgo.app

Length of output: 48896


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- seed/reset function locations ---'
for term in reset_and_seed_app_stats_data reset_app_stats_data reset_and_seed_app_data; do
  printf '\n[%s]\n' "$term"
  rg -n -F "$term" supabase/migrations tests/test-utils.ts | head -30
done
printf '%s\n' '--- queue function and refresh logic ---'
sed -n '10760,10855p' supabase/migrations/20260708000000_prod_baseline.sql
printf '%s\n' '--- test affected-state references ---'
for term in 'daily_' 'storage_hourly' 'stats_updated_at' 'plan_calculated_at' 'queue_cron' 'rpc('; do
  printf '\n[%s]\n' "$term"
  rg -n -F "$term" tests/cron_stat_app.test.ts || true
done
printf '%s\n' '--- targeted SQL extraction ---'
python3 - <<'PY'
from pathlib import Path
text = Path('supabase/migrations/20260708000000_prod_baseline.sql').read_text()
terms = ('reset_and_seed_app_stats_data', 'reset_app_stats_data', 'reset_and_seed_app_data')
for term in terms:
    pos = text.find(term)
    print(f'\n--- {term}: offset {pos} ---')
    if pos >= 0:
        start = max(0, text.rfind('CREATE OR REPLACE FUNCTION', 0, pos) - 100)
        end = text.find('ALTER FUNCTION', pos)
        print(text[start:end if end >= 0 else pos + 6000][:12000])
PY

Repository: Cap-go/capgo.app

Length of output: 4275


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- locate reset function definitions across the repository ---'
rg -n -F 'reset_and_seed_app_stats_data' . || true
rg -n -F 'reset_app_stats_data' . || true
rg -n -F 'CREATE OR REPLACE FUNCTION' supabase tests | rg 'reset|seed' | head -120
printf '%s\n' '--- locate sync and pending-refresh definitions ---'
rg -n -F 'syncOrgStatsRefresh' supabase/functions/_backend/triggers/cron_stat_app.ts
sed -n '350,490p' supabase/functions/_backend/triggers/cron_stat_app.ts
rg -n -F 'has_pending_app_stats_refresh' supabase/migrations supabase | head -30
printf '%s\n' '--- identify all assertions in the test ---'
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path('tests/cron_stat_app.test.ts').read_text().splitlines(), 1):
    if any(x in line for x in ('expect(', '.select(', '.update(', '.rpc(')):
        print(f'{i}: {line}')
PY

Repository: Cap-go/capgo.app

Length of output: 10009


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- reset_app_stats_data ---'
sed -n '1110,1165p' supabase/seed.sql
printf '%s\n' '--- reset_and_seed_app_stats_data ---'
sed -n '1150,1228p' supabase/seed.sql
printf '%s\n' '--- reset_and_seed_app_data ---'
sed -n '895,1015p' supabase/seed.sql

Repository: Cap-go/capgo.app

Length of output: 11336


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- shared cron test constants and usages ---'
rg -n -F 'ORG_ID_CRON_APP' tests | head -120
printf '%s\n' '--- stats trigger test constants ---'
rg -n -F 'STRIPE_CUSTOMER_ID_CRON_APP' tests | head -120

Repository: Cap-go/capgo.app

Length of output: 1615


Use an inert warm-up payload.

This payload executes the trigger and mutates daily stats, refresh timestamps, and the plan-refresh queue before the tests run. Use {} so warmEdgeEndpoint warms the isolate without running business logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cron_stat_app.test.ts` around lines 28 - 32, Update the
warmEdgeEndpoint call in the cron_stat_app test setup to send an inert empty
object payload instead of appId and orgId, while preserving the POST method and
headers so warming does not execute business logic.

})

afterAll(async () => {
Expand Down
5 changes: 4 additions & 1 deletion tests/cron_stat_org.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto'
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { BASE_URL, fetchTestRequest, getBaseData, getSupabaseClient, PRODUCT_ID, postUpdate, TEST_EMAIL, USER_ID } from './test-utils.ts'
import { BASE_URL, fetchTestRequest, getBaseData, getSupabaseClient, PRODUCT_ID, postUpdate, TEST_EMAIL, USER_ID, warmEdgeEndpoint } from './test-utils.ts'

// Create unique IDs for this test file to avoid parallel test interference
const id = randomUUID()
Expand Down Expand Up @@ -55,6 +55,9 @@ beforeAll(async () => {
})
if (appError)
throw appError

// Cold first POST to cron_stat_org can 502 under Deno load; warm before assertions.
await warmEdgeEndpoint('/triggers/cron_stat_org')
})

// Reset state before each test
Expand Down
Loading
Loading