
























@@ -1,7 +1,8 @@
1+import { createHash } from "node:crypto";
12import fs from "node:fs/promises";
23import os from "node:os";
34import path from "node:path";
4-import { describe, expect, it, vi } from "vitest";
5+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
56import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js";
67import {
78DEFAULT_AGENTS_FILENAME,
@@ -19,9 +20,26 @@ import {
1920reconcileWorkspaceBootstrapCompletion,
2021resolveWorkspaceBootstrapStatus,
2122resolveDefaultAgentWorkspaceDir,
23+resolveWorkspaceAttestationPath,
24+WORKSPACE_VANISHED_ERROR_CODE,
2225type WorkspaceBootstrapFile,
2326} from "./workspace.js";
242728+let testStateDir: string | undefined;
29+30+beforeEach(async () => {
31+testStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-state-"));
32+vi.stubEnv("OPENCLAW_STATE_DIR", testStateDir);
33+});
34+35+afterEach(async () => {
36+vi.unstubAllEnvs();
37+if (testStateDir) {
38+await fs.rm(testStateDir, { recursive: true, force: true });
39+testStateDir = undefined;
40+}
41+});
42+2543describe("resolveDefaultAgentWorkspaceDir", () => {
2644it("uses OPENCLAW_HOME for default workspace resolution", () => {
2745const dir = resolveDefaultAgentWorkspaceDir({
@@ -68,6 +86,17 @@ async function expectPathMissing(filePath: string): Promise<void> {
6886await expect(fs.access(filePath)).rejects.toHaveProperty("code", "ENOENT");
6987}
708889+async function expectWorkspaceVanished(
90+action: Promise<unknown>,
91+expected?: { attestationPath?: string },
92+): Promise<void> {
93+await expect(action).rejects.toMatchObject({
94+code: WORKSPACE_VANISHED_ERROR_CODE,
95+name: "WorkspaceVanishedError",
96+ ...expected,
97+});
98+}
99+71100async function expectCompletedWithoutBootstrap(dir: string) {
72101await expect(fs.access(path.join(dir, DEFAULT_IDENTITY_FILENAME))).resolves.toBeUndefined();
73102await expectPathMissing(path.join(dir, DEFAULT_BOOTSTRAP_FILENAME));
@@ -95,6 +124,289 @@ describe("ensureAgentWorkspace", () => {
95124expect((await readWorkspaceState(tempDir)).setupCompletedAt).toBeUndefined();
96125});
97126127+it("refuses to re-seed a recently attested workspace after the directory disappears", async () => {
128+const tempDir = await makeTempWorkspace("openclaw-workspace-");
129+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
130+await expect(fs.access(resolveWorkspaceAttestationPath(tempDir))).resolves.toBeUndefined();
131+132+await fs.rm(tempDir, { recursive: true, force: true });
133+134+await expectWorkspaceVanished(
135+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
136+);
137+await expectPathMissing(tempDir);
138+});
139+140+it("refuses to re-seed a recently attested workspace after its contents are wiped", async () => {
141+const tempDir = await makeTempWorkspace("openclaw-workspace-");
142+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
143+144+await fs.rm(tempDir, { recursive: true, force: true });
145+await fs.mkdir(tempDir, { recursive: true });
146+147+await expectWorkspaceVanished(
148+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
149+);
150+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
151+await expectPathMissing(path.join(tempDir, ...WORKSPACE_STATE_PATH_SEGMENTS));
152+});
153+154+it("refuses to re-seed a recently attested workspace after only generated remnants survive", async () => {
155+const tempDir = await makeTempWorkspace("openclaw-workspace-");
156+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
157+const generatedAgents = await fs.readFile(path.join(tempDir, DEFAULT_AGENTS_FILENAME), "utf-8");
158+159+await fs.rm(tempDir, { recursive: true, force: true });
160+await fs.mkdir(tempDir, { recursive: true });
161+await fs.writeFile(path.join(tempDir, DEFAULT_AGENTS_FILENAME), generatedAgents);
162+163+await expectWorkspaceVanished(
164+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
165+);
166+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
167+await expectPathMissing(path.join(tempDir, ...WORKSPACE_STATE_PATH_SEGMENTS));
168+});
169+170+it("refuses to re-seed a recently attested workspace after only generated git metadata survives", async () => {
171+const tempDir = await makeTempWorkspace("openclaw-workspace-");
172+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
173+174+await fs.rm(tempDir, { recursive: true, force: true });
175+await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
176+await fs.mkdir(path.join(tempDir, ".openclaw"), { recursive: true });
177+178+await expectWorkspaceVanished(
179+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
180+);
181+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
182+});
183+184+it("refuses to accept old generated bootstrap files recorded by the attestation marker", async () => {
185+const tempDir = await makeTempWorkspace("openclaw-workspace-");
186+const oldGeneratedAgents = "old generated agents\n";
187+await fs.writeFile(path.join(tempDir, DEFAULT_AGENTS_FILENAME), oldGeneratedAgents);
188+const attestationPath = resolveWorkspaceAttestationPath(tempDir);
189+await fs.mkdir(path.dirname(attestationPath), { recursive: true });
190+await fs.writeFile(
191+attestationPath,
192+[
193+"openclaw-workspace-attestation:v1",
194+new Date().toISOString(),
195+`generated:${DEFAULT_AGENTS_FILENAME}:${createHash("sha256").update(oldGeneratedAgents).digest("hex")}`,
196+"",
197+].join("\n"),
198+);
199+200+await expectWorkspaceVanished(
201+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
202+);
203+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
204+});
205+206+it("refuses a recently attested workspace when generated state and only one generated file survive", async () => {
207+const tempDir = await makeTempWorkspace("openclaw-workspace-");
208+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
209+const generatedAgents = await fs.readFile(path.join(tempDir, DEFAULT_AGENTS_FILENAME), "utf-8");
210+const state = await fs.readFile(path.join(tempDir, ...WORKSPACE_STATE_PATH_SEGMENTS), "utf-8");
211+212+await fs.rm(tempDir, { recursive: true, force: true });
213+await fs.mkdir(path.join(tempDir, WORKSPACE_STATE_PATH_SEGMENTS[0]), { recursive: true });
214+await fs.writeFile(path.join(tempDir, DEFAULT_AGENTS_FILENAME), generatedAgents);
215+await fs.writeFile(path.join(tempDir, ...WORKSPACE_STATE_PATH_SEGMENTS), state);
216+217+await expectWorkspaceVanished(
218+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
219+);
220+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
221+});
222+223+it("accepts a recently attested workspace when customized AGENTS.md survives", async () => {
224+const tempDir = await makeTempWorkspace("openclaw-workspace-");
225+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
226+await fs.writeFile(path.join(tempDir, DEFAULT_AGENTS_FILENAME), "custom instructions\n");
227+await fs.rm(path.join(tempDir, ...WORKSPACE_STATE_PATH_SEGMENTS), { force: true });
228+await fs.rm(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME), { force: true });
229+230+await expect(
231+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
232+).resolves.toMatchObject({ dir: tempDir });
233+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
234+});
235+236+it("accepts a recently attested workspace when only custom skills survive", async () => {
237+const tempDir = await makeTempWorkspace("openclaw-workspace-");
238+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
239+240+await fs.rm(tempDir, { recursive: true, force: true });
241+await fs.mkdir(path.join(tempDir, "skills", "local-skill"), { recursive: true });
242+await fs.writeFile(path.join(tempDir, "skills", "local-skill", "SKILL.md"), "---\n");
243+244+await expect(
245+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
246+).resolves.toMatchObject({ dir: tempDir });
247+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
248+expect((await readWorkspaceState(tempDir)).setupCompletedAt).toMatch(/\d{4}-\d{2}-\d{2}T/);
249+});
250+251+it("refuses a recently attested workspace when only non-skill skills leftovers survive", async () => {
252+const tempDir = await makeTempWorkspace("openclaw-workspace-");
253+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
254+255+await fs.rm(tempDir, { recursive: true, force: true });
256+await fs.mkdir(path.join(tempDir, "skills"), { recursive: true });
257+await fs.writeFile(path.join(tempDir, "skills", ".DS_Store"), "");
258+259+await expectWorkspaceVanished(
260+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
261+);
262+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
263+});
264+265+it("refuses to recreate a skip-bootstrap workspace after the directory disappears", async () => {
266+const tempDir = await makeTempWorkspace("openclaw-workspace-");
267+await fs.writeFile(path.join(tempDir, "seed.txt"), "preseeded\n");
268+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false });
269+270+await fs.rm(tempDir, { recursive: true, force: true });
271+272+await expectWorkspaceVanished(
273+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false }),
274+);
275+await expectPathMissing(tempDir);
276+});
277+278+it("refuses to accept an empty skip-bootstrap workspace after contents are wiped", async () => {
279+const tempDir = await makeTempWorkspace("openclaw-workspace-");
280+await fs.writeFile(path.join(tempDir, "seed.txt"), "preseeded\n");
281+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false });
282+283+await fs.rm(tempDir, { recursive: true, force: true });
284+await fs.mkdir(tempDir, { recursive: true });
285+286+await expectWorkspaceVanished(
287+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false }),
288+);
289+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
290+});
291+292+it("refuses to accept a wiped skip-bootstrap workspace with only metadata leftovers", async () => {
293+const tempDir = await makeTempWorkspace("openclaw-workspace-");
294+await fs.writeFile(path.join(tempDir, "seed.txt"), "preseeded\n");
295+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false });
296+297+await fs.rm(tempDir, { recursive: true, force: true });
298+await fs.mkdir(path.join(tempDir, ".openclaw"), { recursive: true });
299+await fs.mkdir(path.join(tempDir, "skills"), { recursive: true });
300+await fs.writeFile(path.join(tempDir, ".DS_Store"), "");
301+302+await expectWorkspaceVanished(
303+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false }),
304+);
305+await expectPathMissing(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME));
306+});
307+308+it("allows repeated skip-bootstrap setup for an intentionally empty workspace", async () => {
309+const tempDir = await makeTempWorkspace("openclaw-workspace-");
310+311+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false });
312+await expect(
313+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: false }),
314+).resolves.toMatchObject({ dir: tempDir });
315+});
316+317+it("allows a brand new workspace when the only attestation marker is stale", async () => {
318+const tempDir = await makeTempWorkspace("openclaw-workspace-");
319+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
320+await fs.rm(tempDir, { recursive: true, force: true });
321+const staleDate = new Date(Date.now() - 25 * 60 * 60 * 1000);
322+await fs.utimes(resolveWorkspaceAttestationPath(tempDir), staleDate, staleDate);
323+324+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
325+326+await expectBootstrapSeeded(tempDir);
327+});
328+329+it("does not overwrite a sibling file that is not an OpenClaw attestation marker", async () => {
330+const tempDir = await makeTempWorkspace("openclaw-workspace-");
331+const attestationPath = `${tempDir}.attested`;
332+const siblingContent = "external attestation data\n";
333+await fs.writeFile(attestationPath, siblingContent);
334+335+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
336+337+await expectBootstrapSeeded(tempDir);
338+expect(await fs.readFile(attestationPath, "utf-8")).toBe(siblingContent);
339+});
340+341+it("does not read or overwrite a large sibling file at the marker path", async () => {
342+const tempDir = await makeTempWorkspace("openclaw-workspace-");
343+const attestationPath = `${tempDir}.attested`;
344+const siblingContent = "x".repeat(1024);
345+await fs.writeFile(attestationPath, siblingContent);
346+347+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
348+349+await expectBootstrapSeeded(tempDir);
350+expect(await fs.readFile(attestationPath, "utf-8")).toBe(siblingContent);
351+});
352+353+it.skipIf(process.platform === "win32")(
354+"refuses to re-seed when a recent owned marker becomes unreadable",
355+async () => {
356+const tempDir = await makeTempWorkspace("openclaw-workspace-");
357+const attestationPath = resolveWorkspaceAttestationPath(tempDir);
358+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
359+await fs.chmod(attestationPath, 0o000);
360+await fs.rm(tempDir, { recursive: true, force: true });
361+362+try {
363+await expectWorkspaceVanished(
364+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
365+);
366+} finally {
367+await fs.chmod(attestationPath, 0o600);
368+}
369+},
370+);
371+372+it.skipIf(process.platform === "win32")(
373+"refuses to re-seed when the state marker directory is unreadable",
374+async () => {
375+const tempDir = await makeTempWorkspace("openclaw-workspace-");
376+const attestationDir = path.dirname(resolveWorkspaceAttestationPath(tempDir));
377+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
378+await fs.chmod(attestationDir, 0o000);
379+await fs.rm(tempDir, { recursive: true, force: true });
380+381+try {
382+await expectWorkspaceVanished(
383+ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }),
384+);
385+} finally {
386+await fs.chmod(attestationDir, 0o700);
387+}
388+},
389+);
390+391+it.skipIf(process.platform === "win32")(
392+"ignores symlinked attestation markers without overwriting the target",
393+async () => {
394+const tempDir = await makeTempWorkspace("openclaw-workspace-");
395+const attestationPath = resolveWorkspaceAttestationPath(tempDir);
396+const symlinkTargetPath = `${attestationPath}-target`;
397+const targetContent = "outside-marker\n";
398+await fs.mkdir(path.dirname(attestationPath), { recursive: true });
399+await fs.writeFile(symlinkTargetPath, targetContent);
400+await fs.symlink(symlinkTargetPath, attestationPath);
401+402+await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true });
403+404+await expectBootstrapSeeded(tempDir);
405+expect(await fs.readFile(symlinkTargetPath, "utf-8")).toBe(targetContent);
406+expect((await fs.lstat(attestationPath)).isSymbolicLink()).toBe(true);
407+},
408+);
409+98410it("recovers partial initialization by creating BOOTSTRAP.md when marker is missing", async () => {
99411const tempDir = await makeTempWorkspace("openclaw-workspace-");
100412await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_AGENTS_FILENAME, content: "existing" });
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。