mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(hooks): reload mutable handlers after restored-mtime edits (#128477)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { buildImportUrl } from "./import-url.js";
|
||||
|
||||
describe("buildImportUrl", () => {
|
||||
@@ -25,23 +25,24 @@ describe("buildImportUrl", () => {
|
||||
expect(url).toMatch(/^file:\/\//);
|
||||
});
|
||||
|
||||
it("appends mtime-based cache buster for workspace hooks", () => {
|
||||
it("appends file-metadata cache buster for workspace hooks", () => {
|
||||
const url = buildImportUrl(tmpFile, "openclaw-workspace");
|
||||
expect(url).toMatch(/\?t=[\d.]+&s=\d+/);
|
||||
expect(url).toMatch(/\?t=[\d.]+&c=[\d.]+&s=\d+/);
|
||||
|
||||
const { mtimeMs, size } = fs.statSync(tmpFile);
|
||||
const { ctimeMs, mtimeMs, size } = fs.statSync(tmpFile);
|
||||
expect(url).toContain(`?t=${mtimeMs}`);
|
||||
expect(url).toContain(`&c=${ctimeMs}`);
|
||||
expect(url).toContain(`&s=${size}`);
|
||||
});
|
||||
|
||||
it("appends mtime-based cache buster for managed hooks", () => {
|
||||
it("appends file-metadata cache buster for managed hooks", () => {
|
||||
const url = buildImportUrl(tmpFile, "openclaw-managed");
|
||||
expect(url).toMatch(/\?t=[\d.]+&s=\d+/);
|
||||
expect(url).toMatch(/\?t=[\d.]+&c=[\d.]+&s=\d+/);
|
||||
});
|
||||
|
||||
it("appends mtime-based cache buster for plugin hooks", () => {
|
||||
it("appends file-metadata cache buster for plugin hooks", () => {
|
||||
const url = buildImportUrl(tmpFile, "openclaw-plugin");
|
||||
expect(url).toMatch(/\?t=[\d.]+&s=\d+/);
|
||||
expect(url).toMatch(/\?t=[\d.]+&c=[\d.]+&s=\d+/);
|
||||
});
|
||||
|
||||
it("returns same URL for bundled hooks across calls (cacheable)", () => {
|
||||
@@ -56,6 +57,36 @@ describe("buildImportUrl", () => {
|
||||
expect(url1).toBe(url2);
|
||||
});
|
||||
|
||||
it("reloads a workspace hook after a same-size edit with restored mtime", async () => {
|
||||
const initialSource = 'export default () => "before";\n';
|
||||
const editedSource = 'export default () => "after!";\n';
|
||||
expect(Buffer.byteLength(editedSource)).toBe(Buffer.byteLength(initialSource));
|
||||
|
||||
fs.writeFileSync(tmpFile, initialSource);
|
||||
const cleanTime = new Date(Math.floor(Date.now() / 1000) * 1000);
|
||||
fs.utimesSync(tmpFile, cleanTime, cleanTime);
|
||||
const initialStat = fs.statSync(tmpFile);
|
||||
const initialUrl = buildImportUrl(tmpFile, "openclaw-workspace");
|
||||
const initialHandler = (await import(/* @vite-ignore */ initialUrl)).default as () => string;
|
||||
expect(initialHandler()).toBe("before");
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
fs.writeFileSync(tmpFile, editedSource);
|
||||
fs.utimesSync(tmpFile, initialStat.atime, initialStat.mtime);
|
||||
expect(fs.statSync(tmpFile).ctimeMs).not.toBe(initialStat.ctimeMs);
|
||||
},
|
||||
{ interval: 1, timeout: 1_000 },
|
||||
);
|
||||
|
||||
const editedStat = fs.statSync(tmpFile);
|
||||
expect(editedStat.size).toBe(initialStat.size);
|
||||
expect(editedStat.mtimeMs).toBe(initialStat.mtimeMs);
|
||||
const editedUrl = buildImportUrl(tmpFile, "openclaw-workspace");
|
||||
const editedHandler = (await import(/* @vite-ignore */ editedUrl)).default as () => string;
|
||||
expect(editedHandler()).toBe("after!");
|
||||
});
|
||||
|
||||
it("falls back to Date.now() when file does not exist", () => {
|
||||
const url = buildImportUrl("/nonexistent/handler.js", "openclaw-workspace");
|
||||
expect(url).toMatch(/\?t=\d+/);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* module cache across gateway restarts.
|
||||
*
|
||||
* Workspace, managed, and plugin hooks may be edited by the user between
|
||||
* restarts. For those we append `?t=<mtime>&s=<size>` so the module key
|
||||
* restarts. For those we append `?t=<mtime>&c=<ctime>&s=<size>` so the module key
|
||||
* reflects on-disk changes while staying stable for unchanged files.
|
||||
*/
|
||||
|
||||
@@ -29,8 +29,8 @@ export function buildImportUrl(handlerPath: string, source: HookSource): string
|
||||
|
||||
// Use file metadata so the cache key only changes when the file changes
|
||||
try {
|
||||
const { mtimeMs, size } = fs.statSync(handlerPath);
|
||||
return `${base}?t=${mtimeMs}&s=${size}`;
|
||||
const { ctimeMs, mtimeMs, size } = fs.statSync(handlerPath);
|
||||
return `${base}?t=${mtimeMs}&c=${ctimeMs}&s=${size}`;
|
||||
} catch {
|
||||
// If stat fails (unlikely), fall back to Date.now() to guarantee freshness
|
||||
return `${base}?t=${Date.now()}`;
|
||||
|
||||
Reference in New Issue
Block a user