900 NPC와 300 synthetic 사용자를 5분 턴으로 처리하는 결정론적 engine profile을 추가하고, 전용 PostgreSQL schema와 Redis DB에 300개 인증 session을 준비하는 격리 load fixture를 제공한다.
73 lines
3.8 KiB
TypeScript
73 lines
3.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { chmod, readFile, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { assertRuntimeMetadataFinalized, canonicalJson, expandWeightedOperations, loadTokens, validateLoadConfig } from '../src/config.js';
|
|
|
|
const samplePath = new URL('../config/300-users-900-npcs-5m.json', import.meta.url);
|
|
|
|
void test('the 300 viewer, 900 NPC, five-minute sample validates', async () => {
|
|
const config = validateLoadConfig(JSON.parse(await readFile(samplePath, 'utf8')));
|
|
assert.equal(config.capacity.authenticatedViewers, 300);
|
|
assert.equal(config.capacity.npcGenerals, 900);
|
|
assert.equal(config.capacity.humanGenerals, 300);
|
|
assert.equal(config.capacity.turnIntervalMs, 300_000);
|
|
assert.equal(config.isolation.redisDatabase, 15);
|
|
assert.equal(config.isolation.profileName, 'load-tests:capacity-300-900-5m');
|
|
assert.deepEqual(new Set(config.phases.map((phase) => phase.kind)), new Set(['idle', 'own', 'global', 'mixed']));
|
|
});
|
|
|
|
void test('validation rejects public, non-allowlisted, and mutating targets', async () => {
|
|
const raw = JSON.parse(await readFile(samplePath, 'utf8')) as Record<string, any>;
|
|
raw.target.publicProfile = true;
|
|
raw.target.baseUrl = 'https://public.example.invalid';
|
|
raw.phases[1].operations[0].type = 'mutation';
|
|
assert.throws(() => validateLoadConfig(raw), /publicProfile must be false/u);
|
|
assert.throws(() => validateLoadConfig(raw), /hostname is not explicitly allowlisted/u);
|
|
assert.throws(() => validateLoadConfig(raw), /read-only query/u);
|
|
});
|
|
|
|
void test('a measurement run rejects sample metadata placeholders', async () => {
|
|
const config = validateLoadConfig(JSON.parse(await readFile(samplePath, 'utf8')));
|
|
assert.throws(() => assertRuntimeMetadataFinalized(config), /fixtureSha256, imageDigest, postgresVersion, redisVersion/u);
|
|
});
|
|
|
|
void test('canonical JSON and weighted scheduling do not depend on object insertion order', () => {
|
|
assert.equal(canonicalJson({ b: 2, a: 1 }), canonicalJson({ a: 1, b: 2 }));
|
|
const expanded = expandWeightedOperations([
|
|
{ name: 'a', procedure: 'a.read', type: 'query', weight: 2 },
|
|
{ name: 'b', procedure: 'b.read', type: 'query', weight: 1 },
|
|
]);
|
|
assert.deepEqual(expanded.map((operation) => operation.name), ['a', 'a', 'b']);
|
|
});
|
|
|
|
void test('token loading requires an ignored 0600 file and returns no identity metadata', async () => {
|
|
const workspaceRoot = path.resolve(import.meta.dirname, '../../..');
|
|
const tokenPath = path.join(workspaceRoot, 'tools/load-tests/secrets/unit-test-tokens.json');
|
|
await writeFile(tokenPath, JSON.stringify({ tokens: ['ga_test_token_00000001'] }), { mode: 0o600 });
|
|
await chmod(tokenPath, 0o600);
|
|
try {
|
|
assert.deepEqual(await loadTokens(tokenPath, workspaceRoot, 1), ['ga_test_token_00000001']);
|
|
await chmod(tokenPath, 0o644);
|
|
await assert.rejects(loadTokens(tokenPath, workspaceRoot, 1), /0600/u);
|
|
} finally {
|
|
await unlink(tokenPath).catch(() => undefined);
|
|
}
|
|
});
|
|
|
|
void test('token loading rejects a symlink even when its link path is ignored', async () => {
|
|
const workspaceRoot = path.resolve(import.meta.dirname, '../../..');
|
|
const tokenPath = path.join(workspaceRoot, 'tools/load-tests/secrets/unit-test-token-target.json');
|
|
const linkPath = path.join(workspaceRoot, 'tools/load-tests/secrets/unit-test-token-link.json');
|
|
await writeFile(tokenPath, JSON.stringify({ tokens: ['ga_test_token_00000001'] }), { mode: 0o600 });
|
|
await chmod(tokenPath, 0o600);
|
|
await symlink(tokenPath, linkPath);
|
|
try {
|
|
await assert.rejects(loadTokens(linkPath, workspaceRoot, 1), /symbolic link/u);
|
|
} finally {
|
|
await unlink(linkPath).catch(() => undefined);
|
|
await unlink(tokenPath).catch(() => undefined);
|
|
}
|
|
});
|