mirror of
https://github.com/tiennm99/claude-code-routine-trigger.git
synced 2026-10-05 08:14:27 +00:00
test: vitest in Workers pool, 12 cases incl token redaction + CI
This commit is contained in:
1 parent
b26b101c9a
commit
1b9a6eba89
3 files changed
+212
No files matched your search
+21
@@ -0,0 +1,21 @@
|
||||
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
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: dummy
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
|
||||
|
||||
export default defineWorkersConfig({
|
||||
test: {
|
||||
poolOptions: {
|
||||
workers: {
|
||||
wrangler: { configPath: './wrangler.toml' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import worker, { renderText, formatLocalTime, fireRoutine } from './worker.js';
|
||||
|
||||
const TEST_TOKEN = 'sk-ant-oat01-test-only-not-real';
|
||||
const TEST_URL = 'https://api.anthropic.com/v1/claude_code/routines/trig_TEST/fire';
|
||||
|
||||
/**
|
||||
* Captures `console.log` output for assertion. Restores on cleanup.
|
||||
* @returns {{ logs: string[], restore: () => void }}
|
||||
*/
|
||||
function captureLogs() {
|
||||
const logs = [];
|
||||
const original = console.log;
|
||||
console.log = (msg) => { logs.push(typeof msg === 'string' ? msg : JSON.stringify(msg)); };
|
||||
return { logs, restore: () => { console.log = original; } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a minimal Env for tests.
|
||||
* @param {Partial<import('./worker.js').Env>} overrides
|
||||
* @returns {import('./worker.js').Env}
|
||||
*/
|
||||
function buildEnv(overrides = {}) {
|
||||
return {
|
||||
ROUTINE_FIRE_URL: TEST_URL,
|
||||
ROUTINE_FIRE_TOKEN: TEST_TOKEN,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const fixedTime = Date.UTC(2026, 4, 9, 3, 19, 0); // 2026-05-09T03:19:00Z (cron 19 3 * * *)
|
||||
const baseController = { cron: '19 3 * * *', scheduledTime: fixedTime };
|
||||
|
||||
describe('renderText', () => {
|
||||
it('substitutes {LocalTime}, {Cron}, {ISO}', () => {
|
||||
const out = renderText('At {LocalTime} cron {Cron} iso {ISO}', {
|
||||
LocalTime: '2026-05-09 10:19 GMT+7',
|
||||
Cron: '19 3 * * *',
|
||||
ISO: '2026-05-09T03:19:00.000Z',
|
||||
});
|
||||
expect(out).toBe('At 2026-05-09 10:19 GMT+7 cron 19 3 * * * iso 2026-05-09T03:19:00.000Z');
|
||||
});
|
||||
|
||||
it('leaves unknown {Foo} tokens intact', () => {
|
||||
const out = renderText('hello {Foo}', { Bar: 'baz' });
|
||||
expect(out).toBe('hello {Foo}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatLocalTime', () => {
|
||||
it('defaults to UTC formatting', () => {
|
||||
const s = formatLocalTime(new Date(fixedTime), 'UTC');
|
||||
expect(s).toMatch(/^2026-05-09 03:19 /);
|
||||
});
|
||||
|
||||
it('respects IANA tz', () => {
|
||||
const s = formatLocalTime(new Date(fixedTime), 'Asia/Ho_Chi_Minh');
|
||||
expect(s).toMatch(/^2026-05-09 10:19 /);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fireRoutine', () => {
|
||||
/** @type {ReturnType<typeof captureLogs>} */
|
||||
let cap;
|
||||
/** @type {ReturnType<typeof vi.fn>} */
|
||||
let fetchSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
cap = captureLogs();
|
||||
fetchSpy = vi.fn();
|
||||
globalThis.fetch = fetchSpy;
|
||||
});
|
||||
|
||||
function tearDown() {
|
||||
cap.restore();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
|
||||
it('logs session_url on 2xx', async () => {
|
||||
fetchSpy.mockResolvedValue(new Response(JSON.stringify({
|
||||
type: 'ok',
|
||||
claude_code_session_id: 'sess_1',
|
||||
claude_code_session_url: 'https://claude.ai/s/sess_1',
|
||||
}), { status: 200 }));
|
||||
|
||||
await fireRoutine(buildEnv(), baseController);
|
||||
|
||||
const joined = cap.logs.join('\n');
|
||||
expect(joined).toContain('"level":"info"');
|
||||
expect(joined).toContain('https://claude.ai/s/sess_1');
|
||||
expect(joined).not.toContain(TEST_TOKEN);
|
||||
tearDown();
|
||||
});
|
||||
|
||||
it('logs level:error on 401 non-2xx', async () => {
|
||||
fetchSpy.mockResolvedValue(new Response('unauthorized', { status: 401 }));
|
||||
|
||||
await fireRoutine(buildEnv(), baseController);
|
||||
|
||||
const joined = cap.logs.join('\n');
|
||||
expect(joined).toContain('"level":"error"');
|
||||
expect(joined).toContain('"status":401');
|
||||
expect(joined).not.toContain(TEST_TOKEN);
|
||||
tearDown();
|
||||
});
|
||||
|
||||
it('logs error and does not throw when fetch rejects', async () => {
|
||||
fetchSpy.mockRejectedValue(new Error('network down'));
|
||||
|
||||
await expect(fireRoutine(buildEnv(), baseController)).resolves.toBeUndefined();
|
||||
const joined = cap.logs.join('\n');
|
||||
expect(joined).toContain('"level":"error"');
|
||||
expect(joined).toContain('network down');
|
||||
expect(joined).not.toContain(TEST_TOKEN);
|
||||
tearDown();
|
||||
});
|
||||
|
||||
it('sends required headers and JSON body', async () => {
|
||||
fetchSpy.mockResolvedValue(new Response(JSON.stringify({
|
||||
type: 'ok', claude_code_session_id: 's', claude_code_session_url: 'u',
|
||||
}), { status: 200 }));
|
||||
|
||||
await fireRoutine(buildEnv(), baseController);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe(TEST_URL);
|
||||
expect(init.method).toBe('POST');
|
||||
expect(init.headers['Authorization']).toBe(`Bearer ${TEST_TOKEN}`);
|
||||
expect(init.headers['anthropic-version']).toBe('2023-06-01');
|
||||
expect(init.headers['anthropic-beta']).toBe('experimental-cc-routine-2026-04-01');
|
||||
expect(init.headers['Content-Type']).toBe('application/json');
|
||||
|
||||
const parsed = JSON.parse(init.body);
|
||||
expect(parsed).toHaveProperty('text');
|
||||
expect(typeof parsed.text).toBe('string');
|
||||
tearDown();
|
||||
});
|
||||
|
||||
it('uses default template when TEXT_TEMPLATE unset', async () => {
|
||||
fetchSpy.mockResolvedValue(new Response(JSON.stringify({
|
||||
type: 'ok', claude_code_session_id: 's', claude_code_session_url: 'u',
|
||||
}), { status: 200 }));
|
||||
|
||||
await fireRoutine(buildEnv(), baseController);
|
||||
|
||||
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
||||
expect(body.text).toMatch(/^Scheduled trigger at /);
|
||||
tearDown();
|
||||
});
|
||||
|
||||
it('defaults TZ to UTC when env.TZ unset', async () => {
|
||||
fetchSpy.mockResolvedValue(new Response(JSON.stringify({
|
||||
type: 'ok', claude_code_session_id: 's', claude_code_session_url: 'u',
|
||||
}), { status: 200 }));
|
||||
|
||||
await fireRoutine(buildEnv(), baseController);
|
||||
|
||||
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
||||
expect(body.text).toContain('03:19'); // UTC hour from fixedTime
|
||||
tearDown();
|
||||
});
|
||||
|
||||
it('warn-logs unparseable 2xx body', async () => {
|
||||
fetchSpy.mockResolvedValue(new Response('not-json', { status: 200 }));
|
||||
|
||||
await fireRoutine(buildEnv(), baseController);
|
||||
|
||||
const joined = cap.logs.join('\n');
|
||||
expect(joined).toContain('"level":"warn"');
|
||||
expect(joined).toContain('unparseable');
|
||||
tearDown();
|
||||
});
|
||||
});
|
||||
|
||||
describe('default export (scheduled handler)', () => {
|
||||
it('exposes async scheduled function', () => {
|
||||
expect(typeof worker.scheduled).toBe('function');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user