feat: Ref 호환 접속 제한과 벌점 초기화 구현

실행 중인 프로필에서만 접속 벌점을 누적하고 제한 임계값과 대상 경로를 Ref 순서에 맞춘다. 자기 턴 명령 성공 시 순간 점수를 같은 flush에서 초기화하며 월간 누적 감쇠는 유지한다. 제한 중 메인 자동 갱신과 실시간 구독을 중지하고 수동 갱신 성공 시 복구한다.
This commit is contained in:
2026-08-15 18:49:41 +00:00
parent a99f8166eb
commit 48d94e5ff2
29 changed files with 982 additions and 232 deletions
@@ -0,0 +1,50 @@
import { createHmac } from 'node:crypto';
import fastify from 'fastify';
import { describe, expect, it, vi } from 'vitest';
import type { GatewayProfileRepository } from '../src/orchestrator/profileRepository.js';
import { registerProfileStatusInternalRoute } from '../src/lobby/profileStatusInternalRoute.js';
const secret = 'gateway-profile-status-test-secret';
const token = createHmac('sha256', secret).update('sammo:profile-status-source:v1').digest('hex');
describe('profile status internal route', () => {
it('requires a purpose-derived token and returns only the durable status', async () => {
const app = fastify();
const profiles = {
getProfile: vi.fn(async (profileName: string) => ({ profileName, status: 'PAUSED' })),
} as unknown as GatewayProfileRepository;
registerProfileStatusInternalRoute(app, { profiles, secret });
const unauthorized = await app.inject({
method: 'GET',
url: '/internal/profile-status/che%3Adefault',
headers: { 'x-sammo-internal-token': secret },
});
expect(unauthorized.statusCode).toBe(401);
const response = await app.inject({
method: 'GET',
url: '/internal/profile-status/che%3Adefault',
headers: { 'x-sammo-internal-token': token },
});
expect(response.statusCode).toBe(200);
expect(response.headers['cache-control']).toBe('no-store');
expect(response.json()).toEqual({ profileName: 'che:default', status: 'PAUSED' });
expect(Object.keys(response.json()).sort()).toEqual(['profileName', 'status']);
});
it('returns 404 for an unknown profile', async () => {
const app = fastify();
const profiles = { getProfile: vi.fn(async () => null) } as unknown as GatewayProfileRepository;
registerProfileStatusInternalRoute(app, { profiles, secret });
const response = await app.inject({
method: 'GET',
url: '/internal/profile-status/missing',
headers: { 'x-sammo-internal-token': token },
});
expect(response.statusCode).toBe(404);
expect(response.json()).toEqual({ ok: false, error: 'not_found' });
});
});