diff --git a/docs/MDTODO/details/pikaoa-enterprise-platform/R1.5.9.9_Task_Report.md b/docs/MDTODO/details/pikaoa-enterprise-platform/R1.5.9.9_Task_Report.md new file mode 100644 index 00000000..b081a8ba --- /dev/null +++ b/docs/MDTODO/details/pikaoa-enterprise-platform/R1.5.9.9_Task_Report.md @@ -0,0 +1,39 @@ +# R1.5.9.9 任务报告 + +## 状态 + +- 当前状态:进行中。 +- 关联问题:`pikasTech/unidesk#2090`。 +- 待办:由主代理在真实 `pikaoa-test` 运行面复测后决定是否完成任务。 + +## 实现 + +- 将 migration runner 从单一 `kubectl wait --for=condition=complete` 改为有界短轮询。 +- `Complete=True` 时成功结束。 +- `FailureTarget=True` 时立即返回 `migration-job-failure-target`。 +- `Failed=True` 时立即返回 `migration-job-failed`。 +- Job 查询失败时返回 `migration-job-query-failed`。 +- condition JSON 无法解析时返回 `migration-job-condition-invalid`。 +- 超过总等待时间时返回 `migration-job-timeout`。 +- 每轮查询前检查取消请求,查询超时上限收紧为 5 秒,轮询间隔为 1 秒。 +- 保留异步 submit-and-poll、现有状态文件边界和 Secret/DSN 脱敏行为。 + +## Target 原入口证据 + +- Target:`NC01`。 +- targetWorkspace:`/root/unidesk/.worktree/pikaoa-migration-job-terminal`。 +- 分支:`fix/pikaoa-migration-job-terminal`。 +- 基线提交:`47ca9a7cb218528c45fef0f88cb4e4bc51a5b805`。 +- `bun --check scripts/src/pikaoa-test-target.ts`:通过。 +- `bun test scripts/src/pikaoa-test-target-async.test.ts`:2 pass,0 fail。 +- 定向测试覆盖 `Complete`、`Failed`、`FailureTarget` 和查询失败终态。 +- `bun scripts/cli.ts check --syntax-only`:11 pass,0 fail。 +- `git diff --check`:通过。 +- `scripts/src/pikaoa-test-target.ts`:1818 行,低于 2000 行。 +- 未执行运行面 mutation,未删除或重建真实 Job。 + +## Primary Workspace 轻量准备 + +- 仅用于读取 runtime resource bundle 中的技能与 `trans` 工具说明。 +- 未在 primary workspace 读取、修改或验证目标源码。 +- 未将 runner 容器本地结果作为 Target 验证证据。 diff --git a/scripts/src/pikaoa-test-target-async.test.ts b/scripts/src/pikaoa-test-target-async.test.ts index 1c1ed373..e2e4e34c 100644 --- a/scripts/src/pikaoa-test-target-async.test.ts +++ b/scripts/src/pikaoa-test-target-async.test.ts @@ -74,9 +74,15 @@ if printf '%s' "$args" | grep -q ' rollout status '; then [ ! -f "$root/slow" ] || sleep 0.4 exit 0 fi -if printf '%s' "$args" | grep -q ' wait '; then - [ ! -f "$root/slow" ] || sleep 0.4 - if [ -f "$root/fail-migration" ]; then printf '%s\n' 'migration failed' >&2; exit 1; fi +if printf '%s' "$args" | grep -q ' get job/pikaoa-migrate '; then + if [ -f "$root/fail-migration-query" ]; then printf '%s\n' 'job query failed' >&2; exit 1; fi + if [ -f "$root/failure-target-migration" ]; then + printf '%s\n' '{"status":{"conditions":[{"type":"FailureTarget","status":"True","reason":"BackoffLimitExceeded"}]}}' + elif [ -f "$root/fail-migration" ]; then + printf '%s\n' '{"status":{"conditions":[{"type":"Failed","status":"True","reason":"BackoffLimitExceeded"}]}}' + else + printf '%s\n' '{"status":{"conditions":[{"type":"Complete","status":"True"}]}}' + fi exit 0 fi if printf '%s' "$args" | grep -Eq '(^| )get namespace '; then @@ -257,10 +263,30 @@ exit 64 await projection("start", "runtime", true, "migration"); const failed = await waitFor("runtime", "start-failed", "migration"); const failedTask = ((failed.status as Record).task as Record); - assert.equal(failedTask.code, "migration-wait-failed"); + assert.equal(failedTask.code, "migration-job-failed"); assert.equal(failedTask.exitCode, 46); rmSync(join(fakeRoot, "fail-migration")); + writeFileSync(join(fakeRoot, "failure-target-migration"), "1\n"); + await projectionWithCommit("start", "runtime", concurrentCommit, "migration"); + const failureTarget = await waitFor("runtime", "start-failed", "migration"); + const failureTargetTask = ((failureTarget.status as Record).task as Record); + assert.equal(failureTargetTask.code, "migration-job-failure-target"); + assert.equal(failureTargetTask.exitCode, 46); + rmSync(join(fakeRoot, "failure-target-migration")); + + writeFileSync(join(fakeRoot, "fail-migration-query"), "1\n"); + await projectionWithCommit("start", "runtime", "abcdef0123456789", "migration"); + const migrationQueryFailed = await waitFor("runtime", "start-failed", "migration"); + const migrationQueryFailedTask = ((migrationQueryFailed.status as Record).task as Record); + assert.equal(migrationQueryFailedTask.code, "migration-job-query-failed"); + assert.equal(migrationQueryFailedTask.exitCode, 46); + rmSync(join(fakeRoot, "fail-migration-query")); + + await projectionWithCommit("start", "runtime", "1234567890abcdef", "migration"); + const migrationSucceeded = await waitFor("runtime", "start-succeeded", "migration"); + assert.equal((((migrationSucceeded.status as Record).task as Record).exitCode), 0); + writeFileSync(join(fakeRoot, "slow"), "1\n"); await projection("start", "runtime", true, "web"); const stopped = await projection("stop", "runtime", false, "web"); @@ -303,6 +329,9 @@ exit 64 workerMissingReset, workerMissingSucceeded, failed, + failureTarget, + migrationQueryFailed, + migrationSucceeded, stopped, foundationSubmitted, foundationSucceeded, diff --git a/scripts/src/pikaoa-test-target.ts b/scripts/src/pikaoa-test-target.ts index 69c6af15..37a33861 100644 --- a/scripts/src/pikaoa-test-target.ts +++ b/scripts/src/pikaoa-test-target.ts @@ -1206,8 +1206,45 @@ if [ "$step" = all ] || [ "$step" = migration ]; then if ! kubectl apply --server-side --force-conflicts --field-manager=${shQuote(target.fieldManager)} -f "$tmp/migration.yaml" >"$tmp/migration-apply.out" 2>"$tmp/migration-apply.err"; then failure_code=migration-apply-failed; exit 45; fi current_phase=migration-wait write_status running "$current_phase" start-running null - check_canceled - if ! kubectl -n "$namespace" wait --for=condition=complete job/${target.migration.jobName} --timeout=${target.waitTimeoutSeconds}s >/dev/null 2>"$tmp/migration-wait.err"; then failure_code=migration-wait-failed; exit 46; fi + migration_deadline=$(( $(date +%s) + ${target.waitTimeoutSeconds} )) + while :; do + check_canceled + if ! kubectl -n "$namespace" get job/${target.migration.jobName} --request-timeout=${Math.min(target.taskState.statusRequestTimeoutSeconds, 5)}s -o json >"$tmp/migration-job.json" 2>"$tmp/migration-wait.err"; then + failure_code=migration-job-query-failed + exit 46 + fi + if ! migration_condition="$(python3 - "$tmp/migration-job.json" <<'PY' +import json, pathlib, sys +payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) +conditions = { + item.get("type"): item.get("status") + for item in payload.get("status", {}).get("conditions", []) + if isinstance(item, dict) +} +if conditions.get("Complete") == "True": + print("complete") +elif conditions.get("FailureTarget") == "True": + print("failure-target") +elif conditions.get("Failed") == "True": + print("failed") +else: + print("running") +PY +)"; then + failure_code=migration-job-condition-invalid + exit 46 + fi + case "$migration_condition" in + complete) break ;; + failure-target) failure_code=migration-job-failure-target; exit 46 ;; + failed) failure_code=migration-job-failed; exit 46 ;; + esac + if [ "$(date +%s)" -ge "$migration_deadline" ]; then + failure_code=migration-job-timeout + exit 46 + fi + sleep 1 + done fi for component in api worker web; do if [ "$step" != all ] && [ "$step" != "$component" ]; then continue; fi