diff --git a/CHANGELOG.md b/CHANGELOG.md index 462350661eb..68dfd8e6b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai - Plugins/externalization: add official npm-first catalogs for externalized channel, provider, and generic plugins, keep unpublished ACPX/Google Chat/LINE bundled, and make missing-plugin repair honor npm-first metadata while ClawHub pack files roll out. Thanks @vincentkoc. - Plugins/update: detect tracked plugin install records whose package directories disappeared during `openclaw update`, reinstall them before normal plugin updates, and fail the update if any install record still points at missing disk payloads. - Plugins/registry: hash manifest and package metadata when validating persisted plugin registries so fast same-size rewrites cannot leave stale plugin metadata trusted. +- Plugins/registry: canonicalize install-record provenance paths before trust diagnostics, so npm plugins installed under symlinked temp/state roots no longer warn as untracked local code. - CLI/infer: reject local `codex/*` one-shot model probes before simple-completion dispatch and point operators at the Codex app-server runtime path instead of ending with an empty-output error. - Agents/sessions: preserve terminal lifecycle state when final run metadata persists from a stale in-memory snapshot, preventing `main` sessions from staying stuck as running after completed or timed-out turns. - Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting. diff --git a/docs/ci.md b/docs/ci.md index 04508524491..f0da6566b62 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -54,7 +54,7 @@ Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests - **CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits** use a fast Node-only manifest path: `preflight`, security, and a single `checks-fast-core` task. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the change is limited to the routing or helper surfaces the fast task exercises directly. - **Windows Node checks** are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes. -The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, small core unit lanes are paired, auto-reply runs as four balanced workers (with the reply subtree split into agent-runner, dispatch, and commands/state-routing shards), and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job, including `pnpm prompt:snapshots:check` so Codex happy-path prompt drift is pinned to the PR that caused it. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built. +The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, small core unit lanes are paired, auto-reply runs as four balanced workers (with the reply subtree split into agent-runner, dispatch, and commands/state-routing shards), and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job, including `pnpm prompt:snapshots:check` so Codex runtime happy-path prompt drift is pinned to the PR that caused it. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built. Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push. diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index e8fd9d02eae..807c2af1c9c 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -111,8 +111,8 @@ in both the global system prompt and channel context. ## Prompt snapshots -OpenClaw keeps committed happy-path prompt snapshots for the Codex/message-tool -runtime under `test/fixtures/agents/prompt-snapshots/happy-path/`. They render +OpenClaw keeps committed prompt snapshots for the Codex runtime happy path under +`test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/`. They render selected app-server thread/turn params plus a reconstructed model-bound prompt layer stack for Telegram direct, Discord group, and heartbeat turns. That stack includes a pinned Codex `gpt-5.5` model prompt fixture generated from Codex's diff --git a/scripts/generate-prompt-snapshots.ts b/scripts/generate-prompt-snapshots.ts index b5784944c55..bf2a2e0b20f 100644 --- a/scripts/generate-prompt-snapshots.ts +++ b/scripts/generate-prompt-snapshots.ts @@ -5,8 +5,8 @@ import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { + CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, createHappyPathPromptSnapshotFiles, - HAPPY_PATH_PROMPT_SNAPSHOT_DIR, } from "../test/helpers/agents/happy-path-prompt-snapshots.js"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -40,7 +40,7 @@ async function writeSnapshotFiles(root: string, files: PromptSnapshotFile[]) { async function formatSnapshotFiles(root: string, files: PromptSnapshotFile[]) { const filePaths = files - .filter((file) => file.path.endsWith(".json")) + .filter((file) => file.path.endsWith(".md") || file.path.endsWith(".json")) .map((file) => path.resolve(root, file.path)); if (filePaths.length === 0) { return; @@ -62,7 +62,9 @@ async function readSnapshotFiles(root: string, files: PromptSnapshotFile[]) { async function listCommittedSnapshotArtifactPaths(root: string): Promise { let committedEntries: string[]; try { - committedEntries = await fs.readdir(path.resolve(root, HAPPY_PATH_PROMPT_SNAPSHOT_DIR)); + committedEntries = await fs.readdir( + path.resolve(root, CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR), + ); } catch (error) { if (!hasErrorCode(error, "ENOENT")) { throw error; @@ -71,7 +73,7 @@ async function listCommittedSnapshotArtifactPaths(root: string): Promise entry.endsWith(".md") || entry.endsWith(".json")) - .map((entry) => path.join(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, entry)); + .map((entry) => path.join(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, entry)); } export async function deleteStalePromptSnapshotFiles( @@ -100,7 +102,9 @@ export async function createFormattedPromptSnapshotFiles(): Promise 0 ? ` Deleted ${deleted.length} stale file(s).` : ""; diff --git a/src/plugins/loader-provenance.ts b/src/plugins/loader-provenance.ts index cddf50c7d85..bd958ac8148 100644 --- a/src/plugins/loader-provenance.ts +++ b/src/plugins/loader-provenance.ts @@ -3,7 +3,7 @@ import { resolveUserPath } from "../utils.js"; import type { PluginCandidate } from "./discovery.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; -import { isPathInside, safeStatSync } from "./path-safety.js"; +import { isPathInside, safeRealpathSync, safeStatSync } from "./path-safety.js"; import type { PluginRecord, PluginRegistry } from "./registry.js"; import type { PluginLogger } from "./types.js"; @@ -44,15 +44,16 @@ function addPathToMatcher( if (!resolved) { return; } - if (matcher.exact.has(resolved) || matcher.dirs.includes(resolved)) { + const canonical = safeRealpathSync(resolved) ?? resolved; + if (matcher.exact.has(canonical) || matcher.dirs.includes(canonical)) { return; } - const stat = safeStatSync(resolved); + const stat = safeStatSync(canonical); if (stat?.isDirectory()) { - matcher.dirs.push(resolved); + matcher.dirs.push(canonical); return; } - matcher.exact.add(resolved); + matcher.exact.add(canonical); } function matchesPathMatcher(matcher: PathMatcher, sourcePath: string): boolean { @@ -101,16 +102,17 @@ function isTrackedByProvenance(params: { env: NodeJS.ProcessEnv; }): boolean { const sourcePath = resolveUserPath(params.source, params.env); + const canonicalSourcePath = safeRealpathSync(sourcePath) ?? sourcePath; const installRule = params.index.installRules.get(params.pluginId); if (installRule) { if (installRule.trackedWithoutPaths) { return true; } - if (matchesPathMatcher(installRule.matcher, sourcePath)) { + if (matchesPathMatcher(installRule.matcher, canonicalSourcePath)) { return true; } } - return matchesPathMatcher(params.index.loadPathMatcher, sourcePath); + return matchesPathMatcher(params.index.loadPathMatcher, canonicalSourcePath); } function matchesExplicitInstallRule(params: { @@ -120,11 +122,12 @@ function matchesExplicitInstallRule(params: { env: NodeJS.ProcessEnv; }): boolean { const sourcePath = resolveUserPath(params.source, params.env); + const canonicalSourcePath = safeRealpathSync(sourcePath) ?? sourcePath; const installRule = params.index.installRules.get(params.pluginId); if (!installRule || installRule.trackedWithoutPaths) { return false; } - return matchesPathMatcher(installRule.matcher, sourcePath); + return matchesPathMatcher(installRule.matcher, canonicalSourcePath); } function resolveCandidateDuplicateRank(params: { diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 67da8248698..2836db62b22 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -6273,6 +6273,74 @@ module.exports = { }; }, }, + { + label: "does not warn when install paths resolve through a symlinked state root", + loadRegistry: () => { + useNoBundledPlugins(); + const stateDir = makeTempDir(); + const realHome = path.join(stateDir, "real-home"); + const linkedHome = path.join(stateDir, "linked-home"); + mkdirSafe(realHome); + fs.symlinkSync(realHome, linkedHome, process.platform === "win32" ? "junction" : "dir"); + + const pluginDir = path.join( + realHome, + ".openclaw", + "npm", + "node_modules", + "@example", + "tracked-symlink-install", + ); + mkdirSafe(pluginDir); + const plugin = writePlugin({ + id: "tracked-symlink-install", + body: simplePluginBody("tracked-symlink-install"), + dir: pluginDir, + filename: "index.cjs", + }); + writePersistedInstalledPluginIndexInstallRecordsSync( + { + [plugin.id]: { + source: "npm", + spec: "@example/tracked-symlink-install@1.0.0", + installPath: path.join( + linkedHome, + ".openclaw", + "npm", + "node_modules", + "@example", + "tracked-symlink-install", + ), + version: "1.0.0", + }, + }, + { stateDir }, + ); + + const warnings: string[] = []; + const registry = loadOpenClawPlugins({ + cache: false, + logger: createWarningLogger(warnings), + env: { + ...process.env, + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", + }, + config: { + plugins: { + enabled: true, + }, + }, + }); + + return { + registry, + warnings, + pluginId: plugin.id, + expectWarning: false, + }; + }, + }, ] as const; runScenarioCases(scenarios, (scenario) => { diff --git a/test/fixtures/agents/prompt-snapshots/happy-path/README.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/README.md similarity index 100% rename from test/fixtures/agents/prompt-snapshots/happy-path/README.md rename to test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/README.md diff --git a/test/fixtures/agents/prompt-snapshots/happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json similarity index 100% rename from test/fixtures/agents/prompt-snapshots/happy-path/codex-dynamic-tools.discord-group.json rename to test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json diff --git a/test/fixtures/agents/prompt-snapshots/happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json similarity index 100% rename from test/fixtures/agents/prompt-snapshots/happy-path/codex-dynamic-tools.heartbeat-turn.json rename to test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json diff --git a/test/fixtures/agents/prompt-snapshots/happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json similarity index 100% rename from test/fixtures/agents/prompt-snapshots/happy-path/codex-dynamic-tools.telegram-direct.json rename to test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json diff --git a/test/fixtures/agents/prompt-snapshots/happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md similarity index 99% rename from test/fixtures/agents/prompt-snapshots/happy-path/discord-group-codex-message-tool.md rename to test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index eb317e4f4de..4b894b4632d 100644 --- a/test/fixtures/agents/prompt-snapshots/happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -568,9 +568,7 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "type": "string" }, "action": { - "enum": [ - "send" - ], + "enum": ["send"], "type": "string" }, "activityName": { @@ -938,9 +936,7 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "type": "string" } }, - "required": [ - "action" - ], + "required": ["action"], "type": "object" }, "name": "message" diff --git a/test/fixtures/agents/prompt-snapshots/happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md similarity index 99% rename from test/fixtures/agents/prompt-snapshots/happy-path/telegram-direct-codex-message-tool.md rename to test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index 99a89f7fbee..a3c3ca34ea0 100644 --- a/test/fixtures/agents/prompt-snapshots/happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -545,9 +545,7 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "type": "string" }, "action": { - "enum": [ - "send" - ], + "enum": ["send"], "type": "string" }, "activityName": { @@ -915,9 +913,7 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "type": "string" } }, - "required": [ - "action" - ], + "required": ["action"], "type": "object" }, "name": "message" diff --git a/test/fixtures/agents/prompt-snapshots/happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md similarity index 99% rename from test/fixtures/agents/prompt-snapshots/happy-path/telegram-heartbeat-codex-tool.md rename to test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 50726abe46e..24274682b18 100644 --- a/test/fixtures/agents/prompt-snapshots/happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -547,9 +547,7 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "type": "string" }, "action": { - "enum": [ - "send" - ], + "enum": ["send"], "type": "string" }, "activityName": { @@ -917,9 +915,7 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "type": "string" } }, - "required": [ - "action" - ], + "required": ["action"], "type": "object" }, "name": "message" @@ -939,21 +935,11 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "type": "boolean" }, "outcome": { - "enum": [ - "no_change", - "progress", - "done", - "blocked", - "needs_attention" - ], + "enum": ["no_change", "progress", "done", "blocked", "needs_attention"], "type": "string" }, "priority": { - "enum": [ - "low", - "normal", - "high" - ], + "enum": ["low", "normal", "high"], "type": "string" }, "reason": { @@ -963,11 +949,7 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "type": "string" } }, - "required": [ - "outcome", - "notify", - "summary" - ], + "required": ["outcome", "notify", "summary"], "type": "object" }, "name": "heartbeat_respond" diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index 8158ceefff1..528dbe4953d 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -23,7 +23,8 @@ import { normalizeAgentRuntimeTools } from "../../../src/plugin-sdk/agent-harnes import { createOpenClawCodingTools } from "../../../src/plugin-sdk/agent-harness.js"; import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; -export const HAPPY_PATH_PROMPT_SNAPSHOT_DIR = "test/fixtures/agents/prompt-snapshots/happy-path"; +export const CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR = + "test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path"; export const CODEX_MODEL_PROMPT_FIXTURE_DIR = "test/fixtures/agents/prompt-snapshots/codex-model-catalog"; @@ -697,8 +698,14 @@ function renderReadme(scenarios: PromptScenario[]): string { "", "Codex model prompt fixtures:", "", - `- ${path.relative(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, CODEX_MODEL_PROMPT_FIXTURE_PATH)}`, - `- ${path.relative(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, CODEX_MODEL_PROMPT_SOURCE_PATH)}`, + `- ${path.relative( + CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, + CODEX_MODEL_PROMPT_FIXTURE_PATH, + )}`, + `- ${path.relative( + CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, + CODEX_MODEL_PROMPT_SOURCE_PATH, + )}`, "", ].join("\n"); } @@ -707,15 +714,15 @@ export function createHappyPathPromptSnapshotFiles(): PromptSnapshotFile[] { const scenarios = createScenarios(); return [ { - path: path.join(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, "README.md"), + path: path.join(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, "README.md"), content: renderReadme(scenarios), }, ...scenarios.map((scenario) => ({ - path: path.join(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, `${scenario.id}.md`), + path: path.join(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, `${scenario.id}.md`), content: renderScenarioSnapshot(scenario), })), ...scenarios.map((scenario) => ({ - path: path.join(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, scenario.toolSnapshotFile), + path: path.join(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, scenario.toolSnapshotFile), content: stableJson(scenario.dynamicTools), })), ].map((file) => ({ diff --git a/test/scripts/prompt-snapshots.test.ts b/test/scripts/prompt-snapshots.test.ts index d43c54307b3..691f6003888 100644 --- a/test/scripts/prompt-snapshots.test.ts +++ b/test/scripts/prompt-snapshots.test.ts @@ -14,7 +14,7 @@ import { } from "../../scripts/sync-codex-model-prompt-fixture.js"; import { CODEX_MODEL_PROMPT_FIXTURE_DIR, - HAPPY_PATH_PROMPT_SNAPSHOT_DIR, + CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, } from "../helpers/agents/happy-path-prompt-snapshots.js"; describe("happy path prompt snapshots", () => { @@ -25,22 +25,25 @@ describe("happy path prompt snapshots", () => { expect(fs.readFileSync(file.path, "utf8"), file.path).toBe(file.content); } const committed = fs - .readdirSync(HAPPY_PATH_PROMPT_SNAPSHOT_DIR) + .readdirSync(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR) .filter((entry) => entry.endsWith(".md") || entry.endsWith(".json")) - .map((entry) => path.join(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, entry)); + .map((entry) => path.join(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, entry)); expect(committed.toSorted()).toEqual([...expectedPaths].toSorted()); }); it("deletes stale generated snapshot artifacts", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-prompt-snapshot-stale-")); try { - const snapshotDir = path.join(root, HAPPY_PATH_PROMPT_SNAPSHOT_DIR); + const snapshotDir = path.join(root, CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR); fs.mkdirSync(snapshotDir, { recursive: true }); - const stalePath = path.join(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, "stale-snapshot.md"); + const stalePath = path.join( + CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, + "stale-snapshot.md", + ); fs.writeFileSync(path.join(root, stalePath), "stale\n"); const deleted = await deleteStalePromptSnapshotFiles(root, [ - { path: path.join(HAPPY_PATH_PROMPT_SNAPSHOT_DIR, "current.md") }, + { path: path.join(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, "current.md") }, ]); expect(deleted).toEqual([stalePath]);