test: 실제 Game API 명령 전수 근거를 고정한다

This commit is contained in:
2026-08-24 16:30:48 +00:00
parent cca2b3d925
commit a9e2431115
17 changed files with 1138 additions and 48 deletions
@@ -0,0 +1,178 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const defaultWorkspaceRoot = path.resolve(scriptDirectory, '..');
const defaultRegistryPath = path.join(scriptDirectory, 'conditional-integration-file-registry.tsv');
export const supportedRequirements = new Set([
'reference_command',
'reference_full_lifecycle',
'reference_instant_diplomacy',
'reference_monthly',
'reference_snapshot',
'saved_trace_pair',
]);
const databaseMarkerPattern = /process\.env\.[A-Z0-9_]+_DATABASE_URL/u;
const redisMarkerPattern = /process\.env\.REDIS_URL/u;
const integrationFilePattern = /^test\/[A-Za-z0-9._/-]+\.integration\.test\.ts$/u;
export const parseConditionalIntegrationFileRegistry = (source) => {
const entries = [];
const seen = new Set();
for (const [index, rawLine] of source.split(/\r?\n/u).entries()) {
const line = rawLine.trim();
if (line === '' || line.startsWith('#')) continue;
const fields = rawLine.split('\t');
if (fields.length !== 2) {
throw new Error(`line ${index + 1} must contain exactly one tab-separated file and requirement`);
}
const [file, requirement] = fields.map((field) => field.trim());
if (!integrationFilePattern.test(file)) {
throw new Error(`line ${index + 1} has an invalid integration test path: ${file}`);
}
if (!supportedRequirements.has(requirement)) {
throw new Error(`line ${index + 1} has an unsupported environment requirement: ${requirement}`);
}
if (seen.has(file)) {
throw new Error(`line ${index + 1} duplicates integration test path: ${file}`);
}
seen.add(file);
entries.push({ file, requirement });
}
return entries;
};
const listIntegrationSources = async (testDirectory) => {
const sources = [];
const visit = async (directory) => {
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) {
await visit(absolute);
} else if (entry.isFile() && entry.name.endsWith('.integration.test.ts')) {
sources.push({
file: path.relative(path.dirname(testDirectory), absolute).split(path.sep).join('/'),
source: await fs.readFile(absolute, 'utf8'),
});
}
}
};
await visit(testDirectory);
return sources.sort((left, right) => left.file.localeCompare(right.file));
};
const isNonDatabaseConditionalReference = (source) => {
if (!source.includes('describe.skipIf')) return false;
if (databaseMarkerPattern.test(source) || redisMarkerPattern.test(source)) return false;
return (
source.includes('TURN_DIFFERENTIAL_REFERENCE') ||
(source.includes('TURN_REFERENCE_TRACE') && source.includes('TURN_CORE_TRACE'))
);
};
const validateRequirementEvidence = ({ file, requirement, source }) => {
const errors = [];
const requireToken = (token) => {
if (!source.includes(token)) errors.push(`${file}: ${requirement} requires source token ${token}`);
};
if (requirement.startsWith('reference_')) {
requireToken('TURN_DIFFERENTIAL_REFERENCE');
}
switch (requirement) {
case 'reference_command':
requireToken('runReferenceTurnCommandTrace');
break;
case 'reference_full_lifecycle':
requireToken('turn_full_lifecycle_trace.php');
break;
case 'reference_instant_diplomacy':
requireToken('instant_diplomacy_response_trace.php');
break;
case 'reference_monthly':
requireToken('monthly_event_trace.php');
break;
case 'reference_snapshot':
requireToken('readReferenceDatabaseSnapshot');
break;
case 'saved_trace_pair':
requireToken('TURN_REFERENCE_TRACE');
requireToken('TURN_CORE_TRACE');
break;
}
return errors;
};
export const validateConditionalIntegrationFileRegistry = async ({
workspaceRoot = defaultWorkspaceRoot,
registryPath = defaultRegistryPath,
} = {}) => {
const packageRoot = path.join(workspaceRoot, 'tools/integration-tests');
const testDirectory = path.join(packageRoot, 'test');
const registrySource = await fs.readFile(registryPath, 'utf8');
const entries = parseConditionalIntegrationFileRegistry(registrySource);
const sources = await listIntegrationSources(testDirectory);
const sourceByFile = new Map(sources.map((entry) => [entry.file, entry.source]));
const discovered = sources
.filter(({ source }) => isNonDatabaseConditionalReference(source))
.map(({ file }) => file);
const registered = entries.map(({ file }) => file);
const errors = [];
const missing = discovered.filter((file) => !registered.includes(file));
const stale = registered.filter((file) => !discovered.includes(file));
if (missing.length > 0) errors.push(`unregistered non-database conditional suite(s): ${missing.join(', ')}`);
if (stale.length > 0) errors.push(`stale non-database conditional suite(s): ${stale.join(', ')}`);
for (const entry of entries) {
const source = sourceByFile.get(entry.file);
if (source === undefined) continue;
if (databaseMarkerPattern.test(source) || redisMarkerPattern.test(source)) {
errors.push(`${entry.file}: file registry overlaps a database/Redis marker suite`);
}
errors.push(...validateRequirementEvidence({ ...entry, source }));
}
if (errors.length > 0) {
throw new Error(
`invalid conditional integration file registry:\n${errors.map((error) => ` ${error}`).join('\n')}`
);
}
return entries;
};
export const selectEnabledConditionalIntegrationFiles = (entries, environment = process.env) => {
const hasReferenceTrace = Boolean(environment.TURN_REFERENCE_TRACE);
const hasCoreTrace = Boolean(environment.TURN_CORE_TRACE);
if (hasReferenceTrace !== hasCoreTrace) {
throw new Error('TURN_REFERENCE_TRACE and TURN_CORE_TRACE must be provided together');
}
return {
referenceFiles:
environment.TURN_DIFFERENTIAL_REFERENCE === '1'
? entries.filter(({ requirement }) => requirement.startsWith('reference_')).map(({ file }) => file)
: [],
savedTraceFiles:
hasReferenceTrace && hasCoreTrace
? entries.filter(({ requirement }) => requirement === 'saved_trace_pair').map(({ file }) => file)
: [],
};
};
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
try {
const entries = await validateConditionalIntegrationFileRegistry();
selectEnabledConditionalIntegrationFiles(entries);
process.stdout.write(`conditional integration file registry is valid (${entries.length} files)\n`);
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
}
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
parseConditionalIntegrationFileRegistry,
selectEnabledConditionalIntegrationFiles,
validateConditionalIntegrationFileRegistry,
} from './check-conditional-integration-files.mjs';
test('keeps every non-database conditional Ref suite in one requirement group', async () => {
const entries = await validateConditionalIntegrationFileRegistry();
const counts = Object.fromEntries(
[...new Set(entries.map(({ requirement }) => requirement))]
.sort()
.map((requirement) => [requirement, entries.filter((entry) => entry.requirement === requirement).length])
);
assert.deepEqual(counts, {
reference_command: 5,
reference_full_lifecycle: 1,
reference_instant_diplomacy: 1,
reference_monthly: 1,
reference_snapshot: 1,
saved_trace_pair: 1,
});
});
test('rejects duplicate files and unsupported requirements', () => {
assert.throws(
() =>
parseConditionalIntegrationFileRegistry(
'test/example.integration.test.ts\treference_command\n' +
'test/example.integration.test.ts\treference_command\n'
),
/duplicates integration test path/u
);
assert.throws(
() => parseConditionalIntegrationFileRegistry('test/example.integration.test.ts\tunknown\n'),
/unsupported environment requirement/u
);
});
test('enables Ref suites only with the Ref runtime and saved traces only as a pair', async () => {
const entries = await validateConditionalIntegrationFileRegistry();
assert.deepEqual(selectEnabledConditionalIntegrationFiles(entries, {}), {
referenceFiles: [],
savedTraceFiles: [],
});
assert.equal(
selectEnabledConditionalIntegrationFiles(entries, { TURN_DIFFERENTIAL_REFERENCE: '1' }).referenceFiles.length,
9
);
assert.deepEqual(
selectEnabledConditionalIntegrationFiles(entries, {
TURN_REFERENCE_TRACE: '/tmp/ref.json',
TURN_CORE_TRACE: '/tmp/core.json',
}).savedTraceFiles,
['test/turnTraceFiles.integration.test.ts']
);
assert.throws(
() => selectEnabledConditionalIntegrationFiles(entries, { TURN_REFERENCE_TRACE: '/tmp/ref.json' }),
/must be provided together/u
);
});
@@ -0,0 +1,11 @@
# Test file Environment requirement
test/instantDiplomacyCoreReference.integration.test.ts reference_instant_diplomacy
test/instantDiplomacyReference.integration.test.ts reference_command
test/monthlyDisasterCoreReference.integration.test.ts reference_monthly
test/turnCommandCoreReference.integration.test.ts reference_command
test/turnCommandFullLifecycle.integration.test.ts reference_full_lifecycle
test/turnCommandGeneralMatrix.integration.test.ts reference_command
test/turnCommandNationMatrix.integration.test.ts reference_command
test/turnCommandReference.integration.test.ts reference_command
test/turnSnapshotReference.integration.test.ts reference_snapshot
test/turnTraceFiles.integration.test.ts saved_trace_pair
1 # Test file Environment requirement
2 test/instantDiplomacyCoreReference.integration.test.ts reference_instant_diplomacy
3 test/instantDiplomacyReference.integration.test.ts reference_command
4 test/monthlyDisasterCoreReference.integration.test.ts reference_monthly
5 test/turnCommandCoreReference.integration.test.ts reference_command
6 test/turnCommandFullLifecycle.integration.test.ts reference_full_lifecycle
7 test/turnCommandGeneralMatrix.integration.test.ts reference_command
8 test/turnCommandNationMatrix.integration.test.ts reference_command
9 test/turnCommandReference.integration.test.ts reference_command
10 test/turnSnapshotReference.integration.test.ts reference_snapshot
11 test/turnTraceFiles.integration.test.ts saved_trace_pair
+116 -2
View File
@@ -11,6 +11,7 @@ usage() {
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
workspace_root=$(CDPATH= cd -- "$script_dir/.." && pwd)
registry_file="$script_dir/conditional-integration-registry.tsv"
file_registry="$script_dir/conditional-integration-file-registry.tsv"
env_file=${1:-"$workspace_root/.env.ci"}
if [ ! -f "$env_file" ]; then
@@ -21,6 +22,10 @@ if [ ! -f "$registry_file" ]; then
echo "missing integration marker registry: $registry_file" >&2
exit 66
fi
if [ ! -f "$file_registry" ]; then
echo "missing integration file registry: $file_registry" >&2
exit 66
fi
env_file=$(CDPATH= cd -- "$(dirname -- "$env_file")" && pwd)/$(basename -- "$env_file")
# .env.ci is generated by the sam_rebuild development Compose helper and is a
@@ -369,6 +374,102 @@ markers_for_mode() {
paste -sd '|' -
}
registered_reference_files() {
awk -F '\t' '$2 ~ /^reference_/ { print $1 }' "$file_registry"
}
files_for_requirement() {
requirement=$1
awk -F '\t' -v requirement="$requirement" '$2 == requirement { print $1 }' "$file_registry"
}
resolve_stack_relative_path() {
candidate_path=$1
case "$candidate_path" in
/*) printf '%s\n' "$candidate_path" ;;
*) printf '%s/%s\n' "$reference_stack" "$candidate_path" ;;
esac
}
require_reference_file() {
required_path=$1
requirement=$2
if [ ! -f "$required_path" ]; then
echo "$requirement requires a reference file that is not available: $required_path" >&2
exit 69
fi
}
validate_reference_file_requirements() {
if [ -n "${REF_COMPARE_SOURCE_ROOT:-}" ]; then
reference_source_root=$(
cd "$workspace_root/tools/integration-tests"
CDPATH= cd -- "$REF_COMPARE_SOURCE_ROOT"
pwd
)
else
reference_source_root="$reference_workspace_root/ref/sam"
fi
requirements=$(awk -F '\t' '$2 ~ /^reference_/ { print $2 }' "$file_registry" | sort -u)
for requirement in $requirements; do
case "$requirement" in
reference_command)
command_runner=${TURN_DIFFERENTIAL_RUNNER_SCRIPT:-"$reference_source_root/hwe/compare/turn_command_trace.php"}
require_reference_file "$(resolve_stack_relative_path "$command_runner")" "$requirement"
;;
reference_full_lifecycle)
require_reference_file \
"$reference_source_root/hwe/compare/turn_full_lifecycle_trace.php" \
"$requirement"
;;
reference_instant_diplomacy)
require_reference_file \
"$reference_source_root/hwe/compare/instant_diplomacy_response_trace.php" \
"$requirement"
;;
reference_monthly)
monthly_runner=${MONTHLY_DIFFERENTIAL_RUNNER_SCRIPT:-"$reference_workspace_root/ref/sam/hwe/compare/monthly_event_trace.php"}
require_reference_file "$(resolve_stack_relative_path "$monthly_runner")" "$requirement"
;;
reference_snapshot)
require_reference_file \
"$reference_workspace_root/ref/sam/hwe/compare/turn_state_snapshot.php" \
"$requirement"
;;
*)
echo "unsupported reference integration requirement: $requirement" >&2
exit 65
;;
esac
done
}
run_saved_trace_pair_tests() {
reference_trace=${TURN_REFERENCE_TRACE:-}
core_trace=${TURN_CORE_TRACE:-}
if [ -z "$reference_trace" ] && [ -z "$core_trace" ]; then
return
fi
if [ -z "$reference_trace" ] || [ -z "$core_trace" ]; then
echo "TURN_REFERENCE_TRACE and TURN_CORE_TRACE must be provided together" >&2
exit 64
fi
if ! (cd "$workspace_root/tools/integration-tests" && [ -f "$reference_trace" ] && [ -f "$core_trace" ]); then
echo "TURN_REFERENCE_TRACE and TURN_CORE_TRACE must both name readable trace files" >&2
exit 66
fi
trace_files=$(files_for_requirement saved_trace_pair)
if [ -z "$trace_files" ]; then
echo "saved_trace_pair has no registered integration test" >&2
exit 65
fi
# Registry paths cannot contain whitespace.
# shellcheck disable=SC2086
run_vitest tools/integration-tests "saved_trace_pair" $trace_files
}
record_vitest_result() {
label=$1
result_file=$2
@@ -469,6 +570,7 @@ run_redis_only_tests() {
export PATH
cd "$workspace_root"
node "$script_dir/check-conditional-integration-files.mjs"
validate_marker_registry
pnpm install --frozen-lockfile
@@ -680,6 +782,18 @@ if [ "${TURN_DIFFERENTIAL_REFERENCE:-}" = "1" ]; then
exit 69
fi
export TURN_DIFFERENTIAL_WORKSPACE_ROOT=$reference_workspace_root
export TURN_DIFFERENTIAL_STACK_DIR=$reference_stack
validate_reference_file_requirements
reference_files=$(registered_reference_files)
if [ -z "$reference_files" ]; then
echo "no non-database reference integration files are registered" >&2
exit 65
fi
# Registry paths cannot contain whitespace.
# shellcheck disable=SC2086
run_vitest tools/integration-tests "reference_runtime" $reference_files
create_owned_schema "$npc_possession_differential_schema"
npc_possession_differential_database_url=$(build_database_url "$npc_possession_differential_schema")
(
@@ -689,8 +803,6 @@ if [ "${TURN_DIFFERENTIAL_REFERENCE:-}" = "1" ]; then
pnpm --filter @sammo-ts/infra prisma:db:push:game
)
export NPC_POSSESSION_DIFFERENTIAL_DATABASE_URL=$npc_possession_differential_database_url
export TURN_DIFFERENTIAL_WORKSPACE_ROOT=$reference_workspace_root
export TURN_DIFFERENTIAL_STACK_DIR=$reference_stack
run_marked_tests tools/integration-tests \
"$(markers_for_mode reference_npc_possession)" \
"npc_possession_reference"
@@ -740,6 +852,8 @@ if [ "${TURN_DIFFERENTIAL_REFERENCE:-}" = "1" ]; then
export DATABASE_URL=$database_url
fi
run_saved_trace_pair_tests
all_database_markers=$(cut -f1 "$validated_registry_file" | paste -sd '|' -)
run_redis_only_tests "$all_database_markers"