统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
This commit is contained in:
@@ -120,25 +120,15 @@
|
||||
"visual_novel_work_profile": []
|
||||
},
|
||||
"profileIds": {
|
||||
"puzzle": [
|
||||
"profile-001",
|
||||
"profile-002",
|
||||
"profile-003"
|
||||
],
|
||||
"customWorld": [
|
||||
"profile-081"
|
||||
],
|
||||
"puzzle": ["profile-001", "profile-002", "profile-003"],
|
||||
"customWorld": ["profile-081"],
|
||||
"match3d": [],
|
||||
"squareHole": [],
|
||||
"bigFish": [],
|
||||
"visualNovel": []
|
||||
},
|
||||
"workIds": {
|
||||
"puzzle": [
|
||||
"work-001",
|
||||
"work-002",
|
||||
"work-003"
|
||||
],
|
||||
"puzzle": ["work-001", "work-002", "work-003"],
|
||||
"customWorld": [],
|
||||
"match3d": [],
|
||||
"squareHole": [],
|
||||
|
||||
@@ -120,25 +120,15 @@
|
||||
"visual_novel_work_profile": []
|
||||
},
|
||||
"profileIds": {
|
||||
"puzzle": [
|
||||
"profile-001",
|
||||
"profile-002",
|
||||
"profile-003"
|
||||
],
|
||||
"customWorld": [
|
||||
"profile-081"
|
||||
],
|
||||
"puzzle": ["profile-001", "profile-002", "profile-003"],
|
||||
"customWorld": ["profile-081"],
|
||||
"match3d": [],
|
||||
"squareHole": [],
|
||||
"bigFish": [],
|
||||
"visualNovel": []
|
||||
},
|
||||
"workIds": {
|
||||
"puzzle": [
|
||||
"work-001",
|
||||
"work-002",
|
||||
"work-003"
|
||||
],
|
||||
"puzzle": ["work-001", "work-002", "work-003"],
|
||||
"customWorld": [],
|
||||
"match3d": [],
|
||||
"squareHole": [],
|
||||
|
||||
@@ -30,10 +30,18 @@ const TABLE_OUTPUT_ORDER = [
|
||||
'visual_novel_work_profile',
|
||||
];
|
||||
|
||||
const WORK_TYPES = ['puzzle', 'customWorld', 'match3d', 'squareHole', 'bigFish', 'visualNovel'];
|
||||
const WORK_TYPES = [
|
||||
'puzzle',
|
||||
'customWorld',
|
||||
'match3d',
|
||||
'squareHole',
|
||||
'bigFish',
|
||||
'visualNovel',
|
||||
];
|
||||
const SHORT_TEXT_LIMIT = 120;
|
||||
const LONG_TEXT_LIMIT = 500;
|
||||
const SENSITIVE_PATTERN = /(token|secret|password|passwd|phone|wallet|credential|authorization|auth[_-]?key|api[_-]?key)/giu;
|
||||
const SENSITIVE_PATTERN =
|
||||
/(token|secret|password|passwd|phone|wallet|credential|authorization|auth[_-]?key|api[_-]?key)/giu;
|
||||
|
||||
class StableMapper {
|
||||
constructor(prefix) {
|
||||
@@ -98,11 +106,13 @@ function redactSensitiveText(value) {
|
||||
|
||||
function sanitizeCoverImageSrc(value) {
|
||||
const unwrapped = unwrapSpacetimeOption(value);
|
||||
if (unwrapped === undefined || unwrapped === null || unwrapped === '') return unwrapped;
|
||||
if (unwrapped === undefined || unwrapped === null || unwrapped === '')
|
||||
return unwrapped;
|
||||
const text = String(unwrapped);
|
||||
if (text.startsWith('data:image/')) return '[redacted-data-image]';
|
||||
let withoutQuery = text.split('?')[0].split('#')[0];
|
||||
if (withoutQuery.length > 180) withoutQuery = `${withoutQuery.slice(0, 180)}…`;
|
||||
if (withoutQuery.length > 180)
|
||||
withoutQuery = `${withoutQuery.slice(0, 180)}…`;
|
||||
return withoutQuery;
|
||||
}
|
||||
|
||||
@@ -113,9 +123,15 @@ function sanitizeLargeJson(value) {
|
||||
return truncateText(redactSensitiveText(unwrapped), LONG_TEXT_LIMIT);
|
||||
}
|
||||
try {
|
||||
return truncateText(redactSensitiveText(JSON.stringify(unwrapped)), LONG_TEXT_LIMIT);
|
||||
return truncateText(
|
||||
redactSensitiveText(JSON.stringify(unwrapped)),
|
||||
LONG_TEXT_LIMIT,
|
||||
);
|
||||
} catch {
|
||||
return truncateText(redactSensitiveText(String(unwrapped)), LONG_TEXT_LIMIT);
|
||||
return truncateText(
|
||||
redactSensitiveText(String(unwrapped)),
|
||||
LONG_TEXT_LIMIT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,27 +144,42 @@ function firstDefined(row, keys) {
|
||||
|
||||
function sanitizeShortField(row, sanitized, key) {
|
||||
if (row[key] !== undefined) {
|
||||
sanitized[key] = truncateText(unwrapSpacetimeOption(row[key]), SHORT_TEXT_LIMIT);
|
||||
sanitized[key] = truncateText(
|
||||
unwrapSpacetimeOption(row[key]),
|
||||
SHORT_TEXT_LIMIT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeWorkRow(row, ctx) {
|
||||
const sanitized = {};
|
||||
const profileId = unwrapSpacetimeOption(firstDefined(row, ['profile_id', 'profileId']));
|
||||
const workId = unwrapSpacetimeOption(firstDefined(row, ['work_id', 'workId']));
|
||||
const profileId = unwrapSpacetimeOption(
|
||||
firstDefined(row, ['profile_id', 'profileId']),
|
||||
);
|
||||
const workId = unwrapSpacetimeOption(
|
||||
firstDefined(row, ['work_id', 'workId']),
|
||||
);
|
||||
|
||||
if (profileId !== undefined) sanitized.profile_id = ctx.profile.map(profileId);
|
||||
if (profileId !== undefined)
|
||||
sanitized.profile_id = ctx.profile.map(profileId);
|
||||
if (workId !== undefined) sanitized.work_id = ctx.work.map(workId);
|
||||
if (row.owner_user_id !== undefined) {
|
||||
sanitized.owner_user_id = ctx.user.map(unwrapSpacetimeOption(row.owner_user_id));
|
||||
sanitized.owner_user_id = ctx.user.map(
|
||||
unwrapSpacetimeOption(row.owner_user_id),
|
||||
);
|
||||
}
|
||||
if (row.user_id !== undefined) sanitized.user_id = ctx.user.map(unwrapSpacetimeOption(row.user_id));
|
||||
if (row.user_id !== undefined)
|
||||
sanitized.user_id = ctx.user.map(unwrapSpacetimeOption(row.user_id));
|
||||
|
||||
if (row.author_display_name !== undefined) {
|
||||
sanitized.author_display_name = ctx.author.map(unwrapSpacetimeOption(row.author_display_name));
|
||||
sanitized.author_display_name = ctx.author.map(
|
||||
unwrapSpacetimeOption(row.author_display_name),
|
||||
);
|
||||
}
|
||||
if (row.public_work_code !== undefined) {
|
||||
sanitized.public_work_code = ctx.publicWorkCode.map(unwrapSpacetimeOption(row.public_work_code));
|
||||
sanitized.public_work_code = ctx.publicWorkCode.map(
|
||||
unwrapSpacetimeOption(row.public_work_code),
|
||||
);
|
||||
}
|
||||
if (row.author_public_user_code !== undefined) {
|
||||
sanitized.author_public_user_code = ctx.authorCode.map(
|
||||
@@ -156,9 +187,12 @@ function sanitizeWorkRow(row, ctx) {
|
||||
);
|
||||
}
|
||||
if (row.cover_asset_id !== undefined) {
|
||||
sanitized.cover_asset_id = ctx.coverAsset.map(unwrapSpacetimeOption(row.cover_asset_id));
|
||||
sanitized.cover_asset_id = ctx.coverAsset.map(
|
||||
unwrapSpacetimeOption(row.cover_asset_id),
|
||||
);
|
||||
}
|
||||
if (row.cover_image_src !== undefined) sanitized.cover_image_src = sanitizeCoverImageSrc(row.cover_image_src);
|
||||
if (row.cover_image_src !== undefined)
|
||||
sanitized.cover_image_src = sanitizeCoverImageSrc(row.cover_image_src);
|
||||
|
||||
for (const key of [
|
||||
'title',
|
||||
@@ -174,7 +208,12 @@ function sanitizeWorkRow(row, ctx) {
|
||||
sanitizeShortField(row, sanitized, key);
|
||||
}
|
||||
|
||||
for (const key of ['levels_json', 'profile_payload_json', 'anchor_pack_json', 'theme_tags_json']) {
|
||||
for (const key of [
|
||||
'levels_json',
|
||||
'profile_payload_json',
|
||||
'anchor_pack_json',
|
||||
'theme_tags_json',
|
||||
]) {
|
||||
if (row[key] !== undefined) sanitized[key] = sanitizeLargeJson(row[key]);
|
||||
}
|
||||
|
||||
@@ -196,7 +235,8 @@ function sanitizeWorkRow(row, ctx) {
|
||||
'tags',
|
||||
];
|
||||
for (const key of passthroughKeys) {
|
||||
if (row[key] !== undefined) sanitized[key] = unwrapSpacetimeOption(row[key]);
|
||||
if (row[key] !== undefined)
|
||||
sanitized[key] = unwrapSpacetimeOption(row[key]);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
@@ -211,8 +251,14 @@ function normalizeWork(tableName, row) {
|
||||
ownerUserId: row.owner_user_id,
|
||||
publicWorkCode: row.public_work_code,
|
||||
title: row.title ?? row.work_title ?? row.level_name ?? row.world_name,
|
||||
subtitle: row.subtitle ?? row.summary_text ?? row.summary ?? row.work_description ?? row.description,
|
||||
publicationStatus: row.publicationStatus ?? row.publication_status ?? row.status,
|
||||
subtitle:
|
||||
row.subtitle ??
|
||||
row.summary_text ??
|
||||
row.summary ??
|
||||
row.work_description ??
|
||||
row.description,
|
||||
publicationStatus:
|
||||
row.publicationStatus ?? row.publication_status ?? row.status,
|
||||
playCount: row.playCount ?? row.play_count ?? 0,
|
||||
likeCount: row.likeCount ?? row.like_count ?? 0,
|
||||
remixCount: row.remixCount ?? row.remix_count ?? 0,
|
||||
@@ -299,7 +345,11 @@ function createSampleOutput(output, maxRowsPerTable = 3) {
|
||||
]),
|
||||
);
|
||||
const normalizedWorks = output.normalizedWorks
|
||||
.filter((work) => allowedWorkIds.has(work.workId) || allowedProfileIds.has(work.profileId))
|
||||
.filter(
|
||||
(work) =>
|
||||
allowedWorkIds.has(work.workId) ||
|
||||
allowedProfileIds.has(work.profileId),
|
||||
)
|
||||
.slice(0, maxRowsPerTable * 6);
|
||||
|
||||
return {
|
||||
@@ -318,7 +368,8 @@ function parseArgs(argv) {
|
||||
const arg = argv[index];
|
||||
if (arg === '--input' || arg === '--output' || arg === '--sample-output') {
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
|
||||
if (!value || value.startsWith('--'))
|
||||
throw new Error(`${arg} requires a value`);
|
||||
args[arg.slice(2)] = value;
|
||||
index += 1;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
@@ -345,12 +396,18 @@ export async function runCli(argv = process.argv.slice(2)) {
|
||||
|
||||
const raw = await readFile(args.input, 'utf8');
|
||||
const migration = JSON.parse(raw);
|
||||
const output = extractWorksListData(migration, { source: basename(args.input) });
|
||||
const output = extractWorksListData(migration, {
|
||||
source: basename(args.input),
|
||||
});
|
||||
await writeFile(args.output, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
|
||||
|
||||
if (args['sample-output']) {
|
||||
const sample = createSampleOutput(output);
|
||||
await writeFile(args['sample-output'], `${JSON.stringify(sample, null, 2)}\n`, 'utf8');
|
||||
await writeFile(
|
||||
args['sample-output'],
|
||||
`${JSON.stringify(sample, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
@@ -361,7 +418,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
||||
}
|
||||
}
|
||||
|
||||
const isDirectRun = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
||||
const isDirectRun =
|
||||
process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
||||
if (isDirectRun) {
|
||||
runCli().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
|
||||
@@ -10,7 +10,9 @@ import { describe, expect, it } from 'vitest';
|
||||
import { extractWorksListData } from './extract-works-list-data.mjs';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const scriptPath = fileURLToPath(new URL('./extract-works-list-data.mjs', import.meta.url));
|
||||
const scriptPath = fileURLToPath(
|
||||
new URL('./extract-works-list-data.mjs', import.meta.url),
|
||||
);
|
||||
|
||||
const fixtureMigration = {
|
||||
schema_version: 7,
|
||||
@@ -32,8 +34,13 @@ const fixtureMigration = {
|
||||
play_count: 42,
|
||||
like_count: 7,
|
||||
cover_asset_id: { some: 'asset-secret-cover' },
|
||||
cover_image_src: { some: 'https://cdn.example.test/cover.png?token=***&sig=abc' },
|
||||
levels_json: JSON.stringify({ secret: 'level-token-value', data: 'x'.repeat(2000) }),
|
||||
cover_image_src: {
|
||||
some: 'https://cdn.example.test/cover.png?token=***&sig=abc',
|
||||
},
|
||||
levels_json: JSON.stringify({
|
||||
secret: 'level-token-value',
|
||||
data: 'x'.repeat(2000),
|
||||
}),
|
||||
theme_tags_json: JSON.stringify(['化学家', '实验室']),
|
||||
remix_count: 2,
|
||||
updated_at: '2026-05-01T00:00:00Z',
|
||||
@@ -88,7 +95,12 @@ const fixtureMigration = {
|
||||
},
|
||||
{
|
||||
name: 'refresh_session',
|
||||
rows: [{ token: 'refresh-token-secret', source_session_id: 'session-secret-789' }],
|
||||
rows: [
|
||||
{
|
||||
token: 'refresh-token-secret',
|
||||
source_session_id: 'session-secret-789',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'profile_wallet_ledger',
|
||||
@@ -108,7 +120,9 @@ async function withTempDir(fn) {
|
||||
|
||||
describe('extractWorksListData', () => {
|
||||
it('只保留作品 profile 白名单表,禁用的行为/敏感表不会出现在输出 JSON 字符串中', () => {
|
||||
const output = extractWorksListData(fixtureMigration, { source: 'fixture.local.json' });
|
||||
const output = extractWorksListData(fixtureMigration, {
|
||||
source: 'fixture.local.json',
|
||||
});
|
||||
const serialized = JSON.stringify(output);
|
||||
|
||||
expect(Object.keys(output.tables).sort()).toEqual([
|
||||
@@ -125,7 +139,9 @@ describe('extractWorksListData', () => {
|
||||
});
|
||||
|
||||
it('不会输出 owner/user/session/auth/token/phone/wallet 等敏感原值,owner 稳定映射', () => {
|
||||
const output = extractWorksListData(fixtureMigration, { source: 'fixture.local.json' });
|
||||
const output = extractWorksListData(fixtureMigration, {
|
||||
source: 'fixture.local.json',
|
||||
});
|
||||
const serialized = JSON.stringify(output);
|
||||
|
||||
for (const secret of [
|
||||
@@ -143,12 +159,16 @@ describe('extractWorksListData', () => {
|
||||
|
||||
expect(output.tables.puzzle_work_profile[0].owner_user_id).toBe('user-001');
|
||||
expect(output.tables.puzzle_work_profile[1].owner_user_id).toBe('user-001');
|
||||
expect(output.tables.puzzle_work_profile[0].author_display_name).toBe('author-001');
|
||||
expect(output.tables.puzzle_work_profile[0].author_display_name).toBe(
|
||||
'author-001',
|
||||
);
|
||||
expect(serialized).not.toContain('level-token-value');
|
||||
});
|
||||
|
||||
it('puzzle 数据生成 profileIds/workIds 和 normalizedWorks,并保留列表展示字段', () => {
|
||||
const output = extractWorksListData(fixtureMigration, { source: 'fixture.local.json' });
|
||||
const output = extractWorksListData(fixtureMigration, {
|
||||
source: 'fixture.local.json',
|
||||
});
|
||||
|
||||
expect(output.source).toBe('fixture.local.json');
|
||||
expect(output.generatedAt).toEqual(expect.any(String));
|
||||
@@ -168,8 +188,12 @@ describe('extractWorksListData', () => {
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(output.tables.puzzle_work_profile[0].cover_image_src).toBe('https://cdn.example.test/cover.png');
|
||||
expect(output.tables.puzzle_work_profile[0].theme_tags_json).toBe('["化学家","实验室"]');
|
||||
expect(output.tables.puzzle_work_profile[0].cover_image_src).toBe(
|
||||
'https://cdn.example.test/cover.png',
|
||||
);
|
||||
expect(output.tables.puzzle_work_profile[0].theme_tags_json).toBe(
|
||||
'["化学家","实验室"]',
|
||||
);
|
||||
});
|
||||
|
||||
it('data image、URL token 和绝对输入路径不会泄露到输出', async () => {
|
||||
@@ -186,8 +210,13 @@ describe('extractWorksListData', () => {
|
||||
{
|
||||
profile_id: 'profile-real',
|
||||
work_id: 'work-real',
|
||||
cover_image_src: { some: 'data:image/png;base64,SECRET_IMAGE_BYTES' },
|
||||
levels_json: JSON.stringify({ token: 'SECRET_TOKEN_VALUE', title: 'safe' }),
|
||||
cover_image_src: {
|
||||
some: 'data:image/png;base64,SECRET_IMAGE_BYTES',
|
||||
},
|
||||
levels_json: JSON.stringify({
|
||||
token: 'SECRET_TOKEN_VALUE',
|
||||
title: 'safe',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -196,7 +225,13 @@ describe('extractWorksListData', () => {
|
||||
'utf8',
|
||||
);
|
||||
|
||||
await execFileAsync(process.execPath, [scriptPath, '--input', input, '--output', output]);
|
||||
await execFileAsync(process.execPath, [
|
||||
scriptPath,
|
||||
'--input',
|
||||
input,
|
||||
'--output',
|
||||
output,
|
||||
]);
|
||||
const extracted = JSON.parse(await readFile(output, 'utf8'));
|
||||
const serialized = JSON.stringify(extracted);
|
||||
|
||||
@@ -204,7 +239,9 @@ describe('extractWorksListData', () => {
|
||||
expect(serialized).not.toContain(dir);
|
||||
expect(serialized).not.toContain('SECRET_IMAGE_BYTES');
|
||||
expect(serialized).not.toContain('SECRET_TOKEN_VALUE');
|
||||
expect(extracted.tables.puzzle_work_profile[0].cover_image_src).toBe('[redacted-data-image]');
|
||||
expect(extracted.tables.puzzle_work_profile[0].cover_image_src).toBe(
|
||||
'[redacted-data-image]',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,11 +260,21 @@ describe('extractWorksListData', () => {
|
||||
}));
|
||||
await writeFile(
|
||||
input,
|
||||
JSON.stringify({ tables: [{ name: 'puzzle_work_profile', rows: manyRows }] }),
|
||||
JSON.stringify({
|
||||
tables: [{ name: 'puzzle_work_profile', rows: manyRows }],
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
await execFileAsync(process.execPath, [scriptPath, '--input', input, '--output', output, '--sample-output', sampleOutput]);
|
||||
await execFileAsync(process.execPath, [
|
||||
scriptPath,
|
||||
'--input',
|
||||
input,
|
||||
'--output',
|
||||
output,
|
||||
'--sample-output',
|
||||
sampleOutput,
|
||||
]);
|
||||
const sample = JSON.parse(await readFile(sampleOutput, 'utf8'));
|
||||
const serialized = JSON.stringify(sample);
|
||||
|
||||
@@ -239,7 +286,9 @@ describe('extractWorksListData', () => {
|
||||
});
|
||||
|
||||
it('CLI 参数缺失时退出非 0 并输出清晰错误', async () => {
|
||||
await expect(execFileAsync(process.execPath, [scriptPath, '--input', 'missing.json'])).rejects.toMatchObject({
|
||||
await expect(
|
||||
execFileAsync(process.execPath, [scriptPath, '--input', 'missing.json']),
|
||||
).rejects.toMatchObject({
|
||||
code: 1,
|
||||
stderr: expect.stringContaining('--output'),
|
||||
});
|
||||
|
||||
@@ -7,7 +7,10 @@ import { Rate, Trend } from 'k6/metrics';
|
||||
// k6 resolves open() paths relative to this script file, not the shell cwd.
|
||||
const DEFAULT_WORKS_DATA = 'data/works-list.local.json';
|
||||
const WORKS_DATA = __ENV.WORKS_DATA || DEFAULT_WORKS_DATA;
|
||||
const BASE_URL = (__ENV.BASE_URL || 'http://127.0.0.1:8787').replace(/\/+$/u, '');
|
||||
const BASE_URL = (__ENV.BASE_URL || 'http://127.0.0.1:8787').replace(
|
||||
/\/+$/u,
|
||||
'',
|
||||
);
|
||||
const AUTH_TOKEN = __ENV.AUTH_TOKEN || '';
|
||||
const SCENARIO = __ENV.SCENARIO || 'smoke';
|
||||
const REQUEST_TIMEOUT = __ENV.REQUEST_TIMEOUT || '30s';
|
||||
@@ -20,8 +23,12 @@ const worksDetailShapeErrorRate = new Rate('works_detail_shape_error_rate');
|
||||
const worksListDuration = new Trend('works_list_duration');
|
||||
const worksDetailDuration = new Trend('works_detail_duration');
|
||||
|
||||
const data = new SharedArray('works-list-data', () => [JSON.parse(open(WORKS_DATA))])[0];
|
||||
const normalizedWorks = Array.isArray(data.normalizedWorks) ? data.normalizedWorks : [];
|
||||
const data = new SharedArray('works-list-data', () => [
|
||||
JSON.parse(open(WORKS_DATA)),
|
||||
])[0];
|
||||
const normalizedWorks = Array.isArray(data.normalizedWorks)
|
||||
? data.normalizedWorks
|
||||
: [];
|
||||
|
||||
const scenarioOptions = {
|
||||
smoke: {
|
||||
@@ -61,9 +68,18 @@ const scenarioOptions = {
|
||||
maxVUs: Number(__ENV.MAX_VUS || 200),
|
||||
timeUnit: '1s',
|
||||
stages: [
|
||||
{ target: Number(__ENV.PEAK_RPS || 25), duration: __ENV.RAMP_UP || '30s' },
|
||||
{ target: Number(__ENV.PEAK_RPS || 25), duration: __ENV.HOLD || '2m' },
|
||||
{ target: Number(__ENV.END_RPS || 5), duration: __ENV.RAMP_DOWN || '30s' },
|
||||
{
|
||||
target: Number(__ENV.PEAK_RPS || 25),
|
||||
duration: __ENV.RAMP_UP || '30s',
|
||||
},
|
||||
{
|
||||
target: Number(__ENV.PEAK_RPS || 25),
|
||||
duration: __ENV.HOLD || '2m',
|
||||
},
|
||||
{
|
||||
target: Number(__ENV.END_RPS || 5),
|
||||
duration: __ENV.RAMP_DOWN || '30s',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -153,10 +169,19 @@ function hasListItemShape(payload, keys) {
|
||||
const item = collection[0];
|
||||
const hasId = Boolean(
|
||||
item &&
|
||||
(item.profileId || item.profile_id || item.workId || item.work_id || item.publicWorkCode),
|
||||
(item.profileId ||
|
||||
item.profile_id ||
|
||||
item.workId ||
|
||||
item.work_id ||
|
||||
item.publicWorkCode),
|
||||
);
|
||||
const hasTitle = Boolean(
|
||||
item && (item.title || item.workTitle || item.work_title || item.levelName || item.worldName),
|
||||
item &&
|
||||
(item.title ||
|
||||
item.workTitle ||
|
||||
item.work_title ||
|
||||
item.levelName ||
|
||||
item.worldName),
|
||||
);
|
||||
return hasId && hasTitle;
|
||||
}
|
||||
@@ -167,7 +192,9 @@ function randomItem(items) {
|
||||
}
|
||||
|
||||
function listEndpoints() {
|
||||
return AUTH_TOKEN ? PUBLIC_ENDPOINTS.concat(AUTH_ENDPOINTS) : PUBLIC_ENDPOINTS;
|
||||
return AUTH_TOKEN
|
||||
? PUBLIC_ENDPOINTS.concat(AUTH_ENDPOINTS)
|
||||
: PUBLIC_ENDPOINTS;
|
||||
}
|
||||
|
||||
function detailEndpointFor(work) {
|
||||
@@ -191,15 +218,22 @@ function detailEndpointFor(work) {
|
||||
|
||||
function performListRequest(endpoint) {
|
||||
const url = buildUrl(endpoint.path);
|
||||
const response = http.request(endpoint.method, url, null, requestParams(endpoint.name));
|
||||
const response = http.request(
|
||||
endpoint.method,
|
||||
url,
|
||||
null,
|
||||
requestParams(endpoint.name),
|
||||
);
|
||||
worksListDuration.add(response.timings.duration, { endpoint: endpoint.name });
|
||||
const json = parseJson(response);
|
||||
const payload = unwrapPayload(json);
|
||||
const ok = check(response, {
|
||||
[`${endpoint.name} status is 200`]: (res) => res.status === 200,
|
||||
[`${endpoint.name} returns json object`]: () => Boolean(payload),
|
||||
[`${endpoint.name} has collection`]: () => hasCollection(payload, endpoint.expectCollectionKeys),
|
||||
[`${endpoint.name} list item shape`]: () => hasListItemShape(payload, endpoint.expectCollectionKeys),
|
||||
[`${endpoint.name} has collection`]: () =>
|
||||
hasCollection(payload, endpoint.expectCollectionKeys),
|
||||
[`${endpoint.name} list item shape`]: () =>
|
||||
hasListItemShape(payload, endpoint.expectCollectionKeys),
|
||||
});
|
||||
worksListShapeErrorRate.add(!ok, { endpoint: endpoint.name });
|
||||
}
|
||||
@@ -208,8 +242,13 @@ function performDetailRequest() {
|
||||
const endpoint = detailEndpointFor(randomItem(normalizedWorks));
|
||||
if (!endpoint) return;
|
||||
|
||||
const response = http.get(buildUrl(endpoint.path), requestParams(endpoint.name));
|
||||
worksDetailDuration.add(response.timings.duration, { endpoint: endpoint.name });
|
||||
const response = http.get(
|
||||
buildUrl(endpoint.path),
|
||||
requestParams(endpoint.name),
|
||||
);
|
||||
worksDetailDuration.add(response.timings.duration, {
|
||||
endpoint: endpoint.name,
|
||||
});
|
||||
const json = parseJson(response);
|
||||
const payload = unwrapPayload(json);
|
||||
const ok = check(response, {
|
||||
@@ -224,10 +263,16 @@ export default function () {
|
||||
for (const endpoint of listEndpoints()) {
|
||||
performListRequest(endpoint);
|
||||
}
|
||||
if (normalizedWorks.length && DETAIL_RATIO > 0 && Math.random() < DETAIL_RATIO) {
|
||||
if (
|
||||
normalizedWorks.length &&
|
||||
DETAIL_RATIO > 0 &&
|
||||
Math.random() < DETAIL_RATIO
|
||||
) {
|
||||
performDetailRequest();
|
||||
}
|
||||
|
||||
const jitter = SLEEP_MIN_SECONDS + Math.random() * Math.max(0, SLEEP_MAX_SECONDS - SLEEP_MIN_SECONDS);
|
||||
const jitter =
|
||||
SLEEP_MIN_SECONDS +
|
||||
Math.random() * Math.max(0, SLEEP_MAX_SECONDS - SLEEP_MIN_SECONDS);
|
||||
sleep(jitter);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user