chore(agent-roadmap): sync change-aware routing updates
This commit is contained in:
parent
d925691dc5
commit
7fb7033024
6 changed files with 310 additions and 90 deletions
|
|
@ -193,6 +193,7 @@ tools/check_proto_sync.sh
|
|||
|
||||
- `run_full_test.sh --dry-run` 출력이 `functional-full`, `functional-full-dart-web`, `functional-full-dart-web-wss`를 `recommended` 상태로 포함하면 full functional periodic이 발동된다. 이 dry-run은 내부에서 `run_test_by_change.sh --route full --dry-run` classifier를 호출한다.
|
||||
- full functional 호출 시 `run_matrix.sh --all` 또는 `--cross` 실행으로 언어 PASS 매트릭스(Dart.io, Dart.web, Dart.web(WSS), Go, Kotlin, Python, TypeScript)와 크로스테스트 상세 표를 반드시 확인한다.
|
||||
- 언어 anchor 후보는 full 5x5 대신 `run_matrix.sh --cross --anchor <lang> --direction server|client|both`로 실행한다. self 방향은 해당 언어 동일언어 테스트로 채우고, Dart.web/WSS는 anchor x5에 포함하지 않는다.
|
||||
- Dart.web과 Dart.web(WSS)는 브라우저 client runtime이므로 서버 행에는 넣지 않는다.
|
||||
- Dart.web/WSS client coverage는 full matrix의 `Dart.web` 및 `Dart.web(WSS)` 열에서 각 server-capable 언어 행이 모두 `PASS`여야 한다.
|
||||
- `recommended`: 즉시 실행 후보. runner가 실행해야 함.
|
||||
|
|
@ -227,4 +228,4 @@ tools/check_proto_sync.sh
|
|||
- non-dry-run 실행 시 선택 이유, 실행 명령, 결과 record 경로를 출력에 남긴다.
|
||||
- 실행하지 않은 skipped/nightly-pending 항목은 PASS로 보고하지 않는다.
|
||||
- `nightly-pending` 후보는 `## nightly-pending 후보 (미실행, PASS 아님)` 섹션에 후보 command와 함께 기록한다.
|
||||
- anchor 실행(`functional-server-x5`, `functional-client-x5`, `functional-both-x5`)은 anchor-runner-options 범위로 남기고 `anchor-skipped`로 기록한다.
|
||||
- anchor 실행(`functional-server-x5`, `functional-client-x5`, `functional-both-x5`)은 지원 언어 target이면 `run_matrix.sh --cross --anchor <lang> --direction <server|client|both>`를 실행하고 결과 record를 남긴다. `transport/queue` 같은 비언어 target은 실행하지 않고 skipped로 기록한다.
|
||||
|
|
|
|||
|
|
@ -3,23 +3,110 @@ set -u
|
|||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: run_matrix.sh [--all|--unit|--cross|--proto]
|
||||
Usage: run_matrix.sh [--all|--unit|--cross|--proto] [--anchor LANG --direction server|client|both]
|
||||
|
||||
Runs Proto Socket proto schema sync, unit/same-language tests, and/or
|
||||
cross-language communication tests, then prints Markdown PASS/FAIL tables.
|
||||
Each run also writes a local result record under agent-test/runs/.
|
||||
Defaults to --all.
|
||||
|
||||
Anchor options:
|
||||
--anchor LANG Focus cross tests around dart, go, kotlin, python, or typescript.
|
||||
--direction DIR server, client, or both. Anchor mode runs the anchor same-language
|
||||
test for the self cell and only the selected x5 cross directions.
|
||||
USAGE
|
||||
}
|
||||
|
||||
normalize_anchor_language() {
|
||||
case "$1" in
|
||||
dart|Dart|dart.io|Dart.io) echo "Dart" ;;
|
||||
go|Go) echo "Go" ;;
|
||||
kotlin|Kotlin) echo "Kotlin" ;;
|
||||
python|Python) echo "Python" ;;
|
||||
typescript|TypeScript|ts|TS) echo "TypeScript" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
language_slug() {
|
||||
case "$1" in
|
||||
Dart) echo "dart" ;;
|
||||
Go) echo "go" ;;
|
||||
Kotlin) echo "kotlin" ;;
|
||||
Python) echo "python" ;;
|
||||
TypeScript) echo "typescript" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
mode="all"
|
||||
case "${1:-}" in
|
||||
""|--all) mode="all" ;;
|
||||
--unit) mode="unit" ;;
|
||||
--cross) mode="cross" ;;
|
||||
--proto) mode="proto" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) usage >&2; exit 2 ;;
|
||||
mode_set=0
|
||||
anchor_arg=""
|
||||
anchor_language=""
|
||||
anchor_slug=""
|
||||
direction="both"
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--all) mode="all"; mode_set=1 ;;
|
||||
--unit) mode="unit"; mode_set=1 ;;
|
||||
--cross) mode="cross"; mode_set=1 ;;
|
||||
--proto) mode="proto"; mode_set=1 ;;
|
||||
--anchor=*) anchor_arg="${1#--anchor=}" ;;
|
||||
--anchor)
|
||||
shift
|
||||
if [ "$#" -eq 0 ]; then
|
||||
printf 'Error: --anchor requires a language\n' >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
anchor_arg="$1"
|
||||
;;
|
||||
--direction=*) direction="${1#--direction=}" ;;
|
||||
--direction)
|
||||
shift
|
||||
if [ "$#" -eq 0 ]; then
|
||||
printf 'Error: --direction requires server, client, or both\n' >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
direction="$1"
|
||||
;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) usage >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ -n "$anchor_arg" ]; then
|
||||
if ! anchor_language="$(normalize_anchor_language "$anchor_arg")"; then
|
||||
printf 'Error: unknown anchor language "%s"\n' "$anchor_arg" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
anchor_slug="$(language_slug "$anchor_language")"
|
||||
if [ "$mode" = "all" ] && [ "$mode_set" -eq 0 ]; then
|
||||
mode="cross"
|
||||
elif [ "$mode" != "cross" ]; then
|
||||
printf 'Error: --anchor is only supported with --cross\n' >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$anchor_language" ] && [ "$direction" != "both" ]; then
|
||||
printf 'Error: --direction requires --anchor\n' >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "$direction" in
|
||||
server|client|both) ;;
|
||||
*)
|
||||
printf 'Error: unknown direction "%s". Use: server, client, both\n' "$direction" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
|
@ -42,12 +129,16 @@ remote_web_ref="${PROTO_SOCKET_DART_WEB_REF:-$(git -C "$repo_root" rev-parse --a
|
|||
remote_web_execution="local"
|
||||
remote_web_prepare_summary=""
|
||||
|
||||
case "$mode" in
|
||||
all) record_profile="proto-socket-full-matrix" ;;
|
||||
unit) record_profile="proto-socket-unit-matrix" ;;
|
||||
cross) record_profile="proto-socket-cross-matrix" ;;
|
||||
proto) record_profile="proto-socket-proto-sync" ;;
|
||||
esac
|
||||
if [ -n "$anchor_language" ]; then
|
||||
record_profile="proto-socket-anchor-${anchor_slug}-${direction}-matrix"
|
||||
else
|
||||
case "$mode" in
|
||||
all) record_profile="proto-socket-full-matrix" ;;
|
||||
unit) record_profile="proto-socket-unit-matrix" ;;
|
||||
cross) record_profile="proto-socket-cross-matrix" ;;
|
||||
proto) record_profile="proto-socket-proto-sync" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
languages=("Dart" "Go" "Kotlin" "Python" "TypeScript")
|
||||
|
||||
|
|
@ -305,24 +396,41 @@ $cmd
|
|||
" >"$log_file" 2>&1
|
||||
}
|
||||
|
||||
run_unit_test_for_language() {
|
||||
local lang="$1"
|
||||
local cmd dir log_file status
|
||||
cmd="${unit_cmds[$lang]}"
|
||||
dir="${unit_dirs[$lang]}"
|
||||
log_file="$log_dir/unit_${lang}.log"
|
||||
run_logged "unit $lang: $cmd" "$dir" "$cmd" "$log_file"
|
||||
status=$?
|
||||
unit_status[$lang]="$status"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
unit_result[$lang]="PASS"
|
||||
else
|
||||
unit_result[$lang]="FAIL"
|
||||
fi
|
||||
unit_summary[$lang]="$(tail -n 1 "$log_file" | tr '\n' ' ')"
|
||||
}
|
||||
|
||||
run_unit_tests() {
|
||||
local lang cmd dir log_file status
|
||||
local lang
|
||||
for lang in "${languages[@]}"; do
|
||||
cmd="${unit_cmds[$lang]}"
|
||||
dir="${unit_dirs[$lang]}"
|
||||
log_file="$log_dir/unit_${lang}.log"
|
||||
run_logged "unit $lang: $cmd" "$dir" "$cmd" "$log_file"
|
||||
status=$?
|
||||
unit_status[$lang]="$status"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
unit_result[$lang]="PASS"
|
||||
else
|
||||
unit_result[$lang]="FAIL"
|
||||
fi
|
||||
unit_summary[$lang]="$(tail -n 1 "$log_file" | tr '\n' ' ')"
|
||||
run_unit_test_for_language "$lang"
|
||||
done
|
||||
}
|
||||
|
||||
should_run_cross_case() {
|
||||
local server="$1"
|
||||
local client="$2"
|
||||
[ -z "$anchor_language" ] && return 0
|
||||
case "$direction" in
|
||||
server) [ "$server" = "$anchor_language" ] ;;
|
||||
client) [ "$client" = "$anchor_language" ] ;;
|
||||
both) [ "$server" = "$anchor_language" ] || [ "$client" = "$anchor_language" ] ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_proto_sync() {
|
||||
local log_file status
|
||||
log_file="$log_dir/proto_sync.log"
|
||||
|
|
@ -341,6 +449,7 @@ run_cross_tests() {
|
|||
local item server client dir cmd key log_file status pass_count fail_count
|
||||
for item in "${cross_cases[@]}"; do
|
||||
IFS='|' read -r server client dir cmd <<<"$item"
|
||||
should_run_cross_case "$server" "$client" || continue
|
||||
key="$server|$client"
|
||||
log_file="$log_dir/cross_${server}_${client}.log"
|
||||
cross_cmds[$key]="(cd ${dir#$repo_root/} && $cmd)"
|
||||
|
|
@ -467,10 +576,14 @@ print_proto_table() {
|
|||
|
||||
print_unit_table() {
|
||||
local lang
|
||||
local unit_languages=("${languages[@]}")
|
||||
if [ -n "$anchor_language" ]; then
|
||||
unit_languages=("$anchor_language")
|
||||
fi
|
||||
printf '\n**동일언어**\n'
|
||||
printf '| 언어 | 명령 | 결과 |\n'
|
||||
printf '|---|---|---|\n'
|
||||
for lang in "${languages[@]}"; do
|
||||
for lang in "${unit_languages[@]}"; do
|
||||
printf '| %s | `%s` | %s |\n' "$lang" "${unit_cmds[$lang]}" "${unit_result[$lang]:--}"
|
||||
done
|
||||
}
|
||||
|
|
@ -513,7 +626,9 @@ language_matrix_cell() {
|
|||
key="$server|$client"
|
||||
|
||||
if [ "$server" = "$client" ]; then
|
||||
if [ "$mode" = "all" ] || [ "$mode" = "unit" ]; then
|
||||
if [ -n "${unit_result[$server]:-}" ]; then
|
||||
echo "${unit_result[$server]:--}"
|
||||
elif [ "$mode" = "all" ] || [ "$mode" = "unit" ]; then
|
||||
echo "${unit_result[$server]:--}"
|
||||
else
|
||||
echo "-"
|
||||
|
|
@ -526,8 +641,28 @@ language_matrix_cell() {
|
|||
|
||||
print_language_pass_matrix() {
|
||||
local server client
|
||||
local server_capable=("Dart.io" "Go" "Kotlin" "Python" "TypeScript")
|
||||
local servers=("Dart.io" "Go" "Kotlin" "Python" "TypeScript")
|
||||
local clients=("Dart.io" "Dart.web" "Dart.web(WSS)" "Go" "Kotlin" "Python" "TypeScript")
|
||||
local anchor_label
|
||||
|
||||
if [ -n "$anchor_language" ]; then
|
||||
anchor_label="$(display_language_label "$anchor_language")"
|
||||
case "$direction" in
|
||||
server)
|
||||
servers=("$anchor_label")
|
||||
clients=("${server_capable[@]}")
|
||||
;;
|
||||
client)
|
||||
servers=("${server_capable[@]}")
|
||||
clients=("$anchor_label")
|
||||
;;
|
||||
both)
|
||||
servers=("${server_capable[@]}")
|
||||
clients=("${server_capable[@]}")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
printf '\n**언어 PASS 매트릭스**\n'
|
||||
printf '| 서버 \\ 클라이언트'
|
||||
|
|
@ -556,19 +691,22 @@ print_cross_detail_table() {
|
|||
printf '|---|---:|---:|---:|---:|\n'
|
||||
for item in "${cross_cases[@]}"; do
|
||||
IFS='|' read -r server client dir cmd <<<"$item"
|
||||
should_run_cross_case "$server" "$client" || continue
|
||||
key="$server|$client"
|
||||
printf '| %s -> %s | %s | %s | %s | %s |\n' \
|
||||
"$(display_language_label "$server")" "$(display_language_label "$client")" \
|
||||
"${cross_result[$key]:--}" "${cross_pass_lines[$key]:--}" "$expected_cross_pass_lines" "${cross_fail_lines[$key]:--}"
|
||||
done
|
||||
for item in "${web_cases[@]}"; do
|
||||
IFS='|' read -r server dir cmd <<<"$item"
|
||||
key="$server"
|
||||
printf '| %s -> Dart.web | %s | %s | %s | %s |\n' \
|
||||
"$server" "${web_ws_result[$key]:--}" "${web_ws_pass_lines[$key]:--}" "$expected_web_transport_pass_lines" "${web_fail_lines[$key]:--}"
|
||||
printf '| %s -> Dart.web(WSS) | %s | %s | %s | %s |\n' \
|
||||
"$server" "${web_wss_result[$key]:--}" "${web_wss_pass_lines[$key]:--}" "$expected_web_transport_pass_lines" "${web_fail_lines[$key]:--}"
|
||||
done
|
||||
if [ -z "$anchor_language" ]; then
|
||||
for item in "${web_cases[@]}"; do
|
||||
IFS='|' read -r server dir cmd <<<"$item"
|
||||
key="$server"
|
||||
printf '| %s -> Dart.web | %s | %s | %s | %s |\n' \
|
||||
"$server" "${web_ws_result[$key]:--}" "${web_ws_pass_lines[$key]:--}" "$expected_web_transport_pass_lines" "${web_fail_lines[$key]:--}"
|
||||
printf '| %s -> Dart.web(WSS) | %s | %s | %s | %s |\n' \
|
||||
"$server" "${web_wss_result[$key]:--}" "${web_wss_pass_lines[$key]:--}" "$expected_web_transport_pass_lines" "${web_fail_lines[$key]:--}"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
print_failures() {
|
||||
|
|
@ -597,6 +735,7 @@ print_failures() {
|
|||
|
||||
for item in "${cross_cases[@]}"; do
|
||||
IFS='|' read -r server client dir cmd <<<"$item"
|
||||
should_run_cross_case "$server" "$client" || continue
|
||||
key="$server|$client"
|
||||
if [ "${cross_result[$key]:-}" = "FAIL" ]; then
|
||||
failed=1
|
||||
|
|
@ -608,23 +747,25 @@ print_failures() {
|
|||
fi
|
||||
done
|
||||
|
||||
for item in "${web_cases[@]}"; do
|
||||
IFS='|' read -r server dir cmd <<<"$item"
|
||||
key="$server"
|
||||
if [ "${web_result[$key]:-}" = "BLOCKED" ]; then
|
||||
failed=1
|
||||
printf '\n**차단: %s -> Dart.web 크로스테스트**\n' "$server"
|
||||
printf '재현 명령: `%s`\n' "${web_cmds[$key]}"
|
||||
printf '차단 사유: %s\n\n' "${web_block_reason[$key]:-unknown}"
|
||||
elif [ "${web_result[$key]:-}" = "FAIL" ]; then
|
||||
failed=1
|
||||
log_file="$log_dir/web_${server}.log"
|
||||
printf '\n**실패: %s -> Dart.web 크로스테스트**\n' "$server"
|
||||
printf '재현 명령: `%s`\n\n' "${web_cmds[$key]}"
|
||||
tail -n 80 "$log_file"
|
||||
printf '\n'
|
||||
fi
|
||||
done
|
||||
if [ -z "$anchor_language" ]; then
|
||||
for item in "${web_cases[@]}"; do
|
||||
IFS='|' read -r server dir cmd <<<"$item"
|
||||
key="$server"
|
||||
if [ "${web_result[$key]:-}" = "BLOCKED" ]; then
|
||||
failed=1
|
||||
printf '\n**차단: %s -> Dart.web 크로스테스트**\n' "$server"
|
||||
printf '재현 명령: `%s`\n' "${web_cmds[$key]}"
|
||||
printf '차단 사유: %s\n\n' "${web_block_reason[$key]:-unknown}"
|
||||
elif [ "${web_result[$key]:-}" = "FAIL" ]; then
|
||||
failed=1
|
||||
log_file="$log_dir/web_${server}.log"
|
||||
printf '\n**실패: %s -> Dart.web 크로스테스트**\n' "$server"
|
||||
printf '재현 명령: `%s`\n\n' "${web_cmds[$key]}"
|
||||
tail -n 80 "$log_file"
|
||||
printf '\n'
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
return "$failed"
|
||||
}
|
||||
|
|
@ -653,6 +794,7 @@ has_fail_results() {
|
|||
done
|
||||
for item in "${cross_cases[@]}"; do
|
||||
IFS='|' read -r server client dir cmd <<<"$item"
|
||||
should_run_cross_case "$server" "$client" || continue
|
||||
key="$server|$client"
|
||||
if [ "${cross_result[$key]:-}" = "FAIL" ]; then
|
||||
return 0
|
||||
|
|
@ -685,6 +827,9 @@ print_report_body() {
|
|||
if [ "$mode" = "all" ] || [ "$mode" = "unit" ]; then
|
||||
print_unit_table
|
||||
fi
|
||||
if [ "$mode" = "cross" ] && [ -n "$anchor_language" ]; then
|
||||
print_unit_table
|
||||
fi
|
||||
if [ "$mode" = "all" ] || [ "$mode" = "cross" ]; then
|
||||
print_language_pass_matrix
|
||||
print_cross_detail_table
|
||||
|
|
@ -722,10 +867,16 @@ write_result_record() {
|
|||
printf '## 실행 정보\n\n'
|
||||
printf -- '- 실행 일시: %s\n' "$run_started_at"
|
||||
printf -- '- git ref: %s\n' "$git_ref"
|
||||
printf -- '- 실행 명령: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --%s`\n' "$mode"
|
||||
if [ -n "$anchor_language" ]; then
|
||||
printf -- '- 실행 명령: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --%s --anchor %s --direction %s`\n' "$mode" "$anchor_slug" "$direction"
|
||||
printf -- '- anchor: %s\n' "$anchor_slug"
|
||||
printf -- '- direction: %s\n' "$direction"
|
||||
else
|
||||
printf -- '- 실행 명령: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --%s`\n' "$mode"
|
||||
fi
|
||||
printf -- '- exit code: %s\n' "$exit_code"
|
||||
printf -- '- 로그 디렉터리: `%s`\n' "$log_dir"
|
||||
if [ "$mode" = "all" ] || [ "$mode" = "cross" ]; then
|
||||
if [ -z "$anchor_language" ] && { [ "$mode" = "all" ] || [ "$mode" = "cross" ]; }; then
|
||||
printf -- '- Dart.web 실행 환경: %s\n' "$remote_web_execution"
|
||||
if [ "$remote_web_execution" = "remote" ]; then
|
||||
printf -- '- Dart.web remote host: `%s`\n' "$remote_web_host"
|
||||
|
|
@ -758,8 +909,13 @@ main() {
|
|||
run_unit_tests
|
||||
;;
|
||||
cross)
|
||||
run_cross_tests
|
||||
run_web_tests
|
||||
if [ -n "$anchor_language" ]; then
|
||||
run_unit_test_for_language "$anchor_language"
|
||||
run_cross_tests
|
||||
else
|
||||
run_cross_tests
|
||||
run_web_tests
|
||||
fi
|
||||
;;
|
||||
proto)
|
||||
run_proto_sync
|
||||
|
|
|
|||
|
|
@ -327,6 +327,29 @@ md_escape_cell() {
|
|||
printf '%s' "$1" | sed 's/|/\\|/g'
|
||||
}
|
||||
|
||||
is_anchor_kind() {
|
||||
case "$1" in
|
||||
functional-server-x5|functional-client-x5|functional-both-x5) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
anchor_target_supported() {
|
||||
case "$1" in
|
||||
dart|go|kotlin|python|typescript) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
anchor_direction_for_kind() {
|
||||
case "$1" in
|
||||
functional-server-x5) echo "server" ;;
|
||||
functional-client-x5) echo "client" ;;
|
||||
functional-both-x5) echo "both" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 라우팅 추천 기록을 위한 상태/중복 제어 헬퍼
|
||||
# 추천 상태:
|
||||
|
|
@ -729,12 +752,11 @@ main() {
|
|||
# -----------------------------------------------------------------------
|
||||
# non-dry-run 실행 분기
|
||||
# recommended 후보 중 지원 가능한 항목을 기존 wrapper command로 실행한다.
|
||||
# anchor(functional-server-x5, functional-client-x5, functional-both-x5)는
|
||||
# anchor-runner-options 범위이므로 skipped로 기록한다.
|
||||
# 언어 anchor x5 후보는 run_matrix.sh --cross --anchor/--direction으로 실행한다.
|
||||
# -----------------------------------------------------------------------
|
||||
printf '\n### 실행 계획\n\n'
|
||||
|
||||
local supported_kinds="performance-quick stability-quick functional-full runner-docs-selfcheck docs-selfcheck"
|
||||
local supported_kinds="performance-quick stability-quick functional-full functional-server-x5 functional-client-x5 functional-both-x5 runner-docs-selfcheck docs-selfcheck"
|
||||
local exec_log=""
|
||||
local skipped_log=""
|
||||
local nightly_log=""
|
||||
|
|
@ -759,8 +781,12 @@ main() {
|
|||
for sup in $supported_kinds; do
|
||||
[ "$kd" = "$sup" ] && is_supported=1 && break
|
||||
done
|
||||
if is_anchor_kind "$kd" && ! anchor_target_supported "$tg"; then
|
||||
skipped_log="${skipped_log}- [anchor-skipped] ${kd} / ${tg}: anchor target이 지원 언어가 아님\n"
|
||||
continue
|
||||
fi
|
||||
if [ "$is_supported" -eq 0 ]; then
|
||||
skipped_log="${skipped_log}- [anchor-skipped] ${kd} / ${tg}: anchor 실행은 anchor-runner-options 범위\n"
|
||||
skipped_log="${skipped_log}- [skipped] ${kd} / ${tg}: 지원 실행 wrapper 없음\n"
|
||||
continue
|
||||
fi
|
||||
exec_log="${exec_log}- [실행 예정] ${kd} / ${tg}: ${rs}\n"
|
||||
|
|
@ -791,8 +817,11 @@ main() {
|
|||
[ "$kd" = "$sup" ] && is_supported=1 && break
|
||||
done
|
||||
[ "$is_supported" -eq 0 ] && continue
|
||||
if is_anchor_kind "$kd" && ! anchor_target_supported "${reco_targets[$i]}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local cmd="" exit_code=0 record_path=""
|
||||
local cmd="" exit_code=0 record_path="" anchor="" anchor_dir=""
|
||||
case "$kd" in
|
||||
performance-quick)
|
||||
cmd="bash $(printf '%q' "$script_dir/run_performance.sh") --quick"
|
||||
|
|
@ -815,6 +844,15 @@ main() {
|
|||
exit_code=$?
|
||||
record_path="$(find "$repo_root/agent-test/runs" -maxdepth 1 -name '*proto-socket-full-matrix*' -type f 2>/dev/null | sort -r | head -n 1)"
|
||||
;;
|
||||
functional-server-x5|functional-client-x5|functional-both-x5)
|
||||
anchor_dir="$(anchor_direction_for_kind "$kd")"
|
||||
anchor="${reco_targets[$i]}"
|
||||
cmd="bash $(printf '%q' "$script_dir/run_matrix.sh") --cross --anchor $anchor --direction $anchor_dir"
|
||||
printf '\n--- 실행: %s ---\n' "$cmd" >&2
|
||||
(cd "$repo_root" && bash "$script_dir/run_matrix.sh" --cross --anchor "$anchor" --direction "$anchor_dir") >&2
|
||||
exit_code=$?
|
||||
record_path="$(find "$repo_root/agent-test/runs" -maxdepth 1 -name "*proto-socket-anchor-${anchor}-${anchor_dir}-matrix*" -type f 2>/dev/null | sort -r | head -n 1)"
|
||||
;;
|
||||
runner-docs-selfcheck|docs-selfcheck)
|
||||
cmd="bash -n $(printf '%q' "$script_dir/run_test_by_change.sh")"
|
||||
printf '\n--- 실행: %s ---\n' "$cmd" >&2
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ Proto Socket은 여러 언어와 플랫폼에서 일관되게 동작하는 얇
|
|||
- [고성능 병렬 운용 기준선과 최적화](archive/phase/stability-maintenance/milestones/high-performance-parallel-operations.md) - 상태: 완료; 목표: 현재 5개 언어 구현의 병렬 운용 성능을 언어별/transport별로 측정하고 gateway, queue, worker, serialization 병목을 최적화한다.
|
||||
- [언어별 성능 병목 개선](archive/phase/stability-maintenance/milestones/performance-hotspot-optimization.md) - 상태: 완료; 목표: 측정 결과에서 확인된 Dart TCP fixed latency/large payload 및 isolate receive path hardening, TypeScript WS large payload, Kotlin WS latency/slow-mix 검증 모델, TypeScript gateway `worker_threads` overhead 병목을 안정성 hard gate와 단일 mandatory receive path 원칙을 유지하면서 개선하고, Go/Python reference 기준점을 보강한다.
|
||||
- [워크스페이스 포트/환경 표준화](archive/phase/stability-maintenance/milestones/workspace-port-env-standardization.md) - 상태: 완료; 목표: proto-socket이 runtime service port를 소유하지 않는다는 점과 cross-language test runner의 fixed local port 대역을 workspace 표준 예외로 문서화한다.
|
||||
- [변경 기반 테스트 라우팅 정형화](milestones/change-aware-test-routing.md) - 상태: [진행중]; 목표: 마지막 통과 테스트 지점과 현재 변경 내용을 분석해 기능, 속도, 안정성 테스트 중 필요한 검증 범위를 자동 선택하고, 언어별 anchor x5와 야간 장시간 측정을 분리한다.
|
||||
- [야간 대용량 패킷 성능 기준선](milestones/nightly-large-payload-baseline.md) - 상태: [계획]; 목표: 64KB/1MB payload 성능 baseline과 회귀 판단을 실행 host local time 20:00 이후 야간 window에서 별도 수집해 주간 작업 부하 편차와 분리한다.
|
||||
- [변경 기반 테스트 라우팅 정형화](archive/phase/stability-maintenance/milestones/change-aware-test-routing.md) - 상태: 완료; 목표: 마지막 통과 테스트 지점과 현재 변경 내용을 분석해 기능, 속도, 안정성 테스트 중 필요한 검증 범위를 자동 선택하고, 언어별 anchor x5와 야간 장시간 측정을 분리한다.
|
||||
- [야간 대용량 패킷 성능 기준선](milestones/nightly-large-payload-baseline.md) - 상태: [진행중]; 목표: 64KB/1MB payload 성능 baseline과 회귀 판단을 실행 host local time 20:00 이후 야간 window에서 별도 수집해 주간 작업 부하 편차와 분리한다.
|
||||
- [대용량 WS/병렬 전송 후속 최적화](milestones/large-payload-followup-optimization.md) - 상태: [계획]; 목표: 야간 기준선에서 병목이 유지되는 row를 근거로 TypeScript WS 1MB, WS fixed latency, Kotlin WS 1MB, Dart large-packet guard 후보를 후속 최적화로 구체화한다.
|
||||
|
||||
### 남은 native platform 포팅
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
[완료]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
|
|
@ -72,24 +72,27 @@
|
|||
|
||||
스킬이 선택한 라우팅을 실제 명령으로 연결한다.
|
||||
|
||||
- [ ] [routing-skill] 프로젝트 스킬 또는 기존 `run-proto-socket-test-matrix` 스킬에 "작업된 내용을 바탕으로 테스트" 요청 라우팅 절차를 추가한다. 검증: 사용자가 언어와 테스트 종류를 지정하지 않아도 변경 분석 결과와 선택한 검증 범위를 먼저 보고한다.
|
||||
- [ ] [auto-test-shell] `run_test_by_change.sh` 자동 라우팅 shell을 추가해 `테스트 해봐` 요청의 기본 실행 단위로 쓴다. 검증: 마지막 PASS 지점, 변경 분류, 선택된 하위 command가 결과 기록에 남는다.
|
||||
- [ ] [speed-test-shell] `run_speed_test.sh` 속도 전용 shell을 추가해 quick/daytime과 full/nightly 후보를 관리한다. 검증: 성능 관련 변경에서 기존 `run_performance.sh`/`run_stress.sh`를 직접 외우지 않아도 실행된다.
|
||||
- [ ] [stability-test-shell] `run_stability_test.sh` 안정성 전용 shell을 추가해 quick stress와 nightly long-run 후보를 관리한다. 검증: stability hard gate 요약이 별도 결과로 남는다.
|
||||
- [x] [routing-skill] 프로젝트 스킬 또는 기존 `run-proto-socket-test-matrix` 스킬에 "작업된 내용을 바탕으로 테스트" 요청 라우팅 절차를 추가한다. 검증: 사용자가 언어와 테스트 종류를 지정하지 않아도 변경 분석 결과와 선택한 검증 범위를 먼저 보고한다.
|
||||
- [x] [auto-test-shell] `run_test_by_change.sh` 자동 라우팅 shell을 추가해 `테스트 해봐` 요청의 기본 실행 단위로 쓴다. 검증: 마지막 PASS 지점, 변경 분류, 선택된 하위 command가 결과 기록에 남는다.
|
||||
- [x] [speed-test-shell] `run_speed_test.sh` 속도 전용 shell을 추가해 quick/daytime과 full/nightly 후보를 관리한다. 검증: 성능 관련 변경에서 기존 `run_performance.sh`/`run_stress.sh`를 직접 외우지 않아도 실행된다.
|
||||
- [x] [stability-test-shell] `run_stability_test.sh` 안정성 전용 shell을 추가해 quick stress와 nightly long-run 후보를 관리한다. 검증: stability hard gate 요약이 별도 결과로 남는다.
|
||||
- [x] [full-test-shell] `run_full_test.sh` 전체 테스트 shell을 추가해 functional full과 nightly heavy 후보를 분리한다. 검증: 기존 `run_matrix.sh --all`과 performance/stability full이 한 결과 계약 안에서 구분된다.
|
||||
- [ ] [anchor-runner-options] 필요한 경우 각 shell 또는 기존 runner에 `--anchor <lang>`와 `--direction server|client|both` 또는 동등한 개별 명령 조합을 추가한다. 검증: 수정 언어 중심 x5를 full 5x5 없이 실행할 수 있다.
|
||||
- [ ] [test-profile-docs] `agent-test/local/rules.md`와 관련 profile 문서에 4개 shell entrypoint와 주야간 분리 기준을 반영한다. 검증: 테스트 문서만 읽어도 네 라우팅의 명령과 판정 기준을 알 수 있다.
|
||||
- [ ] [result-report-contract] 결과 보고에 선택 이유, 실행 명령, 결과 record, skipped/nightly-pending 항목을 남긴다. 검증: 실행하지 않은 야간 full을 PASS로 오인하지 않는다.
|
||||
- [x] [anchor-runner-options] 필요한 경우 각 shell 또는 기존 runner에 `--anchor <lang>`와 `--direction server|client|both` 또는 동등한 개별 명령 조합을 추가한다. 검증: 수정 언어 중심 x5를 full 5x5 없이 실행할 수 있다.
|
||||
- [x] [test-profile-docs] `agent-test/local/rules.md`와 관련 profile 문서에 4개 shell entrypoint와 주야간 분리 기준을 반영한다. 검증: 테스트 문서만 읽어도 네 라우팅의 명령과 판정 기준을 알 수 있다.
|
||||
- [x] [result-report-contract] 결과 보고에 선택 이유, 실행 명령, 결과 record, skipped/nightly-pending 항목을 남긴다. 검증: 실행하지 않은 야간 full을 PASS로 오인하지 않는다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 없음
|
||||
- 상태: 승인됨
|
||||
- 요청일: 2026-06-10
|
||||
- 완료 근거:
|
||||
- `run_matrix.sh --cross --anchor <lang> --direction server|client|both`가 추가되어 언어 anchor x5를 full 5x5 없이 실행할 수 있다.
|
||||
- `run_test_by_change.sh`가 언어별 `functional-server-x5`, `functional-client-x5`, `functional-both-x5` 추천을 anchor matrix 실행으로 연결한다.
|
||||
- focused 검증 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --cross --anchor go --direction server` PASS, 결과 기록 `agent-test/runs/20260610-104319-proto-socket-anchor-go-server-matrix.md`.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
- [x] 사용자가 완료 결과를 확인했다
|
||||
- [x] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 2026-06-10 사용자 요청으로 완료 승인 후 archive 이동.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -117,3 +120,5 @@
|
|||
- 완료: `agent-task/archive/2026/06/m-change-aware-test-routing/04_user_entrypoints_full_route/complete.log` -> `full-test-route`
|
||||
- 완료(직접 처리): `user-entrypoints` auto/speed/stability dry-run 정책 보강 -> `auto-test-route`, `speed-test-route`, `stability-test-route`; 검증=`bash -n .../run_test_by_change.sh`, focused temp repo route smoke(auto/speed/stability), `rg --sort path ...`
|
||||
- 계획 작성: `agent-task/m-change-aware-test-routing/05_user_entrypoint_runner_contract/PLAN-cloud-G07.md` -> `routing-skill`, `auto-test-shell`, `speed-test-shell`, `stability-test-shell`, `test-profile-docs`, `result-report-contract`
|
||||
- 완료(직접 처리): `skill-and-runner-contract` user-entrypoint runner 계약 동기화 -> `routing-skill`, `auto-test-shell`, `speed-test-shell`, `stability-test-shell`, `test-profile-docs`, `result-report-contract`; 검증=`bash -n .../run_test_by_change.sh .../run_speed_test.sh .../run_stability_test.sh .../run_full_test.sh`, `run_test_by_change.sh --route auto --dry-run`, `run_speed_test.sh --dry-run`, `run_stability_test.sh --dry-run`, `run_full_test.sh --dry-run`
|
||||
- 완료(직접 처리): `skill-and-runner-contract` anchor x5 실행 연결 -> `anchor-runner-options`; 검증=`bash -n .../run_matrix.sh .../run_test_by_change.sh .../run_speed_test.sh .../run_stability_test.sh .../run_full_test.sh`, invalid anchor/mode guard, direction-without-anchor guard, `run_matrix.sh --cross --anchor go --direction server` PASS (`agent-test/runs/20260610-104319-proto-socket-anchor-go-server-matrix.md`)
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
[진행중]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
|
|
@ -33,25 +33,25 @@
|
|||
|
||||
대용량 payload baseline은 낮 시간 작업 부하와 분리한다.
|
||||
|
||||
- [ ] [time-window-rule] 64KB/1MB payload baseline 후보는 실행 host local time 기준 20:00 이후 시작한 record만 인정한다. 검증: 결과 기록의 실행 일시와 host timezone 또는 실행 메모가 야간 조건을 확인 가능하게 남는다.
|
||||
- [ ] [daytime-policy] 20:00 이전에 수집한 대용량 payload 결과는 exploratory record로 표시하고, baseline/regression 완료 근거에서 제외한다. 검증: milestone 완료 근거에 주간 record가 baseline으로 쓰이지 않는다.
|
||||
- [x] [time-window-rule] 64KB/1MB payload baseline 후보는 실행 host local time 기준 20:00 이후 시작한 record만 인정한다. 검증: 2026-06-10 실행 메모에 host local time `2026-06-10 21:18:24 KST +0900` full wrapper 시작과 `2026-06-10 21:40:06 KST` payload focused record 생성 근거를 남겼다.
|
||||
- [x] [daytime-policy] 20:00 이전에 수집한 대용량 payload 결과는 exploratory record로 표시하고, baseline/regression 완료 근거에서 제외한다. 검증: 2026-06-10 완료 근거는 야간 focused record만 baseline 후보로 사용하고, 기존 주간/시간대 미확인 record는 방향성 비교용으로만 남겼다.
|
||||
|
||||
### Epic: [payload-baseline] 대용량 payload 기준선
|
||||
|
||||
작업 부하가 낮은 시간대에 같은 profile을 반복해 편차를 줄인다.
|
||||
|
||||
- [ ] [night-full-record] 20:00 이후 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh --full`을 실행해 야간 full performance record를 남긴다. 검증: same-language, cross-language, TypeScript gateway 구성요소가 모두 기록되고 대용량 payload rows가 포함된다.
|
||||
- [ ] [payload-focused-record] 20:00 이후 `run_stress.sh --full --profile payload` 계열 focused record를 수집해 64KB/1MB TCP/WS rows를 별도로 확인한다. 검증: Dart, Go, Kotlin, Python, TypeScript payload rows가 stability hard gate 0으로 기록된다.
|
||||
- [ ] [repeatability] 필요하면 서로 다른 야간 window에서 최소 2회 이상 반복해 p99/throughput 편차를 비교한다. 검증: 같은 host/runtime/profile의 night records끼리 비교하고, 편차가 큰 row는 baseline 확정 대신 watchlist로 남긴다.
|
||||
- [ ] [night-full-record] 20:00 이후 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh --full`을 실행해 야간 full performance record를 남긴다. 검증: same-language, cross-language, TypeScript gateway 구성요소가 모두 기록되고 대용량 payload rows가 포함된다. 진행 메모: 2026-06-10 21:18 KST에 시작했으나 Dart same-language full stress 단계가 20분 이상 record 없이 지속되어 중단했다. 2일차 장기 실행으로 이월한다.
|
||||
- [x] [payload-focused-record] 20:00 이후 `run_stress.sh --full --profile payload` 계열 focused record를 수집해 64KB/1MB TCP/WS rows를 별도로 확인한다. 검증: `agent-test/runs/20260610-124006-proto-socket-stress-full.md`에서 Dart, Go, Kotlin, Python, TypeScript payload rows가 모두 PASS이고 stability violations 0으로 기록됐다.
|
||||
- [x] [repeatability] 필요하면 2회 이상 반복해 p99/throughput 편차를 비교한다. 검증: `agent-test/runs/20260610-124006-proto-socket-stress-full.md`와 `agent-test/runs/20260610-131132-proto-socket-stress-full.md`를 같은 host/runtime/profile의 night records로 비교했다. 모든 64KB/1MB row는 PASS/PASS와 stability violations 0을 유지했고, 편차가 큰 row는 baseline 확정값 대신 watchlist로 남겼다.
|
||||
|
||||
### Epic: [large-packet-watchlist] 큰 패킷 병목 감시
|
||||
|
||||
기존 최적화가 실제 idle 조건에서도 유지되는지 확인한다.
|
||||
|
||||
- [ ] [dart-large-packet] Dart TCP/WS 64KB/1MB rows가 야간 기준에서도 안정성 hard gate를 유지하고, 주간 대비 방향성이 개선으로 유지되는지 확인한다.
|
||||
- [ ] [kotlin-ws-large-packet] Kotlin WS 64KB/1MB rows의 p99, throughput, memory, queue backlog를 야간 기준으로 다시 확인한다.
|
||||
- [ ] [typescript-ws-large-packet] TypeScript WS 1MB row를 야간 기준으로 재측정하고, 병목이 여전히 남으면 다음 최적화 후보로 분리한다.
|
||||
- [ ] [reference-large-packet] Go/Python TCP/WS 1MB rows를 야간 reference fast/medium path로 확정한다.
|
||||
- [x] [dart-large-packet] Dart TCP/WS 64KB/1MB rows가 야간 기준에서도 안정성 hard gate를 유지하고, 주간 대비 방향성이 개선으로 유지되는지 확인한다. 검증: 2026-06-10 focused records에서 Dart 64KB/1MB TCP/WS 모두 PASS, stability violations 0. WS 1MB는 2회 모두 2026-06-07 full record 대비 개선됐고, TCP 1MB는 1회차 개선 후 2회차 p99가 141.681ms로 흔들려 watchlist에 남긴다.
|
||||
- [x] [kotlin-ws-large-packet] Kotlin WS 64KB/1MB rows의 p99, throughput, memory, queue backlog를 야간 기준으로 다시 확인한다. 검증: Kotlin WS 64KB p99 13.461ms/13.049ms, throughput 1068.8/970.4rps, queue backlog 1/2; WS 1MB p99 75.940ms/100.032ms, throughput 113.6/92.6rps, queue backlog 1/1; stability violations 0. WS 1MB p99 편차는 watchlist로 남긴다.
|
||||
- [x] [typescript-ws-large-packet] TypeScript WS 1MB row를 야간 기준으로 재측정하고, 병목이 여전히 남으면 다음 최적화 후보로 분리한다. 검증: TypeScript WS 1MB p99 226.008ms/237.395ms, throughput 33.5/33.3rps, stability violations 0. Go/Python reference 대비 낮은 throughput과 높은 p99가 반복되어 후속 최적화 후보로 남긴다.
|
||||
- [x] [reference-large-packet] Go/Python TCP/WS 1MB rows를 야간 reference fast/medium path로 확정한다. 검증: Go 1MB TCP/WS p99 23.201ms/28.900ms, Python 1MB TCP/WS p99 16.776ms/44.841ms, 모두 stability violations 0으로 기록됐다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
|
|
@ -76,3 +76,23 @@
|
|||
- 선행 작업: `언어별 성능 병목 개선`, `성능 기록 판정 자동화`
|
||||
- 후속 작업: `대용량 WS/병렬 전송 후속 최적화` 스케치에서 야간 기준에서도 병목이 유지되는 row를 구현 후보로 승격한다.
|
||||
- 표준선: 대용량 payload baseline은 실행 host local time 기준 20:00 이후 시작한 결과만 인정한다.
|
||||
|
||||
## 2026-06-10 진행 메모
|
||||
|
||||
- host local time 확인: `2026-06-10 21:18:24 KST +0900`.
|
||||
- full wrapper 시도: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh --full`.
|
||||
- full wrapper 결과: `/tmp/proto-socket-performance.yAs0NV/same-language.log`에 Dart same-language full stress 시작만 남고 20분 이상 record가 생성되지 않아 중단했다. 중단 시 실행 중이던 프로세스 그룹은 종료 확인했다.
|
||||
- payload focused 실행: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --profile payload`.
|
||||
- payload focused 결과: `agent-test/runs/20260610-124006-proto-socket-stress-full.md`, overall PASS, 언어별 PASS 5/5, required skip 0, stability violations 0.
|
||||
- 주요 watchlist: Dart TCP/WS 1MB PASS, Kotlin WS 64KB/1MB PASS, TypeScript WS 1MB PASS이나 p99 226.008ms와 33.5rps로 후속 최적화 후보 유지, Go/Python 1MB reference rows PASS.
|
||||
- 2회차 host local time 확인: `2026-06-10 22:11:12 KST +0900`.
|
||||
- 2회차 payload focused 결과: `agent-test/runs/20260610-131132-proto-socket-stress-full.md`, overall PASS, 언어별 PASS 5/5, required skip 0, stability violations 0.
|
||||
- 1회차 대비 2회차 반복성: 모든 64KB/1MB row가 PASS/PASS를 유지했다. TypeScript WS 1MB는 p99 226.008ms -> 237.395ms, throughput 33.5rps -> 33.3rps로 병목이 안정적으로 재현됐다. Dart TCP 1MB, Kotlin TCP/WS 1MB, Go WS 64KB는 p99 편차가 커서 baseline 확정값 대신 watchlist로 둔다.
|
||||
|
||||
## 2일차 핸드오프
|
||||
|
||||
- 실행 전 host local time이 20:00 이후인지 `date '+%Y-%m-%d %H:%M:%S %Z %z'`로 먼저 남긴다.
|
||||
- 1순위는 full performance record 재수집이다: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh --full`.
|
||||
- full wrapper가 다시 오래 걸리면 Dart same-language full stress에서 멈추는지 먼저 확인하고, full wrapper 완료 전에 baseline 완료 판정을 하지 않는다.
|
||||
- 2순위는 필요 시 다른 야간 window repeatability 강화다: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --profile payload`를 한 번 더 실행해 2026-06-10 2회 기록과 p99/throughput 편차를 비교한다.
|
||||
- 20:00 이전 결과나 host timezone 확인이 없는 결과는 baseline/regression 완료 근거가 아니라 exploratory 또는 방향성 참고로만 사용한다.
|
||||
|
|
|
|||
Loading…
Reference in a new issue