From 8e2d7d0c143e56c0cfa6cabb013730929dde9231 Mon Sep 17 00:00:00 2001 From: PoterPan Date: Sun, 7 Jun 2026 17:32:10 +0800 Subject: [PATCH 1/3] test(worker): make suite pass without .dev.vars; fix time-dependent period test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite assumed a local .dev.vars (gitignored, holds a real bot token) supplied DISCORD_BOT_TOKEN. On a clean clone / in CI the token is undefined, so 7 tests whose assertions depend on a notification actually being sent/claimed failed — sending is gated on env.DISCORD_BOT_TOKEN (scheduled.ts, billing.ts, admin.ts). - billing-initiate / scheduled: set a dummy DISCORD_BOT_TOKEN in beforeAll (fake notifier, no network). Mirrors the existing pattern in discord-initiate / discord-notify tests. - admin: set+restore the token only inside the two fetch-stubbed tests, so the later billing/initiate test (real discordNotifier, no fetch stub) keeps its no-real-send behavior. - discord-initiate 'opens a modal': compute PERIOD via nextBillingPeriod(5) to match the handler instead of taipeiPeriod() — the old assumption only held on days 1–5 of a month. - add .dev.vars.example template + DEPLOY.md note that tests don't need .dev.vars. Verified: full suite green both with and without .dev.vars present (158 passed). --- docs/DEPLOY.md | 10 +++++++++- packages/worker/.dev.vars.example | 15 +++++++++++++++ .../worker/test/adapters/discord-initiate.test.ts | 8 ++++++-- .../worker/test/core/billing-initiate.test.ts | 3 +++ packages/worker/test/core/scheduled.test.ts | 3 +++ packages/worker/test/routes/admin.test.ts | 8 ++++++++ 6 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 packages/worker/.dev.vars.example diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index bb2f806..d8411ce 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -140,11 +140,19 @@ cd packages/worker wrangler secret put DISCORD_BOT_TOKEN # 貼上第 3 步的 token ``` -再建一個 `packages/worker/.dev.vars`(已被 gitignore)給「註冊 slash 指令」腳本與本地開發用: +再建一個 `packages/worker/.dev.vars`(已被 gitignore)給「註冊 slash 指令」腳本與本地開發用。直接從範本複製再填值: +```bash +cd packages/worker +cp .dev.vars.example .dev.vars # 然後填入真實值 +``` +內容(鍵見 `.dev.vars.example`): ``` +CLOUDFLARE_API_TOKEN=你的-cloudflare-api-token DISCORD_BOT_TOKEN=你的-bot-token DISCORD_APPLICATION_ID=你的-application-id +DISCORD_GUILD_ID=你的-guild-id ``` +> 註:跑測試(`pnpm test`)**不需要**這個檔——測試會自帶假 token,乾淨 clone/CI 沒有 `.dev.vars` 也能全綠。 --- diff --git a/packages/worker/.dev.vars.example b/packages/worker/.dev.vars.example new file mode 100644 index 0000000..9149eb6 --- /dev/null +++ b/packages/worker/.dev.vars.example @@ -0,0 +1,15 @@ +# Local dev secrets template. Copy to `.dev.vars` and fill in real values: +# cp .dev.vars.example .dev.vars +# +# `.dev.vars` is gitignored — never commit real secrets (this repo is public). +# NOTE: the test suite does NOT need this file — tests set their own dummy token, +# so `vitest run` passes on a clean checkout / in CI without any `.dev.vars`. +# +# These are only needed to actually run things locally: +# - `wrangler dev` -> CLOUDFLARE_API_TOKEN, DISCORD_BOT_TOKEN +# - `node scripts/register-commands.mjs` -> DISCORD_BOT_TOKEN, DISCORD_APPLICATION_ID, DISCORD_GUILD_ID + +CLOUDFLARE_API_TOKEN=your-cloudflare-api-token +DISCORD_BOT_TOKEN=your-discord-bot-token +DISCORD_APPLICATION_ID=your-discord-application-id +DISCORD_GUILD_ID=your-discord-guild-id diff --git a/packages/worker/test/adapters/discord-initiate.test.ts b/packages/worker/test/adapters/discord-initiate.test.ts index e3bef57..bbc651a 100644 --- a/packages/worker/test/adapters/discord-initiate.test.ts +++ b/packages/worker/test/adapters/discord-initiate.test.ts @@ -1,7 +1,7 @@ import { env } from "cloudflare:test"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { routeInteraction, type DiscordInteraction } from "../../src/adapters/discord/handler"; -import { taipeiPeriod } from "../../src/core/time"; +import { nextBillingPeriod } from "../../src/core/time"; const TS = "2026-05-01T00:00:00.000Z"; const WS = 9025; @@ -11,7 +11,11 @@ const NONADMIN = "rando-9025"; const PLAN = 9025; const SUB = 9025; const CHAN = "chan-9025"; -const PERIOD = taipeiPeriod(); +// The 發起繳費 modal opens the *next* billing period: the handler computes it with +// nextBillingPeriod(workspace.billing_day), which rolls to next month once today is past the +// billing day. Match that here (workspace billing_day = 5, seeded below) instead of assuming the +// current calendar month — that assumption only held on days 1–5 and broke after the 5th. +const PERIOD = nextBillingPeriod(5); const tasks: Promise[] = []; const CTX = { waitUntil: (p: Promise) => tasks.push(p) } as unknown as ExecutionContext; diff --git a/packages/worker/test/core/billing-initiate.test.ts b/packages/worker/test/core/billing-initiate.test.ts index bccbef6..bb48fcc 100644 --- a/packages/worker/test/core/billing-initiate.test.ts +++ b/packages/worker/test/core/billing-initiate.test.ts @@ -17,6 +17,9 @@ const notifier: Notifier = { }; beforeAll(async () => { + // Tests must not depend on .dev.vars (CI / a fresh clone has none). The notifier here is a + // fake, so this token only flips the env gate that lets initiateBillingOpened actually "send". + (env as any).DISCORD_BOT_TOKEN = "test-bot-token"; const settings = JSON.stringify({ discord_billing_channel_id: CHAN }); await env.DB.batch([ env.DB.prepare(`INSERT INTO workspaces (id,name,owner_id,channel_type,billing_day,settings,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`).bind(WS, "W", "o", "discord", 5, settings, TS, TS), diff --git a/packages/worker/test/core/scheduled.test.ts b/packages/worker/test/core/scheduled.test.ts index cb4643e..45b6aac 100644 --- a/packages/worker/test/core/scheduled.test.ts +++ b/packages/worker/test/core/scheduled.test.ts @@ -17,6 +17,9 @@ const notifier: Notifier = { }; beforeAll(async () => { + // Tests must not depend on .dev.vars (CI / a fresh clone has none). The notifier here is a + // fake, so this token only flips the env gate that lets runDailyTasks actually "send". + (env as any).DISCORD_BOT_TOKEN = "test-bot-token"; const settings = JSON.stringify({ discord_billing_channel_id: CHAN, overdue_days: 3, proof_retention_months: 24 }); await env.DB.batch([ env.DB.prepare(`INSERT INTO workspaces (id,name,owner_id,channel_type,billing_day,settings,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`).bind(WS, "W", "o", "discord", 5, settings, TS, TS), diff --git a/packages/worker/test/routes/admin.test.ts b/packages/worker/test/routes/admin.test.ts index d1873a2..b80e6b7 100644 --- a/packages/worker/test/routes/admin.test.ts +++ b/packages/worker/test/routes/admin.test.ts @@ -86,9 +86,14 @@ describe("admin API", () => { it("creates/rebuilds the persistent Discord payment message", async () => { await call("PATCH", "/admin/workspace", { settings: { discord_billing_channel_id: "chan-1" } }); + // Supply the bot token locally (CI has no .dev.vars), then restore it — the later + // billing/initiate test doesn't stub fetch, so it must keep its no-real-send behavior. + const prevToken = (env as any).DISCORD_BOT_TOKEN; + (env as any).DISCORD_BOT_TOKEN = "test-bot-token"; vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: "msg-123" }), { status: 200 }))); const res = await call("POST", "/admin/discord/payment-message"); vi.unstubAllGlobals(); + (env as any).DISCORD_BOT_TOKEN = prevToken; expect(res!.status).toBe(200); expect(((await res!.json()) as any).message_id).toBe("msg-123"); }); @@ -164,9 +169,12 @@ describe("admin notifications", () => { expect(st.billing_opened).toBeNull(); expect(st.overdue).toBeNull(); + const prevToken = (env as any).DISCORD_BOT_TOKEN; + (env as any).DISCORD_BOT_TOKEN = "test-bot-token"; vi.stubGlobal("fetch", vi.fn(async () => new Response("{}", { status: 200 }))); const r = await call("POST", "/admin/notifications/resend", { type: "overdue", period: "2028-03" }); vi.unstubAllGlobals(); + (env as any).DISCORD_BOT_TOKEN = prevToken; expect(r!.status).toBe(200); expect(((await r!.json()) as any).count).toBeGreaterThanOrEqual(1); st = (await (await call("GET", "/admin/notifications?period=2028-03"))!.json()) as any; From e3a7afa91f23fb53758b8b33869b047c0921d909 Mon Sep 17 00:00:00 2001 From: PoterPan Date: Sun, 7 Jun 2026 17:33:48 +0800 Subject: [PATCH 2/3] ci: add GitHub Actions workflow (typecheck + test + build) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs on PRs and pushes to main: pnpm install --frozen-lockfile, then `pnpm -r` typecheck / test / build across the worker/web/admin packages. A red run blocks merge — this is exactly the class of breakage (env-dependent tests, type errors, build failures) that previously slipped through. Also move pnpm's onlyBuiltDependencies from package.json's "pnpm" field to pnpm-workspace.yaml: pnpm 10 no longer reads the former (it logged a warning and the setting was inert), so a fresh CI install would skip building esbuild/workerd/ sharp and break the test pool + vite builds. Add a root "build" script for symmetry. Verified locally: frozen-lockfile install in sync; typecheck/test/build all green with .dev.vars absent (the CI condition). --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ package.json | 6 ++---- pnpm-workspace.yaml | 9 +++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..244008c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +# Cancel an in-progress run when a newer commit is pushed to the same ref. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # pnpm version comes from the root package.json "packageManager" field (single source). + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - run: pnpm -r typecheck + + # The worker test suite needs no secrets: it sets its own dummy DISCORD_BOT_TOKEN, + # so it passes here without a .dev.vars (which is gitignored and never in CI). + - run: pnpm -r test + + - run: pnpm -r build diff --git a/package.json b/package.json index 9db01d5..9fb97c1 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,7 @@ "packageManager": "pnpm@10.33.0", "scripts": { "test": "pnpm -r test", - "typecheck": "pnpm -r typecheck" - }, - "pnpm": { - "onlyBuiltDependencies": ["esbuild", "workerd", "sharp"] + "typecheck": "pnpm -r typecheck", + "build": "pnpm -r build" } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dee51e9..660fdbc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,11 @@ packages: - "packages/*" + +# pnpm 10 only runs build scripts for packages on this allowlist. Required so a fresh +# `pnpm install` (e.g. in CI) compiles these native deps — workerd powers the worker test +# pool, esbuild/sharp power the vite builds. (Moved here from package.json's "pnpm" field, +# which pnpm 10 no longer reads.) +onlyBuiltDependencies: + - esbuild + - workerd + - sharp From cca6161a724dcf2c339e0be0f2ba68365b92ae6e Mon Sep 17 00:00:00 2001 From: PoterPan Date: Sun, 7 Jun 2026 17:38:28 +0800 Subject: [PATCH 3/3] ci: bump actions to v6 (Node 24 runtime) to clear deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/checkout, actions/setup-node, pnpm/action-setup all run on Node.js 20 at their v4 pins; GitHub forces JS actions to Node 24 on 2026-06-16. v6 of each runs on node24, clearing the deprecation warning. Verified safe for this workflow: - checkout v6: bare usage (no inputs), only internal cred-handling change. - setup-node v6: the v6 breaking change limits *automatic* caching to npm; we pass an explicit `cache: pnpm` (still a supported value), unaffected. pnpm/action-setup still runs first so pnpm is on PATH for the cache step. - pnpm/action-setup v6: version still auto-detected from packageManager; adds pnpm 11. Node 22 (app runtime) unchanged — the deprecation was about the action runtime, not it. --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 244008c..2cc0254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,13 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # pnpm version comes from the root package.json "packageManager" field (single source). - - uses: pnpm/action-setup@v4 + # Must run before setup-node so its `cache: pnpm` can find pnpm on PATH. + - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm