From 9800ca5ca050000bc6e4662fbcd23cea03210fec Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 20:48:33 +0200 Subject: [PATCH] fix: expose PikaOA runtime query failures --- docs/MDTODO/pikaoa-enterprise-platform.md | 2 +- scripts/src/pikaoa-test-target-async.test.ts | 38 +++++++++++++++++++- scripts/src/pikaoa-test-target.ts | 26 +++++++++++--- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/docs/MDTODO/pikaoa-enterprise-platform.md b/docs/MDTODO/pikaoa-enterprise-platform.md index 249ca8a4..bd740da9 100644 --- a/docs/MDTODO/pikaoa-enterprise-platform.md +++ b/docs/MDTODO/pikaoa-enterprise-platform.md @@ -92,7 +92,7 @@ #### R1.5.9 [in_progress] 落实用户最新要求:PikaOA 可不经过 CI/CD,由 YAML-first 在专用测试集群启动临时 Kubernetes 做测试和调试;SPEC 分离正式发布与临时测试,登记并验收真实 target,执行记录见 [pikasTech/unidesk#2058](https://github.com/pikasTech/unidesk/issues/2058),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9_Task_Report.md)。 -##### R1.5.9.1 [completed] +##### R1.5.9.1 [in_progress] 将 `pikaoa test-target start` 的 PostgreSQL、迁移和 rollout 长等待改为提交即返回与 `status` 短轮询,保持真实失败可见和 Secret 脱敏,执行记录见 [pikasTech/unidesk#2060](https://github.com/pikasTech/unidesk/issues/2060),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.1_Task_Report.md)。 ### R1.6 diff --git a/scripts/src/pikaoa-test-target-async.test.ts b/scripts/src/pikaoa-test-target-async.test.ts index c6628204..c45faf27 100644 --- a/scripts/src/pikaoa-test-target-async.test.ts +++ b/scripts/src/pikaoa-test-target-async.test.ts @@ -80,6 +80,10 @@ if printf '%s' "$args" | grep -q ' wait '; then fi if printf '%s' "$args" | grep -Eq '(^| )get namespace '; then name="$(namespace_after_resource "$@")" + if [ -f "$root/fail-runtime-query" ]; then + printf '%s\n' 'unable to connect to the server' >&2 + exit 1 + fi if [ ! -f "$root/namespaces/$name" ]; then printf 'Error from server (NotFound): namespaces "%s" not found\n' "$name" >&2 exit 1 @@ -92,6 +96,10 @@ if printf '%s' "$args" | grep -Eq '(^| )get namespace '; then exit 0 fi if printf '%s' "$args" | grep -q ' get deployment,statefulset,job,service,persistentvolumeclaim,pod '; then + if [ -f "$root/fail-runtime-resource-query" ]; then + printf '%s\n' 'resource query failed' >&2 + exit 1 + fi printf '%s\n' '{"items":[]}' exit 0 fi @@ -108,6 +116,8 @@ exit 64 const fixturePath = join(root, "pikaoa.yaml"); const fixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8") .replace("validationOnly: true", "validationOnly: false") + .replace("statusRequestTimeoutSeconds: 3", "statusRequestTimeoutSeconds: 31") + .replace("logTailLines: 8", "logTailLines: 101") .replace("/tmp/unidesk-pikaoa-test-target-fixture", stateRoot); writeFileSync(fixturePath, fixture); @@ -155,13 +165,39 @@ exit 64 const submitted = await projection("start", "success", true); assert.equal(submitted.code, "start-submitted", JSON.stringify(lastCapture)); assert.equal(submitted.mutation, true); + assert.deepEqual((submitted.target as Record).taskState, { + rootPath: stateRoot, + statusRequestTimeoutSeconds: 31, + logTailLines: 101, + }); const running = await projection("status", "success"); assert.equal((running.status as Record).code, "start-running"); rmSync(join(fakeRoot, "slow")); const succeeded = await waitFor("success", "start-succeeded"); const succeededStatus = succeeded.status as Record; assert.equal((succeededStatus.task as Record).exitCode, 0); - assert.ok((succeededStatus.logTail as unknown[]).length <= 8); + assert.ok((succeededStatus.logTail as unknown[]).length <= 101); + + writeFileSync(join(fakeRoot, "fail-runtime-query"), "1\n"); + const runtimeQueryFailed = await projection("status", "success"); + const runtimeQueryFailedStatus = runtimeQueryFailed.status as Record; + assert.equal(runtimeQueryFailed.ok, false); + assert.equal(runtimeQueryFailedStatus.code, "runtime-query-failed"); + assert.equal(runtimeQueryFailedStatus.taskCode, "start-succeeded"); + assert.equal((runtimeQueryFailedStatus.runtime as Record).code, "runtime-query-failed"); + const runtimeQueryFailedText = await runPikaoaCommand(config, args("status", "success").slice(0, -2), localCapture); + assert.match(runtimeQueryFailedText.renderedText, /status=runtime-query-failed/); + rmSync(join(fakeRoot, "fail-runtime-query")); + + writeFileSync(join(fakeRoot, "fail-runtime-resource-query"), "1\n"); + const runtimeResourceQueryFailed = await projection("status", "success"); + const runtimeResourceQueryFailedStatus = runtimeResourceQueryFailed.status as Record; + assert.equal(runtimeResourceQueryFailed.ok, false); + assert.equal(runtimeResourceQueryFailedStatus.code, "runtime-resource-query-failed"); + assert.equal(runtimeResourceQueryFailedStatus.taskCode, "start-succeeded"); + assert.equal((runtimeResourceQueryFailedStatus.runtime as Record).code, "runtime-resource-query-failed"); + rmSync(join(fakeRoot, "fail-runtime-resource-query")); + const repeated = await projection("start", "success", true); assert.equal(repeated.code, "start-already-succeeded"); assert.equal(repeated.mutation, false); diff --git a/scripts/src/pikaoa-test-target.ts b/scripts/src/pikaoa-test-target.ts index 1c1ec702..ffd15d54 100644 --- a/scripts/src/pikaoa-test-target.ts +++ b/scripts/src/pikaoa-test-target.ts @@ -299,8 +299,8 @@ function parseTarget(id: string, root: Record, configLabel: str fieldManager: kubernetesName(nonEmpty(root.fieldManager, `${path}.fieldManager`), `${path}.fieldManager`, 63), taskState: { rootPath: taskStateRoot(taskState.rootPath, `${path}.taskState.rootPath`), - statusRequestTimeoutSeconds: positiveInteger(taskState.statusRequestTimeoutSeconds, `${path}.taskState.statusRequestTimeoutSeconds`, 30), - logTailLines: positiveInteger(taskState.logTailLines, `${path}.taskState.logTailLines`, 100), + statusRequestTimeoutSeconds: positiveSafeInteger(taskState.statusRequestTimeoutSeconds, `${path}.taskState.statusRequestTimeoutSeconds`), + logTailLines: positiveSafeInteger(taskState.logTailLines, `${path}.taskState.logTailLines`), }, cleanup: { mode: exactString(cleanup.mode, `${path}.cleanup.mode`, "delete-namespace") as "delete-namespace", @@ -1124,14 +1124,24 @@ else: code = {"queued": "start-running", "running": "start-running", "succeeded": "start-succeeded", "failed": "start-failed", "canceled": "start-canceled"}.get(state, "start-state-unknown") ok = state not in ("failed", "canceled", "unknown") exists = True +task_code = code runtime = {"queried": True, "exists": runtime_code in ("runtime-present", "runtime-resource-query-failed"), "code": runtime_code} -for key, path in (("namespace", namespace_path), ("resources", resources_path)): - if path.exists(): +runtime_paths = [] +if runtime_code in ("runtime-present", "runtime-resource-query-failed"): + runtime_paths.append(("namespace", namespace_path)) +if runtime_code == "runtime-present": + runtime_paths.append(("resources", resources_path)) +for key, path in runtime_paths: + if path.exists() and path.stat().st_size > 0: try: runtime[key] = json.loads(path.read_text()) except Exception: runtime["code"] = "runtime-response-unreadable" -payload = {"ok": ok, "mutation": False, "code": code, "jobId": job_id, "taskExists": exists, "task": task, "runnerAlive": runner_alive, "logTail": events, "runtime": runtime, "valuesPrinted": False} + else: + runtime["code"] = "runtime-response-unreadable" +if runtime["code"] in ("runtime-query-failed", "runtime-resource-query-failed", "runtime-response-unreadable"): + code, ok = runtime["code"], False +payload = {"ok": ok, "mutation": False, "code": code, "taskCode": task_code, "jobId": job_id, "taskExists": exists, "task": task, "runnerAlive": runner_alive, "logTail": events, "runtime": runtime, "valuesPrinted": False} print(json.dumps(payload, separators=(",", ":"))) PY `; @@ -1213,6 +1223,7 @@ function summarizeRemoteStatus(parsed: Record, target: TestTarg return { ok: parsed.ok === true, code: parsed.code ?? "start-status-unknown", + taskCode: parsed.taskCode ?? "start-status-unknown", jobId: parsed.jobId ?? asyncJobIdentity(target, context).id, taskExists: parsed.taskExists === true, task: optionalRecord(parsed.task, "status.task"), @@ -1540,6 +1551,11 @@ function positiveInteger(value: unknown, path: string, max: number): number { return Number(value); } +function positiveSafeInteger(value: unknown, path: string): number { + if (!Number.isSafeInteger(value) || Number(value) <= 0) throw new Error(`${path} 必须是正安全整数`); + return Number(value); +} + function nonNegativeInteger(value: unknown, path: string): number { if (!Number.isSafeInteger(value) || Number(value) < 0) throw new Error(`${path} 必须是非负安全整数`); return Number(value);