--- phase: 4 title: Tests + CI status: completed priority: P2 effort: 2h dependencies: - 2 - 3 --- # Phase 4: Tests + CI ## Overview Vitest with `@cloudflare/vitest-pool-workers` for in-Workers-runtime tests. Cover template rendering, fire client behavior (success / non-2xx / network error), token redaction. GitHub Actions CI on push. ## Requirements - **Functional:** - Tests cover: template substitution, missing-token rendering, fire success path, non-2xx path, fetch throws path. - CI runs `npm test` on push to `main` and PRs. - **Non-functional:** - Coverage report (text summary in CI logs). - Tests use mocked `fetch` — no real network calls (don't burn user's Anthropic quota). - Tests written in plain JS with JSDoc, matching production code style. - Race-free is N/A on Workers; instead assert `console.log` output never contains token. ## Architecture ### Test layout ``` worker.test.js # all tests in one file initially (KISS) ``` Split if exceeds 200 lines. ### Test cases (mirror Go daemon's test coverage) | # | Case | Assertion | |---|------|-----------| | 1 | `renderText` substitutes `{LocalTime}`, `{Cron}`, `{ISO}` | output equals expected string | | 2 | `renderText` leaves unknown `{Foo}` tokens intact | output contains `{Foo}` | | 3 | `fireRoutine` 2xx response logs `session_url` | console captured contains `session_url` | | 4 | `fireRoutine` 401 response logs `level:error` | captured log has `level:'error'`, `status:401` | | 5 | `fireRoutine` `fetch` rejects (network) logs error, doesn't throw | no thrown exception, error log present | | 6 | Token redaction | for cases 3, 4, 5: stringified logs do NOT contain token value | | 7 | Headers correctness | mocked `fetch` receives `Authorization: Bearer ${token}`, `anthropic-version`, `anthropic-beta` | | 8 | Body shape | request body parses as `{ "text": }` | | 9 | Default template when `TEXT_TEMPLATE` unset | output starts with `Scheduled trigger at ` | | 10 | TZ defaults to UTC when `env.TZ` unset | local time formatting uses UTC | ### CI workflow `.github/workflows/ci.yml` — single job, Node 20, runs test + wrangler dry-run as smoke gate. ## Related Code Files - Create: `worker.test.js` - Create: `vitest.config.js` (uses `@cloudflare/vitest-pool-workers`) - Create: `.github/workflows/ci.yml` ## Implementation Steps 1. Install: `npm i -D vitest @cloudflare/vitest-pool-workers`. 2. Write `vitest.config.js`: ```js import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: './wrangler.toml' } } } }, }); ``` 3. Write `worker.test.js` covering all 10 cases. Use `vi.fn()` mock for `globalThis.fetch`. JSDoc-annotate test helper functions for IDE assist. 4. Run `npm test` — all green. 5. Write `.github/workflows/ci.yml`: ```yaml name: ci on: push: { branches: [main] } pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: 'npm' } - run: npm ci - run: npm test - run: npx wrangler deploy --dry-run ``` 6. Push to GitHub; verify green CI. ## Success Criteria - [ ] All 10 test cases pass locally - [ ] `npm test` exits 0 in CI - [ ] `wrangler deploy --dry-run` exits 0 in CI - [ ] No real HTTP traffic generated by tests (verify by mock spy) - [ ] Coverage ≥ 80% (track via vitest `--coverage`; not enforced as blocking gate) ## Risk Assessment - **Workers test pool quirks:** `@cloudflare/vitest-pool-workers` requires wrangler config; ensure `wrangler.toml` validates first. - **Mocking `fetch` inside Workers runtime:** verify pattern works — fall back to dependency-injected fetch if not. - **Beta header pinning in tests:** assert exact value to catch accidental edits. ## Security Considerations - Test fixtures use placeholder token `sk-ant-oat01-test-only` — never a real value. - Test asserts logs **don't contain** the token (regression guard).