























@@ -0,0 +1,260 @@
1+import { spawnSync } from "node:child_process";
2+import fs from "node:fs/promises";
3+import os from "node:os";
4+import path from "node:path";
5+import { SessionManager } from "@mariozechner/pi-coding-agent";
6+import {
7+queueRuntimeContextForNextTurn,
8+resolveRuntimeContextPromptParts,
9+} from "../../src/agents/pi-embedded-runner/run/runtime-context-prompt.js";
10+11+type TranscriptEntry = {
12+type?: string;
13+customType?: string;
14+content?: string;
15+display?: boolean;
16+message?: {
17+role?: string;
18+content?: unknown;
19+};
20+};
21+22+function assert(condition: unknown, message: string): asserts condition {
23+if (!condition) {
24+throw new Error(message);
25+}
26+}
27+28+async function readJsonl(filePath: string): Promise<TranscriptEntry[]> {
29+const raw = await fs.readFile(filePath, "utf-8");
30+return raw
31+.split(/\r?\n/)
32+.filter(Boolean)
33+.map((line) => JSON.parse(line) as TranscriptEntry);
34+}
35+36+function messageText(content: unknown): string {
37+if (typeof content === "string") {
38+return content;
39+}
40+if (!Array.isArray(content)) {
41+return "";
42+}
43+return content
44+.map((part) =>
45+part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string"
46+ ? (part as { text: string }).text
47+ : "",
48+)
49+.join("");
50+}
51+52+async function verifyRuntimeContextTranscriptShape(root: string) {
53+const sessionFile = path.join(root, ".openclaw", "agents", "main", "sessions", "runtime.jsonl");
54+await fs.mkdir(path.dirname(sessionFile), { recursive: true });
55+const sessionManager = SessionManager.open(sessionFile);
56+const effectivePrompt = [
57+"visible ask",
58+"",
59+"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
60+"secret docker context",
61+"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
62+].join("\n");
63+const promptSubmission = resolveRuntimeContextPromptParts({
64+ effectivePrompt,
65+transcriptPrompt: "visible ask",
66+});
67+68+assert(promptSubmission.prompt === "visible ask", "visible prompt was not preserved");
69+assert(
70+promptSubmission.runtimeContext?.includes("secret docker context"),
71+"runtime context was not extracted",
72+);
73+74+await queueRuntimeContextForNextTurn({
75+runtimeContext: promptSubmission.runtimeContext,
76+session: {
77+sendCustomMessage: async (message, options) => {
78+assert(options?.deliverAs === "nextTurn", "runtime context was not queued for next turn");
79+sessionManager.appendCustomMessageEntry(
80+message.customType,
81+message.content,
82+message.display,
83+message.details,
84+);
85+},
86+},
87+});
88+sessionManager.appendMessage({
89+role: "user",
90+content: promptSubmission.prompt,
91+timestamp: Date.now(),
92+});
93+sessionManager.appendMessage({
94+role: "assistant",
95+content: "done",
96+timestamp: Date.now() + 1,
97+});
98+99+const entries = await readJsonl(sessionFile);
100+const customEntry = entries.find((entry) => entry.type === "custom_message");
101+assert(customEntry, "hidden runtime custom message was not persisted");
102+assert(customEntry.customType === "openclaw.runtime-context", "unexpected custom message type");
103+assert(customEntry.display === false, "runtime custom message should be hidden");
104+assert(
105+customEntry.content?.includes("secret docker context"),
106+"runtime custom message lost context",
107+);
108+109+const userEntries = entries.filter((entry) => entry.message?.role === "user");
110+assert(userEntries.length === 1, `expected one visible user message, got ${userEntries.length}`);
111+const userText = messageText(userEntries[0]?.message?.content);
112+assert(userText === "visible ask", `unexpected visible user text: ${JSON.stringify(userText)}`);
113+assert(
114+!userText.includes("OPENCLAW_INTERNAL_CONTEXT") && !userText.includes("secret docker context"),
115+"visible user transcript leaked runtime context",
116+);
117+}
118+119+async function seedBrokenSession(stateDir: string): Promise<string> {
120+const sessionsDir = path.join(stateDir, "agents", "main", "sessions");
121+const sessionFile = path.join(sessionsDir, "broken.jsonl");
122+await fs.mkdir(sessionsDir, { recursive: true });
123+const entries = [
124+{ type: "session", version: 3, id: "broken-session" },
125+{
126+type: "message",
127+id: "parent",
128+parentId: null,
129+message: { role: "assistant", content: "previous" },
130+},
131+{
132+type: "message",
133+id: "runtime-user",
134+parentId: "parent",
135+message: {
136+role: "user",
137+content: [
138+"visible ask",
139+"",
140+"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
141+"secret doctor context",
142+"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
143+].join("\n"),
144+},
145+},
146+{
147+type: "message",
148+id: "runtime-assistant",
149+parentId: "runtime-user",
150+message: { role: "assistant", content: "stale branch" },
151+},
152+{
153+type: "message",
154+id: "plain-user",
155+parentId: "parent",
156+message: { role: "user", content: "visible ask" },
157+},
158+{
159+type: "message",
160+id: "plain-assistant",
161+parentId: "plain-user",
162+message: { role: "assistant", content: "active answer" },
163+},
164+];
165+await fs.writeFile(
166+sessionFile,
167+`${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
168+"utf-8",
169+);
170+await fs.writeFile(
171+path.join(sessionsDir, "sessions.json"),
172+JSON.stringify(
173+{
174+"agent:main:qa:docker-runtime-context": {
175+sessionId: "broken",
176+sessionFile: "broken.jsonl",
177+updatedAt: Date.now(),
178+displayName: "Docker runtime context repair",
179+},
180+},
181+null,
182+2,
183+),
184+"utf-8",
185+);
186+return sessionFile;
187+}
188+189+async function verifyDoctorRepair(root: string) {
190+const stateDir = path.join(root, ".openclaw");
191+const configPath = path.join(stateDir, "openclaw.json");
192+const sessionFile = await seedBrokenSession(stateDir);
193+await fs.mkdir(path.dirname(configPath), { recursive: true });
194+await fs.writeFile(configPath, JSON.stringify({ plugins: { enabled: false } }, null, 2));
195+196+const entry = await fs.stat("dist/index.mjs").then(
197+() => "dist/index.mjs",
198+() => "dist/index.js",
199+);
200+const result = spawnSync(process.execPath, [entry, "doctor", "--fix", "--yes", "--force"], {
201+cwd: process.cwd(),
202+env: {
203+ ...process.env,
204+HOME: root,
205+OPENCLAW_CONFIG_PATH: configPath,
206+OPENCLAW_DISABLE_BONJOUR: "1",
207+OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
208+OPENCLAW_NO_ONBOARD: "1",
209+OPENCLAW_STATE_DIR: stateDir,
210+OPENCLAW_SKIP_CANVAS_HOST: "1",
211+OPENCLAW_SKIP_CHANNELS: "1",
212+OPENCLAW_SKIP_CRON: "1",
213+OPENCLAW_SKIP_GMAIL_WATCHER: "1",
214+},
215+encoding: "utf-8",
216+timeout: 120_000,
217+});
218+219+assert(
220+result.status === 0,
221+`doctor --fix failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
222+);
223+const entries = await readJsonl(sessionFile);
224+const ids = entries.map((entry) => (entry as { id?: string }).id).filter(Boolean);
225+assert(
226+JSON.stringify(ids) ===
227+JSON.stringify(["broken-session", "parent", "plain-user", "plain-assistant"]),
228+`doctor kept wrong active branch: ${JSON.stringify(ids)}`,
229+);
230+assert(
231+entries.every(
232+(entry) => !messageText(entry.message?.content).includes("secret doctor context"),
233+),
234+"doctor repair left runtime context in active transcript",
235+);
236+const backups = (await fs.readdir(path.dirname(sessionFile))).filter((name) =>
237+name.includes(".pre-doctor-branch-repair-"),
238+);
239+assert(backups.length === 1, `expected one doctor backup, got ${backups.length}`);
240+}
241+242+async function main() {
243+const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-runtime-context-"));
244+process.env.HOME = root;
245+process.env.OPENCLAW_STATE_DIR = path.join(root, ".openclaw");
246+process.env.OPENCLAW_CONFIG_PATH = path.join(process.env.OPENCLAW_STATE_DIR, "openclaw.json");
247+try {
248+await verifyRuntimeContextTranscriptShape(root);
249+await verifyDoctorRepair(root);
250+console.log("session runtime context Docker E2E passed");
251+} finally {
252+if (process.env.OPENCLAW_SESSION_RUNTIME_CONTEXT_KEEP_ARTIFACTS !== "1") {
253+await fs.rm(root, { recursive: true, force: true });
254+} else {
255+console.error(`kept artifacts: ${root}`);
256+}
257+}
258+}
259+260+await main();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。