













@@ -1,9 +1,10 @@
1-import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
1+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22import { tmpdir } from "node:os";
33import path from "node:path";
44import { afterEach, describe, expect, it, vi } from "vitest";
55import { loadAuthProfileStoreWithoutExternalProfiles } from "../agents/auth-profiles.js";
66import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.js";
7+import { measureDiagnosticsTimelineSpan } from "../infra/diagnostics-timeline.js";
78import type { PreparedSecretsRuntimeSnapshot, SecretResolverWarning } from "../secrets/runtime.js";
89import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS } from "./known-weak-gateway-secrets.js";
910import {
@@ -115,6 +116,14 @@ function runtimeSecretsActivatorForTest(params: {
115116});
116117}
117118119+function readTimelineEvents(filePath: string): Array<Record<string, unknown>> {
120+return readFileSync(filePath, "utf8")
121+.trim()
122+.split(/\r?\n/u)
123+.filter(Boolean)
124+.map((line) => JSON.parse(line) as Record<string, unknown>);
125+}
126+118127function installGatewayStartupSecretsRuntimeMock(state: GatewayStartupSecretsRuntimeMock) {
119128(
120129globalThis as typeof globalThis & {
@@ -206,6 +215,122 @@ describe("gateway startup config secret preflight", () => {
206215]);
207216});
208217218+it("emits sanitized diagnostics timeline spans for secrets preparation", async () => {
219+const root = mkdtempSync(path.join(tmpdir(), "openclaw-startup-secrets-timeline-"));
220+const timelinePath = path.join(root, "timeline.jsonl");
221+const previousDiagnostics = process.env.OPENCLAW_DIAGNOSTICS;
222+const previousTimelinePath = process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH;
223+process.env.OPENCLAW_DIAGNOSTICS = "timeline";
224+process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = timelinePath;
225+try {
226+const config = gatewaySecretRefSnapshot().config;
227+const prepareRuntimeSecretsSnapshot = vi.fn(async ({ config }) => preparedSnapshot(config));
228+229+const activateRuntimeSecrets = createRuntimeSecretsActivator({
230+logSecrets: {
231+info: vi.fn(),
232+warn: vi.fn(),
233+error: vi.fn(),
234+},
235+emitStateEvent: vi.fn(),
236+ prepareRuntimeSecretsSnapshot,
237+activateRuntimeSecretsSnapshot: vi.fn(),
238+});
239+240+await activateRuntimeSecrets(config, { reason: "startup", activate: false });
241+242+const events = readTimelineEvents(timelinePath);
243+expect(events).toHaveLength(2);
244+expect(events.map((event) => event.type)).toEqual(["span.start", "span.end"]);
245+for (const event of events) {
246+expect(event.name).toBe("secrets.prepare");
247+expect(event.phase).toBe("startup");
248+expect(event.attributes).toEqual({
249+activate: false,
250+gatewayAuthSecretRef: true,
251+reason: "startup",
252+});
253+}
254+expect(JSON.stringify(events)).not.toContain("GATEWAY_TOKEN_REF");
255+} finally {
256+if (previousDiagnostics === undefined) {
257+delete process.env.OPENCLAW_DIAGNOSTICS;
258+} else {
259+process.env.OPENCLAW_DIAGNOSTICS = previousDiagnostics;
260+}
261+if (previousTimelinePath === undefined) {
262+delete process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH;
263+} else {
264+process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = previousTimelinePath;
265+}
266+rmSync(root, { force: true, recursive: true });
267+}
268+});
269+270+it("omits secret preparation error messages from diagnostics timeline spans", async () => {
271+const root = mkdtempSync(path.join(tmpdir(), "openclaw-startup-secrets-timeline-"));
272+const timelinePath = path.join(root, "timeline.jsonl");
273+const previousDiagnostics = process.env.OPENCLAW_DIAGNOSTICS;
274+const previousTimelinePath = process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH;
275+process.env.OPENCLAW_DIAGNOSTICS = "timeline";
276+process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = timelinePath;
277+try {
278+const prepareRuntimeSecretsSnapshot = vi.fn(async () => {
279+throw new Error('Secret provider "default" is not configured for GATEWAY_TOKEN_REF.');
280+});
281+282+const activateRuntimeSecrets = createRuntimeSecretsActivator({
283+logSecrets: {
284+info: vi.fn(),
285+warn: vi.fn(),
286+error: vi.fn(),
287+},
288+emitStateEvent: vi.fn(),
289+ prepareRuntimeSecretsSnapshot,
290+activateRuntimeSecretsSnapshot: vi.fn(),
291+});
292+293+await expect(
294+prepareGatewayStartupConfig({
295+configSnapshot: gatewaySecretRefSnapshot(),
296+ activateRuntimeSecrets,
297+measure: (name, run, options) =>
298+measureDiagnosticsTimelineSpan(name, run, {
299+env: process.env,
300+omitErrorMessage: options?.omitErrorMessage,
301+phase: "startup",
302+}),
303+}),
304+).rejects.toThrow("Startup failed: required secrets are unavailable.");
305+306+const events = readTimelineEvents(timelinePath);
307+const errorEvents = events.filter((event) => event.type === "span.error");
308+expect(errorEvents.map((event) => event.name)).toEqual([
309+"secrets.prepare",
310+"config.auth.secret-preflight",
311+]);
312+for (const event of errorEvents) {
313+expect(event.phase).toBe("startup");
314+expect(event.errorName).toBe("Error");
315+expect(event.errorMessage).toBeUndefined();
316+}
317+expect(JSON.stringify(events)).not.toContain("GATEWAY_TOKEN_REF");
318+expect(JSON.stringify(events)).not.toContain("default");
319+} finally {
320+if (previousDiagnostics === undefined) {
321+delete process.env.OPENCLAW_DIAGNOSTICS;
322+} else {
323+process.env.OPENCLAW_DIAGNOSTICS = previousDiagnostics;
324+}
325+if (previousTimelinePath === undefined) {
326+delete process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH;
327+} else {
328+process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = previousTimelinePath;
329+}
330+rmSync(root, { force: true, recursive: true });
331+}
332+});
333+209334it("wraps startup secret activation failures without emitting reload state events", async () => {
210335const error = new Error('Environment variable "OPENAI_API_KEY" is missing or empty.');
211336const prepareRuntimeSecretsSnapshot = vi.fn(async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。