Skip to content

Commit 81d090e

Browse files
hakatashiclaude
andcommitted
fix: ponpe/atcoderの起動時ネットワークアクセスを解消し、deploy/mahjong/atcoder/ponpe/google-calendarをスモークテスト対象に追加
- ponpe: default() 内でawaitしていたemoji.json/common_word_listのダウンロードと team.info()呼び出しをlib/utils.tsのLoaderでラップし、実際にゲームが 開始されるまで(isValidEmoji/getTheme呼び出し時まで)遅延させた。 lib/download.tsはファイルが既に存在すればネットワークアクセスをスキップする 実装のため、本番動作への影響はほぼ無い。 - atcoder: default() 起動直後の無条件updateContests()呼び出しを削除。 30分おきのsetIntervalによる定期更新は変更せず維持している。 lib/startup.test.ts: - vi.useFakeTimers({toFake: ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval']}) を追加し、setInterval/node-schedule(内部的にsetTimeoutを 使用)由来の定期実行ジョブがテスト実行中に発火しないようにした。Dateまで フェイク化するとwinstonのタイムスタンプ生成やnode-scheduleのJob生成が クラッシュするため、toFakeでタイマー関数のみに対象を限定している。 - 上記の対応により、atcoder(30分おきのsetIntervalが発火してもエラーに ならなくなった)とgoogle-calendar(毎分発火するnode-scheduleのジョブが テスト中に発火しなくなった)をスモークテスト対象に追加。 - deploy/mahjongは実際に有効化して検証したところ問題なくpassしたため 除外を解除した(旧PR(#1231)のts-jestのESM解決制約が理由だったが、 vitestではこの制約は存在しない。map-guessr/nmpzと同種の誤除外)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BuziKVfh8Zx1WC9bRy2MYu
1 parent 387fef1 commit 81d090e

3 files changed

Lines changed: 33 additions & 27 deletions

File tree

atcoder/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,6 @@ export default async ({eventClient, webClient: slack}: SlackInterface) => {
656656
updateContests();
657657
});
658658
}, 30 * 60 * 1000);
659-
updateContests();
660659

