fix: 修正 warm session 终态诊断
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { compactDiagnoseCodeAgentResult } from "./apply-status-scripts";
|
||||
import { diagnoseCodeAgentScript } from "./diagnose-code-agent-script";
|
||||
import { renderDiagnoseCodeAgentTable } from "./render";
|
||||
import type { DiagnoseCodeAgentOptions, ObservabilityConfig, ObservabilityTarget } from "./types";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) rmSync(directory, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
describe("diagnose-code-agent warm-session terminal classification", () => {
|
||||
test("uses the provider-decision lane and scopes run events by commandId", () => {
|
||||
const diagnosis = runFixture({ authorityAvailable: true, terminalSpan: false });
|
||||
expect(diagnosis.status).toBe(0);
|
||||
expect(diagnosis.payload.agentrun).toMatchObject({
|
||||
terminalStatus: "completed",
|
||||
terminalSource: "agentrun-authority",
|
||||
runnerProviderClassification: "completed",
|
||||
authority: {
|
||||
namespace: "agentrun-v02",
|
||||
namespaceSource: "provider-decision",
|
||||
providerDecisionNamespace: "agentrun-v02",
|
||||
ok: true,
|
||||
terminalStatus: "completed",
|
||||
terminalEventSeq: 10,
|
||||
eventCount: 2,
|
||||
scopedEventCount: 1,
|
||||
},
|
||||
});
|
||||
expect(diagnosis.payload.observabilityGap).toMatchObject({
|
||||
status: "missing-command-terminal-span",
|
||||
complete: false,
|
||||
serviceCoverageComplete: true,
|
||||
terminalSpanSeen: false,
|
||||
authorityQuery: { namespace: "agentrun-v02", namespaceSource: "provider-decision", ok: true },
|
||||
});
|
||||
expect(diagnosis.calls).toContain("-n agentrun-v02 exec deploy/agentrun-mgr");
|
||||
expect(diagnosis.calls).not.toContain("agentrun-v01");
|
||||
|
||||
const compact = compactDiagnoseCodeAgentResult(diagnosis.payload);
|
||||
expect((compact.agentrun as Record<string, any>).authority).toMatchObject({
|
||||
namespace: "agentrun-v02",
|
||||
namespaceSource: "provider-decision",
|
||||
scopedEventCount: 1,
|
||||
});
|
||||
const rendered = renderDiagnoseCodeAgentTable({
|
||||
ok: true,
|
||||
target,
|
||||
options,
|
||||
query: {},
|
||||
result: compact,
|
||||
});
|
||||
expect(rendered.renderedText).toContain("authorityQuery namespace=agentrun-v02 source=provider-decision ok=true commandState=completed terminal=completed");
|
||||
});
|
||||
|
||||
test("reports unknown instead of running when command authority and terminal span are both absent", () => {
|
||||
const diagnosis = runFixture({ authorityAvailable: false, terminalSpan: false });
|
||||
expect(diagnosis.status).toBe(0);
|
||||
expect(diagnosis.payload.agentrun).toMatchObject({
|
||||
terminalStatus: null,
|
||||
runnerProviderClassification: "unknown",
|
||||
authority: { namespace: "agentrun-v02", namespaceSource: "provider-decision", ok: false },
|
||||
});
|
||||
expect(diagnosis.payload.observabilityGap).toMatchObject({
|
||||
status: "authority-query-failed",
|
||||
complete: false,
|
||||
terminalSpanSeen: false,
|
||||
});
|
||||
expect(diagnosis.payload.rootCauseCandidates[0].code).toBe("observability_gap_authority_query_failed");
|
||||
});
|
||||
|
||||
test("keeps a scoped OTel terminal span complete even if the supplemental authority read fails", () => {
|
||||
const diagnosis = runFixture({ authorityAvailable: false, terminalSpan: true });
|
||||
expect(diagnosis.status).toBe(0);
|
||||
expect(diagnosis.payload.agentrun).toMatchObject({
|
||||
terminalStatus: "completed",
|
||||
terminalSource: "otel-span",
|
||||
runnerProviderClassification: "completed",
|
||||
});
|
||||
expect(diagnosis.payload.observabilityGap).toMatchObject({
|
||||
status: "complete",
|
||||
complete: true,
|
||||
terminalSpanSeen: true,
|
||||
terminalSpanCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function runFixture(input: { authorityAvailable: boolean; terminalSpan: boolean }): {
|
||||
status: number | null;
|
||||
payload: Record<string, any>;
|
||||
calls: string;
|
||||
} {
|
||||
const directory = mkdtempSync(join(tmpdir(), "unidesk-otel-diagnose-"));
|
||||
temporaryDirectories.push(directory);
|
||||
writeFileSync(join(directory, "trace.json"), JSON.stringify(traceFixture(input.terminalSpan)));
|
||||
writeFileSync(join(directory, "command.json"), JSON.stringify({ id: "cmd_second", state: "completed" }));
|
||||
writeFileSync(join(directory, "result.json"), JSON.stringify({ ok: true, commandId: "cmd_second", status: "completed", commandState: "completed", terminalStatus: "completed" }));
|
||||
writeFileSync(join(directory, "events.json"), JSON.stringify({
|
||||
items: [
|
||||
{ seq: 10, type: "terminal_status", payload: { commandId: "cmd_second", terminalStatus: "completed" } },
|
||||
{ seq: 11, type: "terminal_status", payload: { commandId: "cmd_first", terminalStatus: "failed", failureKind: "backend-timeout" } },
|
||||
],
|
||||
}));
|
||||
const kubectl = join(directory, "kubectl");
|
||||
writeFileSync(kubectl, `#!/bin/sh
|
||||
set -u
|
||||
echo "$*" >>"$FIXTURE_DIR/calls.log"
|
||||
case "$*" in
|
||||
*"/api/traces/"*) cat "$FIXTURE_DIR/trace.json"; exit 0 ;;
|
||||
esac
|
||||
if [ "$AUTHORITY_AVAILABLE" != "1" ]; then
|
||||
echo "fixture authority unavailable" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$*" in
|
||||
*"-n agentrun-v02 exec deploy/agentrun-mgr"*"/commands/cmd_second/result"*) cat "$FIXTURE_DIR/result.json"; exit 0 ;;
|
||||
*"-n agentrun-v02 exec deploy/agentrun-mgr"*"/commands/cmd_second"*) cat "$FIXTURE_DIR/command.json"; exit 0 ;;
|
||||
*"-n agentrun-v02 exec deploy/agentrun-mgr"*"/runs/run_warm/events"*) cat "$FIXTURE_DIR/events.json"; exit 0 ;;
|
||||
esac
|
||||
echo "unexpected kubectl call: $*" >&2
|
||||
exit 1
|
||||
`);
|
||||
chmodSync(kubectl, 0o755);
|
||||
const script = diagnoseCodeAgentScript(observability, target, null, options);
|
||||
const result = spawnSync("bash", ["-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
AUTHORITY_AVAILABLE: input.authorityAvailable ? "1" : "0",
|
||||
FIXTURE_DIR: directory,
|
||||
PATH: `${directory}:${process.env.PATH ?? ""}`,
|
||||
},
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
expect(result.stdout).not.toBe("");
|
||||
return {
|
||||
status: result.status,
|
||||
payload: JSON.parse(result.stdout),
|
||||
calls: readFileSync(join(directory, "calls.log"), "utf8"),
|
||||
};
|
||||
}
|
||||
|
||||
function traceFixture(terminalSpan: boolean): Record<string, unknown> {
|
||||
const common = [
|
||||
attribute("traceId", "trc_warm_second"),
|
||||
attribute("runId", "run_warm"),
|
||||
attribute("commandId", "cmd_second"),
|
||||
attribute("sessionId", "ses_warm"),
|
||||
];
|
||||
return {
|
||||
batches: [
|
||||
batch("hwlab-cloud-api", [span("provider_decision", [
|
||||
...common,
|
||||
attribute("agent.chat.provider_profile", "gpt.pika"),
|
||||
attribute("agentRunRunnerNamespace", "agentrun-v02"),
|
||||
])]),
|
||||
batch("agentrun-manager", [
|
||||
span("command_created", common),
|
||||
...(terminalSpan ? [span("runner_terminal.completed", [...common, attribute("terminalStatus", "completed"), attribute("eventType", "terminal_status")])] : []),
|
||||
]),
|
||||
batch("agentrun-runner", [span("codex_stdio.notification", common)]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function batch(service: string, spans: Array<Record<string, unknown>>): Record<string, unknown> {
|
||||
return {
|
||||
resource: { attributes: [attribute("service.name", service)] },
|
||||
scopeSpans: [{ scope: { name: `${service}.fixture` }, spans }],
|
||||
};
|
||||
}
|
||||
|
||||
function span(name: string, attributes: Array<Record<string, unknown>>): Record<string, unknown> {
|
||||
return { name, startTimeUnixNano: "1000000", endTimeUnixNano: "2000000", attributes };
|
||||
}
|
||||
|
||||
function attribute(key: string, value: string): Record<string, unknown> {
|
||||
return { key, value: { stringValue: value } };
|
||||
}
|
||||
|
||||
const target: ObservabilityTarget = {
|
||||
id: "NC01",
|
||||
route: "NC01:k3s",
|
||||
namespace: "devops-infra",
|
||||
role: "active",
|
||||
enabled: true,
|
||||
createNamespace: false,
|
||||
};
|
||||
|
||||
const options: DiagnoseCodeAgentOptions = {
|
||||
targetId: "NC01",
|
||||
businessTraceId: "trc_warm_second",
|
||||
traceId: "de11d47d1bab0ca4d3f6259279f3a332",
|
||||
runId: null,
|
||||
commandId: null,
|
||||
sessionId: null,
|
||||
runnerJobId: null,
|
||||
full: false,
|
||||
raw: false,
|
||||
limit: 40,
|
||||
candidateLimit: 20,
|
||||
lookbackMinutes: 60,
|
||||
};
|
||||
|
||||
const observability = {
|
||||
traceBackend: { serviceName: "tempo" },
|
||||
probes: { traceQueryPathTemplate: "/api/traces/{{traceId}}" },
|
||||
} as ObservabilityConfig;
|
||||
Reference in New Issue
Block a user