Merge pull request #1897 from pikasTech/fix/issue-1888-migration-envelope

修复迁移 Job 失败 envelope 解包丢失 command
This commit is contained in:
Lyon
2026-07-13 12:46:15 +08:00
committed by GitHub
2 changed files with 26 additions and 5 deletions
@@ -333,6 +333,16 @@ describe("migrated CLI 执行 guard 与提示清理", () => {
expect(failure.projection?.result).toBeUndefined();
});
test("migration Job 解包 pretty JSON outer command 和 nested data.error", () => {
const importFailure = JSON.stringify({ ok: false, command: "web-probe sentinel migration import --node NC01 --lane v03 --json", data: { ok: false, error: { code: "payload_hash_conflict", message: "payload hash conflicts with the existing import" } } }, null, 2);
const probe = { failed: true, podPhase: "Failed", containers: [{ name: "import", phase: "terminated", exitCode: 1, reason: "Error", message: "-" }], containerLogs: { import: `import prefix\n${importFailure}` }, logsTail: importFailure };
const payload = migrationJobPayloadFromProbe(probe);
expect(payload).toMatchObject({ ok: false, command: "web-probe sentinel migration import --node NC01 --lane v03 --json", error: { code: "payload_hash_conflict", message: "payload hash conflicts with the existing import" } });
const failure = migrationJobFailed("migration", { jobName: "job" }, "failed", payload, probe) as { projection?: Record<string, unknown> };
expect(failure.projection?.failure).toMatchObject({ container: "import", code: "payload_hash_conflict" });
expect(String((failure.projection?.failure as Record<string, unknown>)?.message)).toStartWith("payload hash conflicts with the existin");
});
test("migration Job 成功固定读取 verify,缺结构化 verify 结果显式失败", () => {
const importSuccess = JSON.stringify({ ok: true, data: { ok: true, command: "web-probe sentinel migration import" } }, null, 2);
expect(migrationJobPayloadFromProbe({ succeeded: true, containers: [{ name: "verify", phase: "terminated", exitCode: 0 }], containerLogs: { import: importSuccess }, logsTail: importSuccess })).toMatchObject({ ok: false, error: { code: "monitor-migration-job-result-missing" } });
@@ -983,19 +983,27 @@ export function sentinelPayloadFromLogs(logsTail: string): Record<string, unknow
const parsed = lastJsonObjectFromText(logsTail);
if (parsed === null) return {};
const data = isRecord(parsed.data) ? parsed.data : null;
return data !== null && (data.ok === true || data.ok === false) ? data : parsed.ok === true || parsed.ok === false ? parsed : {};
if (data !== null && (data.ok === true || data.ok === false)) {
return data.command === undefined && typeof parsed.command === "string" ? { ...data, command: parsed.command } : data;
}
return parsed.ok === true || parsed.ok === false ? parsed : {};
}
function lastJsonObjectFromText(text: string): Record<string, unknown> | null {
let last: Record<string, unknown> | null = null;
for (let start = text.indexOf("{"); start >= 0; start = text.indexOf("{", start + 1)) {
for (let start = text.indexOf("{"); start >= 0;) {
const candidate = jsonObjectAt(text, start);
if (candidate !== null && (candidate.ok === true || candidate.ok === false)) last = candidate;
if (candidate === null) {
start = text.indexOf("{", start + 1);
continue;
}
if (candidate.value.ok === true || candidate.value.ok === false) last = candidate.value;
start = text.indexOf("{", candidate.end + 1);
}
return last;
}
function jsonObjectAt(text: string, start: number): Record<string, unknown> | null {
function jsonObjectAt(text: string, start: number): { value: Record<string, unknown>; end: number } | null {
let depth = 0;
let inString = false;
let escaped = false;
@@ -1009,7 +1017,10 @@ function jsonObjectAt(text: string, start: number): Record<string, unknown> | nu
}
if (char === "\"") { inString = true; continue; }
if (char === "{") { depth += 1; continue; }
if (char === "}" && --depth === 0) return parseJsonObject(text.slice(start, index + 1));
if (char === "}" && --depth === 0) {
const value = parseJsonObject(text.slice(start, index + 1));
return value === null ? null : { value, end: index };
}
}
return null;
}