661660
let time = Date.now();
662661
setInterval(() => {

lib/startup.test.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import Fastify from 'fastify';
22
import SlackMock from './slackMock';
33
import {allBots} from './bots';
4+
import {EventEmitter} from 'events';
45

56
vi.setConfig({testTimeout: 60 * 1000});
67

8+
// atcoder/google-calendar等、複数のプラグインが default() 内で
9+
// setInterval や node-schedule の scheduleJob(内部的にsetTimeoutを使用)で
10+
// 定期実行ジョブを登録する。setTimeout/setIntervalのみをフェイク化することで
11+
// テスト実行中にこれらのコールバックが実際に発火することはなくなり、非同期の
12+
// unhandled rejectionによる無関係なテストの汚染を防げる。
13+
// Dateまでフェイク化するとwinstonのタイムスタンプ生成(logform)や
14+
// node-scheduleのJob生成が `toISOString is not a function` で
15+
// クラッシュするため、toFakeで対象を限定する。
16+
vi.useFakeTimers({toFake: ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval']});
17+
afterAll(() => {
18+
vi.useRealTimers();
19+
});
20+
721
// 各プラグインのテストで慣例的に行われているのと同様、実クライアント/実DBに
822
// 接続する共有シングルトンをモック化する。これらは plugin.default(slack) /
923
// plugin.server(slack) に渡す SlackMock とは独立しており、モックしても
@@ -16,7 +30,6 @@ vi.setConfig({testTimeout: 60 * 1000});
1630
// しまい副作用(実ログイン)を防げないため、実モジュールを読み込まない
1731
// factoryを明示的に渡す。
1832
vi.mock('./discord', () => {
19-
const {EventEmitter} = require('events');
2033
const emitter = new EventEmitter();
2134
const chainable: any = new Proxy(() => chainable, {
2235
get: () => chainable,
@@ -78,24 +91,6 @@ vi.mock('axios');
7891
vi.mock('dotenv/config', () => ({}));
7992

8093
const EXCLUDED_BOTS = new Set([
81-
// mahjong は deploy 経由で @octokit/webhooks の Webhooks インスタンスを
82-
// 生成し、起動直後に実GitHub APIへの接続を試みる副作用がある。
83-
'deploy',
84-
'mahjong',
85-
// default() が起動直後に実際のネットワークスクレイピング
86-
// (updateContests)を無条件に開始する。この操作自体が本テストの対象外
87-
// (実外部アクセスを伴う)である上、この環境では Node 内部の
88-
// async_hooks 周りで SIGABRT を引き起こすことが確認されたため除外する。
89-
'atcoder',
90-
// GoogleCalendar.ts が node-schedule で毎分('* * * * *')発火するジョブを
91-
// 登録する。テスト実行中に実際に発火し実Google APIへアクセスしようとして
92-
// 失敗し、そのタイミング次第で無関係な後続テストの失敗として現れるため
93-
// 除外する。
94-
'google-calendar',
95-
// 起動時に絵文字データを実際にダウンロードしてJSON.parseする。共有axios
96-
// モックのデフォルト応答(空文字列)はJSONとして不正なため失敗する。
97-
// 実ネットワークアクセスを伴う初期化のため、本テストの対象外とする。
98-
'ponpe',
9994
// default() が起動直後に updateAll() で複数のCTFプラットフォームを
10095
// 未awaitのままスクレイピングし始める。共有axiosモックの空文字列応答では
10196
// cheerioのHTMLパースが失敗し、そのタイミング次第で無関係な後続テストの

ponpe/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import fs from 'fs';
77
import {getMemberName, getEmoji} from '../lib/slackUtils';
88
import path from 'path';
99
import {download} from '../lib/download';
10+
import {Loader} from '../lib/utils';
1011

1112
function getTimeLink(time:number){
1213
return moment(time).utcOffset('+0900').format('HH:mm:ss');
@@ -59,12 +60,20 @@ export default async ({eventClient, webClient: slack}: SlackInterface) => {
5960
const states : State[] = [];
6061

6162
const emojipath = path.join(__dirname, 'data', 'emoji.json');
62-
await download(emojipath, 'https://raw.githubusercontent.com/iamcal/emoji-data/master/emoji.json');
63-
const default_emoji_list = JSON.parse(await loadFile(emojipath))
64-
.map((x:{short_names:string[]})=>{return x.short_names;}).flat();
63+
const emojiListLoader = new Loader<string[]>(async () => {
64+
await download(emojipath, 'https://raw.githubusercontent.com/iamcal/emoji-data/master/emoji.json');
65+
return JSON.parse(await loadFile(emojipath))
66+
.map((x:{short_names:string[]})=>{return x.short_names;}).flat();
67+
});
68+
69+
const tsgTeamLoader = new Loader<any>(async () => {
70+
const {team}: any = await slack.team.info();
71+
return team;
72+
});
6573

66-
const {team: tsgTeam}: any = await slack.team.info();
6774
async function isValidEmoji(name:string){
75+
const tsgTeam = await tsgTeamLoader.load();
76+
const default_emoji_list = await emojiListLoader.load();
6877
return default_emoji_list.includes(name) ||
6978
await getEmoji(name,tsgTeam.id) !== undefined;
7079
}
@@ -73,10 +82,13 @@ export default async ({eventClient, webClient: slack}: SlackInterface) => {
7382
// | awk '{ print $2 "," $3 }' | grep -E -v "^([^,]{1,5}|[^,]{10,100})," | head -n 50000 | tail -n 20000 > common_word_list
7483

7584
const themepath = path.join(__dirname, 'data', 'common_word_list');
76-
await download(themepath, 'https://drive.google.com/uc?id=1MO5fDrDHLtrVvNcnfUlddo56w29OWFMc');
77-
const themes = (await loadFile(themepath)).split('\n');
85+
const themesLoader = new Loader<string[]>(async () => {
86+
await download(themepath, 'https://drive.google.com/uc?id=1MO5fDrDHLtrVvNcnfUlddo56w29OWFMc');
87+
return (await loadFile(themepath)).split('\n');
88+
});
7889

79-
function getTheme(){
90+
async function getTheme(){
91+
const themes = await themesLoader.load();
8092
const theme = sample(themes).split(',');
8193
return {
8294
word: theme[1],

0 commit comments

Comments
 (0)