mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(memory): accept Windows extra-file Markdown casing (#121976)
* fix(memory): accept Windows extra-file Markdown casing * fix(memory): keep extra-file casing policy private
This commit is contained in:
committed by
GitHub
parent
2b0ceb5622
commit
03f56cfbc3
@@ -0,0 +1,80 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
listMemoryFiles,
|
||||
readMemoryFile,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { scanMemorySources } from "./cli-runtime-common.js";
|
||||
import { resolveMemoryPathClassification } from "./memory/memory-path-provenance.js";
|
||||
|
||||
describe.skipIf(process.platform !== "win32")("Windows explicit memory extra-file casing", () => {
|
||||
let fixtureRoot = "";
|
||||
let workspaceDir = "";
|
||||
let physicalPath = "";
|
||||
let configuredAlias = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-extra-file-case-"));
|
||||
workspaceDir = path.join(fixtureRoot, "workspace");
|
||||
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
|
||||
physicalPath = path.join(fixtureRoot, "shared-notes.md");
|
||||
configuredAlias = path.join(fixtureRoot, "SHARED-NOTES.MD");
|
||||
await fs.writeFile(physicalPath, "shared Windows memory", "utf8");
|
||||
|
||||
const [physicalStat, aliasStat] = await Promise.all([
|
||||
fs.stat(physicalPath),
|
||||
fs.stat(configuredAlias),
|
||||
]);
|
||||
expect({ dev: aliasStat.dev, ino: aliasStat.ino }).toEqual({
|
||||
dev: physicalStat.dev,
|
||||
ino: physicalStat.ino,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it.each(["physical", "alias"] as const)(
|
||||
"lists, reads, and scans the configured %s spelling",
|
||||
async (spelling) => {
|
||||
const configuredPath = spelling === "physical" ? physicalPath : configuredAlias;
|
||||
await expect(listMemoryFiles(workspaceDir, [configuredPath])).resolves.toEqual([
|
||||
configuredPath,
|
||||
]);
|
||||
await expect(
|
||||
readMemoryFile({
|
||||
workspaceDir,
|
||||
extraPaths: [configuredPath],
|
||||
relPath: configuredPath,
|
||||
}),
|
||||
).resolves.toMatchObject({ text: "shared Windows memory" });
|
||||
await expect(
|
||||
scanMemorySources({
|
||||
workspaceDir,
|
||||
agentId: "main",
|
||||
sources: ["memory"],
|
||||
extraPaths: [configuredPath],
|
||||
}),
|
||||
).resolves.toMatchObject({ totalFiles: 1, issues: [] });
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the external file untrusted and built-in discovery case-exact", async () => {
|
||||
await expect(
|
||||
resolveMemoryPathClassification({
|
||||
absolutePath: configuredAlias,
|
||||
source: "memory",
|
||||
workspaceDir,
|
||||
}),
|
||||
).resolves.toEqual({ curatedRoot: false, originClass: "untrusted" });
|
||||
|
||||
const canonicalRoot = path.join(workspaceDir, "MEMORY.md");
|
||||
const uppercaseBuiltIn = path.join(workspaceDir, "memory", "BUILTIN.MD");
|
||||
await fs.writeFile(canonicalRoot, "canonical", "utf8");
|
||||
await fs.writeFile(uppercaseBuiltIn, "excluded built-in alias", "utf8");
|
||||
await expect(listMemoryFiles(workspaceDir)).resolves.toEqual([canonicalRoot]);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1966,7 +1966,7 @@
|
||||
"test:unit:fast:audit": "node --import tsx scripts/test-unit-fast-audit.mts",
|
||||
"test:voicecall:closedloop": "node --import tsx scripts/test-voicecall-closedloop.mts",
|
||||
"test:watch": "node --import tsx scripts/test-projects.mts --watch",
|
||||
"test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/process-env.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts src/cli/mcp-cli.path-case.windows.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/process-env.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts src/cli/mcp-cli.path-case.windows.test.ts extensions/memory-core/src/memory-extra-file-path.windows.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:schtasks:integration": "node --import tsx scripts/run-with-env.mts CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts",
|
||||
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
|
||||
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function isExplicitExtraMarkdownFilePath(
|
||||
filePath: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): boolean {
|
||||
return (
|
||||
filePath.endsWith(".md") || (platform === "win32" && filePath.toLowerCase().endsWith(".md"))
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
stripMemoryAnnotationCarriers,
|
||||
} from "./internal.js";
|
||||
import { normalizeMemoryMultimodalSettings, type MemoryMultimodalSettings } from "./multimodal.js";
|
||||
import { readMemoryFile } from "./read-file.js";
|
||||
|
||||
type FileEntry = NonNullable<Awaited<ReturnType<typeof buildFileEntry>>>;
|
||||
type MultimodalIndexingChunk = NonNullable<
|
||||
@@ -94,6 +95,22 @@ const multimodal: MemoryMultimodalSettings = normalizeMemoryMultimodalSettings({
|
||||
describe("memory host SDK package internals", () => {
|
||||
const getTmpDir = setupTempDirLifecycle("memory-package-");
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects an uppercase explicit extra file on case-sensitive hosts",
|
||||
async () => {
|
||||
const tmpDir = getTmpDir();
|
||||
const workspaceDir = path.join(tmpDir, "workspace");
|
||||
const upperPath = path.join(tmpDir, "NOTES.MD");
|
||||
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
|
||||
await fs.writeFile(upperPath, "not lowercase Markdown", "utf8");
|
||||
|
||||
await expect(listMemoryFiles(workspaceDir, [upperPath])).resolves.toEqual([]);
|
||||
await expect(
|
||||
readMemoryFile({ workspaceDir, extraPaths: [upperPath], relPath: upperPath }),
|
||||
).rejects.toThrow("path required");
|
||||
},
|
||||
);
|
||||
|
||||
it("drains in-flight work before propagating a concurrency failure", async () => {
|
||||
const failure = new Error("embedding failed");
|
||||
let releaseTask!: () => void;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { runWithConcurrency as runWithConcurrencyImpl } from "./concurrency.js";
|
||||
import { MEMORY_HOST_ROOT_FILENAME, normalizeConfiguredMemoryExtraPaths } from "./config-utils.js";
|
||||
import { estimateStructuredEmbeddingInputBytes } from "./embedding-input-limits.js";
|
||||
import { buildTextEmbeddingInput, type EmbeddingInput } from "./embedding-inputs.js";
|
||||
import { isExplicitExtraMarkdownFilePath } from "./explicit-extra-markdown.js";
|
||||
import {
|
||||
isFileMissingError,
|
||||
readRegularFile,
|
||||
@@ -257,7 +258,11 @@ export async function listMemoryFiles(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (stat.isFile() && isAllowedMemoryFilePath(inputPath, multimodal)) {
|
||||
if (
|
||||
stat.isFile() &&
|
||||
(isExplicitExtraMarkdownFilePath(inputPath) ||
|
||||
isAllowedMemoryFilePath(inputPath, multimodal))
|
||||
) {
|
||||
result.push(inputPath);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveMemoryHostSearchPathConfig,
|
||||
type OpenClawConfig,
|
||||
} from "./config-utils.js";
|
||||
import { isExplicitExtraMarkdownFilePath } from "./explicit-extra-markdown.js";
|
||||
import {
|
||||
assertNoSymlinkParents,
|
||||
isFileMissingError,
|
||||
@@ -88,7 +89,7 @@ export async function readMemoryFile(params: {
|
||||
const relPath = path.relative(params.workspaceDir, absPath).replace(/\\/g, "/");
|
||||
const inWorkspace = relPath.length > 0 && !relPath.startsWith("..") && !path.isAbsolute(relPath);
|
||||
const allowedWorkspace = inWorkspace && isMemoryPath(relPath);
|
||||
let allowedAdditional = false;
|
||||
let allowedAdditional: false | "directory" | "file" = false;
|
||||
if (!allowedWorkspace && (params.extraPaths?.length ?? 0) > 0) {
|
||||
const additionalPaths = normalizeExtraMemoryPathEntries(params.workspaceDir, params.extraPaths);
|
||||
for (const additionalPath of additionalPaths) {
|
||||
@@ -106,13 +107,17 @@ export async function readMemoryFile(params: {
|
||||
if (candidateStat?.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
allowedAdditional = true;
|
||||
allowedAdditional = "directory";
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (stat.isFile() && absPath === additionalPath.path && absPath.endsWith(".md")) {
|
||||
allowedAdditional = true;
|
||||
if (
|
||||
stat.isFile() &&
|
||||
absPath === additionalPath.path &&
|
||||
isExplicitExtraMarkdownFilePath(absPath)
|
||||
) {
|
||||
allowedAdditional = "file";
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
@@ -121,7 +126,7 @@ export async function readMemoryFile(params: {
|
||||
if (!allowedWorkspace && !allowedAdditional) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
if (!absPath.endsWith(".md")) {
|
||||
if (!absPath.endsWith(".md") && allowedAdditional !== "file") {
|
||||
throw new Error("path required");
|
||||
}
|
||||
if (allowedWorkspace) {
|
||||
|
||||
@@ -75,6 +75,8 @@ const WINDOWS_HOME_DISPLAY_SCOPE_RE =
|
||||
/^(?:src\/(?:utils(?:\.test)?|infra\/(?:home-display|path-guards)|commands\/agents\.commands\.list(?:\.test)?|cli\/daemon-cli\/status\.print(?:\.test)?|agents\/(?:sandbox\/fs-paths|sessions\/tools\/render-utils)(?:\.test)?)|packages\/terminal-core\/src\/display-string(?:\.test)?)\.ts$/;
|
||||
const WINDOWS_MCP_ENV_SCOPE_RE =
|
||||
/^src\/(?:cli\/mcp-cli(?:\.path-case\.windows)?\.test|cli\/mcp-cli|infra\/process-env(?:\.test)?)\.ts$/;
|
||||
const WINDOWS_MEMORY_EXTRA_FILE_SCOPE_RE =
|
||||
/^(?:packages\/memory-host-sdk\/src\/host\/(?:(?:internal|read-file)(?:\.test)?|explicit-extra-markdown)|extensions\/memory-core\/src\/(?:cli-runtime-common|memory-extra-file-path\.windows\.test))\.ts$/;
|
||||
const CONTROL_UI_I18N_SCOPE_RE =
|
||||
/^(ui\/src\/i18n\/|ui\/config\/control-ui-locales\.ts$|scripts\/(?:control-ui-i18n(?:-verify)?\.ts|lib\/control-ui-i18n-(?:catalog|config|raw-copy|sync-plan)\.ts)$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/;
|
||||
const CONTROL_UI_RAW_COPY_SOURCE_RE = /^ui\/src\/(?:app|components|lib|pages)\/.*\.tsx?$/;
|
||||
@@ -186,7 +188,8 @@ export function detectChangedScope(changedPaths) {
|
||||
WINDOWS_USAGE_TEMPLATE_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path) ||
|
||||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MCP_ENV_SCOPE_RE.test(path)) &&
|
||||
WINDOWS_MCP_ENV_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MEMORY_EXTRA_FILE_SCOPE_RE.test(path)) &&
|
||||
(!facts.isTestOnly ||
|
||||
WINDOWS_TEST_SCOPE_RE.test(path) ||
|
||||
WINDOWS_FILE_URL_SCOPE_RE.test(path) ||
|
||||
@@ -195,7 +198,8 @@ export function detectChangedScope(changedPaths) {
|
||||
WINDOWS_USAGE_TEMPLATE_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path) ||
|
||||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MCP_ENV_SCOPE_RE.test(path))
|
||||
WINDOWS_MCP_ENV_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MEMORY_EXTRA_FILE_SCOPE_RE.test(path))
|
||||
) {
|
||||
runWindows = true;
|
||||
}
|
||||
|
||||
@@ -216,6 +216,22 @@ describe("detectChangedScope Windows routing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("routes explicit memory extra-file owners and native coverage to Windows", () => {
|
||||
for (const memoryPath of [
|
||||
"packages/memory-host-sdk/src/host/explicit-extra-markdown.ts",
|
||||
"packages/memory-host-sdk/src/host/internal.ts",
|
||||
"packages/memory-host-sdk/src/host/internal.test.ts",
|
||||
"packages/memory-host-sdk/src/host/read-file.ts",
|
||||
"extensions/memory-core/src/cli-runtime-common.ts",
|
||||
"extensions/memory-core/src/memory-extra-file-path.windows.test.ts",
|
||||
]) {
|
||||
expect(detectChangedScope([memoryPath]), memoryPath).toMatchObject({
|
||||
runNode: true,
|
||||
runWindows: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("routes SecretRef path-security changes and focused owner coverage to Windows", () => {
|
||||
for (const secretRefPath of [
|
||||
"src/commands/doctor-gateway-auth-token.ts",
|
||||
|
||||
@@ -356,4 +356,10 @@ describe("package scripts", () => {
|
||||
expect(script).toContain("src/infra/process-env.test.ts");
|
||||
expect(script).toContain("src/cli/mcp-cli.path-case.windows.test.ts");
|
||||
});
|
||||
|
||||
it("runs explicit memory extra-file casing coverage in Windows CI", () => {
|
||||
expect(readPackageJson().scripts["test:windows:ci"]).toContain(
|
||||
"extensions/memory-core/src/memory-extra-file-path.windows.test.ts",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user