mirror of
https://github.com/tiennm99/claude-code-routine-trigger.git
synced 2026-10-18 16:13:38 +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