




























@@ -216,6 +216,50 @@ function startedSpanOptions(name: string) {
216216return startedSpanCall(name)?.[1];
217217}
218218219+function mockCall(mock: { mock: { calls: unknown[][] } }, callIndex = 0): unknown[] {
220+const call = mock.mock.calls.at(callIndex);
221+if (!call) {
222+throw new Error(`Expected mock call at index ${callIndex}`);
223+}
224+return call;
225+}
226+227+function mockCallArg(mock: { mock: { calls: unknown[][] } }, argIndex: number, callIndex = 0) {
228+return mockCall(mock, callIndex)[argIndex];
229+}
230+231+function firstExporterOptions(mock: { mock: { calls: unknown[][] } }): { url?: string } {
232+return mockCallArg(mock, 0) as { url?: string };
233+}
234+235+function firstSetSpanContext(): Record<string, unknown> {
236+return mockCallArg(telemetryState.tracer.setSpanContext, 1) as Record<string, unknown>;
237+}
238+239+function spanByName(name: string): (typeof telemetryState.spans)[number] {
240+const span = telemetryState.spans.find((candidate) => candidate.name === name);
241+if (!span) {
242+throw new Error(`Expected span ${name}`);
243+}
244+return span;
245+}
246+247+function firstSpanAttributes(name: string): Record<string, unknown> {
248+return mockCallArg(spanByName(name).setAttributes, 0) as Record<string, unknown>;
249+}
250+251+function firstSpanEndTime(name: string): unknown {
252+return mockCallArg(spanByName(name).end, 0);
253+}
254+255+function firstCounterAddCall(name: string): [unknown, Record<string, unknown>?] {
256+const counter = telemetryState.counters.get(name);
257+if (!counter) {
258+throw new Error(`Expected counter ${name}`);
259+}
260+return mockCall(counter.add) as [unknown, Record<string, unknown>?];
261+}
262+219263function lastHistogramRecord(name: string) {
220264return telemetryState.histograms.get(name)?.record.mock.calls.at(-1) as
221265| [unknown, Record<string, unknown>?]
@@ -246,7 +290,11 @@ async function emitAndCaptureLog(
246290});
247291await flushDiagnosticEvents();
248292expect(logEmit).toHaveBeenCalled();
249-const emitCall = logEmit.mock.calls.at(0)?.[0];
293+const emitCall = mockCallArg(logEmit, 0) as {
294+attributes?: Record<string, unknown>;
295+body?: string;
296+context?: unknown;
297+};
250298await service.stop?.(ctx);
251299return emitCall;
252300}
@@ -753,8 +801,8 @@ describe("diagnostics-otel service", () => {
753801const ctx = createTraceOnlyContext("https://www.comet.com/opik/api/v1/private/otel");
754802await service.start(ctx);
755803756-const options = traceExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
757-expect(options?.url).toBe("https://www.comet.com/opik/api/v1/private/otel/v1/traces");
804+const options = firstExporterOptions(traceExporterCtor);
805+expect(options.url).toBe("https://www.comet.com/opik/api/v1/private/otel/v1/traces");
758806await service.stop?.(ctx);
759807});
760808@@ -763,8 +811,8 @@ describe("diagnostics-otel service", () => {
763811const ctx = createTraceOnlyContext("https://collector.example.com/v1/traces");
764812await service.start(ctx);
765813766-const options = traceExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
767-expect(options?.url).toBe("https://collector.example.com/v1/traces");
814+const options = firstExporterOptions(traceExporterCtor);
815+expect(options.url).toBe("https://collector.example.com/v1/traces");
768816await service.stop?.(ctx);
769817});
770818@@ -773,8 +821,8 @@ describe("diagnostics-otel service", () => {
773821const ctx = createTraceOnlyContext("https://collector.example.com/v1/traces?timeout=30s");
774822await service.start(ctx);
775823776-const options = traceExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
777-expect(options?.url).toBe("https://collector.example.com/v1/traces?timeout=30s");
824+const options = firstExporterOptions(traceExporterCtor);
825+expect(options.url).toBe("https://collector.example.com/v1/traces?timeout=30s");
778826await service.stop?.(ctx);
779827});
780828@@ -783,8 +831,8 @@ describe("diagnostics-otel service", () => {
783831const ctx = createTraceOnlyContext("https://collector.example.com/v1/Traces");
784832await service.start(ctx);
785833786-const options = traceExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
787-expect(options?.url).toBe("https://collector.example.com/v1/Traces");
834+const options = firstExporterOptions(traceExporterCtor);
835+expect(options.url).toBe("https://collector.example.com/v1/Traces");
788836await service.stop?.(ctx);
789837});
790838@@ -801,12 +849,12 @@ describe("diagnostics-otel service", () => {
801849802850await service.start(ctx);
803851804-const traceOptions = traceExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
805-const metricOptions = metricExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
806-const logOptions = logExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
807-expect(traceOptions?.url).toBe("https://trace.example.com/otlp/v1/traces");
808-expect(metricOptions?.url).toBe("https://metric.example.com/v1/metrics");
809-expect(logOptions?.url).toBe("https://log.example.com/otlp/v1/logs");
852+const traceOptions = firstExporterOptions(traceExporterCtor);
853+const metricOptions = firstExporterOptions(metricExporterCtor);
854+const logOptions = firstExporterOptions(logExporterCtor);
855+expect(traceOptions.url).toBe("https://trace.example.com/otlp/v1/traces");
856+expect(metricOptions.url).toBe("https://metric.example.com/v1/metrics");
857+expect(logOptions.url).toBe("https://log.example.com/otlp/v1/logs");
810858await service.stop?.(ctx);
811859});
812860@@ -823,12 +871,12 @@ describe("diagnostics-otel service", () => {
823871});
824872await service.start(ctx);
825873826-const traceOptions = traceExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
827-const metricOptions = metricExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
828-const logOptions = logExporterCtor.mock.calls.at(0)?.[0] as { url?: string } | undefined;
829-expect(traceOptions?.url).toBe("https://trace-env.example.com/v1/traces");
830-expect(metricOptions?.url).toBe("https://metric-env.example.com/otlp/v1/metrics");
831-expect(logOptions?.url).toBe("https://log-env.example.com/otlp/v1/logs");
874+const traceOptions = firstExporterOptions(traceExporterCtor);
875+const metricOptions = firstExporterOptions(metricExporterCtor);
876+const logOptions = firstExporterOptions(logExporterCtor);
877+expect(traceOptions.url).toBe("https://trace-env.example.com/v1/traces");
878+expect(metricOptions.url).toBe("https://metric-env.example.com/otlp/v1/metrics");
879+expect(logOptions.url).toBe("https://log-env.example.com/otlp/v1/logs");
832880await service.stop?.(ctx);
833881});
834882@@ -895,13 +943,11 @@ describe("diagnostics-otel service", () => {
895943);
896944897945expect(telemetryState.tracer.setSpanContext).toHaveBeenCalledTimes(1);
898-const trustedSpanContext = telemetryState.tracer.setSpanContext.mock.calls.at(0)?.[1] as
899-| Record<string, unknown>
900-| undefined;
901-expect(trustedSpanContext?.traceId).toBe(TRACE_ID);
902-expect(trustedSpanContext?.spanId).toBe(SPAN_ID);
903-expect(trustedSpanContext?.traceFlags).toBe(1);
904-expect(trustedSpanContext?.isRemote).toBe(true);
946+const trustedSpanContext = firstSetSpanContext();
947+expect(trustedSpanContext.traceId).toBe(TRACE_ID);
948+expect(trustedSpanContext.spanId).toBe(SPAN_ID);
949+expect(trustedSpanContext.traceFlags).toBe(1);
950+expect(trustedSpanContext.isRemote).toBe(true);
905951const emitContext = emitCall?.context as { spanContext?: Record<string, unknown> } | undefined;
906952const emitSpanContext = emitContext?.spanContext;
907953expect(emitSpanContext?.traceId).toBe(TRACE_ID);
@@ -935,24 +981,27 @@ describe("diagnostics-otel service", () => {
935981} as Parameters<typeof emitDiagnosticEvent>[0]);
936982await flushDiagnosticEvents();
937983938-const emitCall = logEmit.mock.calls.at(0)?.[0];
939-expect(emitCall?.body.length).toBeLessThanOrEqual(4200);
940-expect(String(emitCall?.attributes?.["openclaw.good"])).toMatch(/^y+/);
941-expect(emitCall?.attributes?.["code.lineno"]).toBe(42);
942-expect(emitCall?.attributes?.["code.function"]).toBe("handler");
943-expect(String(emitCall?.attributes?.["openclaw.good"]).length).toBeLessThanOrEqual(4200);
944-expect(Object.hasOwn(emitCall?.attributes ?? {}, `openclaw.${PROTO_KEY}`)).toBe(false);
945-expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.constructor")).toBe(false);
946-expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.prototype")).toBe(false);
984+const emitCall = mockCallArg(logEmit, 0) as {
985+attributes: Record<string, unknown>;
986+body: string;
987+};
988+expect(emitCall.body.length).toBeLessThanOrEqual(4200);
989+expect(String(emitCall.attributes["openclaw.good"])).toMatch(/^y+/);
990+expect(emitCall.attributes["code.lineno"]).toBe(42);
991+expect(emitCall.attributes["code.function"]).toBe("handler");
992+expect(String(emitCall.attributes["openclaw.good"]).length).toBeLessThanOrEqual(4200);
993+expect(Object.hasOwn(emitCall.attributes, `openclaw.${PROTO_KEY}`)).toBe(false);
994+expect(Object.hasOwn(emitCall.attributes, "openclaw.constructor")).toBe(false);
995+expect(Object.hasOwn(emitCall.attributes, "openclaw.prototype")).toBe(false);
947996expect(
948997Object.hasOwn(
949-emitCall?.attributes ?? {},
998+emitCall.attributes,
950999"openclaw.sk-1234567890abcdef1234567890abcdef", // pragma: allowlist secret
9511000),
9521001).toBe(false);
953-expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.bad key")).toBe(false);
954-expect(Object.hasOwn(emitCall?.attributes ?? {}, "code.filepath")).toBe(false);
955-expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.code.location")).toBe(false);
1002+expect(Object.hasOwn(emitCall.attributes, "openclaw.bad key")).toBe(false);
1003+expect(Object.hasOwn(emitCall.attributes, "code.filepath")).toBe(false);
1004+expect(Object.hasOwn(emitCall.attributes, "openclaw.code.location")).toBe(false);
9561005await service.stop?.(ctx);
9571006});
9581007@@ -1384,13 +1433,10 @@ describe("diagnostics-otel service", () => {
13841433expect(timeToFirstByte?.[0]).toBe(45);
13851434expect(timeToFirstByte?.[1]?.["openclaw.provider"]).toBe("openai");
13861435expect(timeToFirstByte?.[1]?.["openclaw.model"]).toBe("gpt-5.4");
1387-const modelCallSpan = telemetryState.spans.find((span) => span.name === "openclaw.model.call");
1388-const modelSpanAttributes = modelCallSpan?.setAttributes.mock.calls.at(0)?.[0] as
1389-| Record<string, unknown>
1390-| undefined;
1391-expect(modelSpanAttributes?.["openclaw.model_call.request_bytes"]).toBe(1234);
1392-expect(modelSpanAttributes?.["openclaw.model_call.response_bytes"]).toBe(567);
1393-expect(modelSpanAttributes?.["openclaw.model_call.time_to_first_byte_ms"]).toBe(45);
1436+const modelSpanAttributes = firstSpanAttributes("openclaw.model.call");
1437+expect(modelSpanAttributes["openclaw.model_call.request_bytes"]).toBe(1234);
1438+expect(modelSpanAttributes["openclaw.model_call.response_bytes"]).toBe(567);
1439+expect(modelSpanAttributes["openclaw.model_call.time_to_first_byte_ms"]).toBe(45);
13941440const runDuration = lastHistogramRecord("openclaw.run.duration_ms");
13951441expect(runDuration?.[0]).toBe(100);
13961442expect(Object.hasOwn(runDuration?.[1] ?? {}, "openclaw.runId")).toBe(false);
@@ -1406,12 +1452,12 @@ describe("diagnostics-otel service", () => {
14061452expect(Object.hasOwn(toolDuration?.[1] ?? {}, "openclaw.errorCode")).toBe(false);
14071453expect(Object.hasOwn(toolDuration?.[1] ?? {}, "openclaw.runId")).toBe(false);
140814541409-const toolSpan = telemetryState.spans.find((span) => span.name === "openclaw.tool.execution");
1455+const toolSpan = spanByName("openclaw.tool.execution");
14101456expect(toolSpan?.setStatus).toHaveBeenCalledWith({
14111457code: 2,
14121458message: "TypeError",
14131459});
1414-expect(toolSpan?.end.mock.calls.at(0)?.[0]).toBeTypeOf("number");
1460+expect(firstSpanEndTime("openclaw.tool.execution")).toBeTypeOf("number");
14151461expect(telemetryState.tracer.setSpanContext).not.toHaveBeenCalled();
14161462await service.stop?.(ctx);
14171463});
@@ -1447,10 +1493,7 @@ describe("diagnostics-otel service", () => {
14471493expect(Object.hasOwn(failoverOptions?.attributes ?? {}, "openclaw.sessionId")).toBe(false);
14481494expect(Object.hasOwn(failoverOptions?.attributes ?? {}, "openclaw.sessionKey")).toBe(false);
14491495expect(failoverOptions?.startTime).toBeTypeOf("number");
1450-const span = telemetryState.spans.find(
1451-(candidate) => candidate.name === "openclaw.model.failover",
1452-);
1453-expect(span?.end.mock.calls.at(0)?.[0]).toBeTypeOf("number");
1496+expect(firstSpanEndTime("openclaw.model.failover")).toBeTypeOf("number");
14541497await service.stop?.(ctx);
14551498});
14561499@@ -1648,11 +1691,9 @@ describe("diagnostics-otel service", () => {
16481691expect(contextOptions?.startTime).toBeTypeOf("number");
16491692expect(JSON.stringify(contextCall)).not.toContain("session-key");
16501693expect(JSON.stringify(contextCall)).not.toContain("prompt text");
1651-const linkedSpanContext = telemetryState.tracer.setSpanContext.mock.calls.at(0)?.[1] as
1652-| Record<string, unknown>
1653-| undefined;
1654-expect(linkedSpanContext?.traceId).toBe(TRACE_ID);
1655-expect(linkedSpanContext?.spanId).toBe(runSpanId);
1694+const linkedSpanContext = firstSetSpanContext();
1695+expect(linkedSpanContext.traceId).toBe(TRACE_ID);
1696+expect(linkedSpanContext.spanId).toBe(runSpanId);
16561697expect(
16571698(contextCall?.[2] as { spanContext?: { spanId?: string } } | undefined)?.spanContext?.spanId,
16581699).toBe(runSpanId);
@@ -1942,16 +1983,14 @@ describe("diagnostics-otel service", () => {
19421983(call) => call[0] === "openclaw.model.usage",
19431984);
194419851945-const linkedSpanContext = telemetryState.tracer.setSpanContext.mock.calls.at(0)?.[1] as
1946-| Record<string, unknown>
1947-| undefined;
1948-expect(linkedSpanContext?.traceId).toBe(TRACE_ID);
1949-expect(linkedSpanContext?.spanId).toBe(runSpanId);
1986+const linkedSpanContext = firstSetSpanContext();
1987+expect(linkedSpanContext.traceId).toBe(TRACE_ID);
1988+expect(linkedSpanContext.spanId).toBe(runSpanId);
19501989expect(
19511990(modelUsageCall?.[2] as { spanContext?: { spanId?: string } } | undefined)?.spanContext
19521991?.spanId,
19531992).toBe(runSpanId);
1954-expect(runSpan?.end.mock.calls.at(0)?.[0]).toBeTypeOf("number");
1993+expect(firstSpanEndTime("openclaw.run")).toBeTypeOf("number");
19551994await service.stop?.(ctx);
19561995});
19571996@@ -2224,12 +2263,12 @@ describe("diagnostics-otel service", () => {
22242263expect(Object.hasOwn(execOptions?.attributes ?? {}, "openclaw.sessionKey")).toBe(false);
22252264expect(execOptions?.startTime).toBeTypeOf("number");
222622652227-const execSpan = telemetryState.spans.find((span) => span.name === "openclaw.exec");
2266+const execSpan = spanByName("openclaw.exec");
22282267expect(execSpan?.setStatus).toHaveBeenCalledWith({
22292268code: 2,
22302269message: "runtime-error",
22312270});
2232-expect(execSpan?.end.mock.calls.at(0)?.[0]).toBeTypeOf("number");
2271+expect(firstSpanEndTime("openclaw.exec")).toBeTypeOf("number");
22332272await service.stop?.(ctx);
22342273});
22352274@@ -2400,20 +2439,16 @@ describe("diagnostics-otel service", () => {
24002439});
24012440await flushDiagnosticEvents();
240224412403-const recoveryRequestedCall = telemetryState.counters
2404-.get("openclaw.session.recovery.requested")
2405-?.add.mock.calls.at(0);
2406-expect(recoveryRequestedCall?.[0]).toBe(1);
2407-expect(recoveryRequestedCall?.[1]?.["openclaw.state"]).toBe("processing");
2408-expect(recoveryRequestedCall?.[1]?.["openclaw.action"]).toBe("abort");
2409-expect(recoveryRequestedCall?.[1]?.["openclaw.active_work_kind"]).toBe("tool_call");
2410-const recoveryCompletedCall = telemetryState.counters
2411-.get("openclaw.session.recovery.completed")
2412-?.add.mock.calls.at(0);
2413-expect(recoveryCompletedCall?.[0]).toBe(1);
2414-expect(recoveryCompletedCall?.[1]?.["openclaw.state"]).toBe("processing");
2415-expect(recoveryCompletedCall?.[1]?.["openclaw.status"]).toBe("released");
2416-expect(recoveryCompletedCall?.[1]?.["openclaw.action"]).toBe("abort-active-run");
2442+const recoveryRequestedCall = firstCounterAddCall("openclaw.session.recovery.requested");
2443+expect(recoveryRequestedCall[0]).toBe(1);
2444+expect(recoveryRequestedCall[1]?.["openclaw.state"]).toBe("processing");
2445+expect(recoveryRequestedCall[1]?.["openclaw.action"]).toBe("abort");
2446+expect(recoveryRequestedCall[1]?.["openclaw.active_work_kind"]).toBe("tool_call");
2447+const recoveryCompletedCall = firstCounterAddCall("openclaw.session.recovery.completed");
2448+expect(recoveryCompletedCall[0]).toBe(1);
2449+expect(recoveryCompletedCall[1]?.["openclaw.state"]).toBe("processing");
2450+expect(recoveryCompletedCall[1]?.["openclaw.status"]).toBe("released");
2451+expect(recoveryCompletedCall[1]?.["openclaw.action"]).toBe("abort-active-run");
24172452const recoveryAgeRecord = lastHistogramRecord("openclaw.session.recovery.age_ms");
24182453expect(recoveryAgeRecord?.[0]).toBe(13_000);
24192454expect(recoveryAgeRecord?.[1]?.["openclaw.status"]).toBe("released");
@@ -2598,9 +2633,9 @@ describe("diagnostics-otel service", () => {
25982633reason: "token=ghp_abcdefghijklmnopqrstuvwxyz123456", // pragma: allowlist secret
25992634});
260026352601-const sessionCounter = telemetryState.counters.get("openclaw.session.state");
2602-const attrs = sessionCounter?.add.mock.calls.at(0)?.[1] as Record<string, unknown> | undefined;
2603-expect(sessionCounter?.add.mock.calls.at(0)?.[0]).toBe(1);
2636+const sessionStateCall = firstCounterAddCall("openclaw.session.state");
2637+const attrs = sessionStateCall[1];
2638+expect(sessionStateCall[0]).toBe(1);
26042639expect(String(attrs?.["openclaw.reason"])).toContain("…");
26052640expect(typeof attrs?.["openclaw.reason"]).toBe("string");
26062641expect(String(attrs?.["openclaw.reason"])).not.toContain(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。