test: 실제 Game API 명령 전수 근거를 고정한다
This commit is contained in:
@@ -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
|
||||
|
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user