Download src/hooks/hooks-lifecycle.e2e.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 7.08 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/hooks-lifecycle.e2e.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/hooks/hooks-lifecycle.e2e.test.ts
-
curl -L -o hooks-lifecycle.e2e.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/hooks-lifecycle.e2e.test.ts
7.08 kB
| import { execFile } from "node:child_process"; | |
| import fs from "node:fs/promises"; | |
| import path from "node:path"; | |
| import { promisify } from "node:util"; | |
| import JSON5 from "json5"; | |
| import * as tar from "tar"; | |
| import { afterEach, describe, expect, it } from "vitest"; | |
| import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; | |
| import type { OpenClawConfig } from "../config/types.openclaw.js"; | |
| import { installHooksFromPath } from "./install.js"; | |
| import { | |
| clearInternalHooks, | |
| createInternalHookEvent, | |
| triggerInternalHook, | |
| } from "./internal-hooks.js"; | |
| import { prepareInternalHooks } from "./loader.js"; | |
| import { loadWorkspaceHookEntries } from "./workspace.js"; | |
| const execFileAsync = promisify(execFile); | |
| const tempDirs = useAutoCleanupTempDirTracker(afterEach); | |
| const hookName = "qa-lifecycle-recorder"; | |
| const hookMarker = "QA_HOOK_LIFECYCLE_OK"; | |
| afterEach(() => { | |
| clearInternalHooks(); | |
| }); | |
| async function runHooksCli(args: string[], env: NodeJS.ProcessEnv) { | |
| try { | |
| const result = await execFileAsync(process.execPath, ["openclaw.mjs", "hooks", ...args], { | |
| cwd: process.cwd(), | |
| env, | |
| encoding: "utf8", | |
| maxBuffer: 1024 * 1024, | |
| timeout: 90_000, | |
| }); | |
| return { stdout: result.stdout, stderr: result.stderr }; | |
| } catch (error) { | |
| const failed = error as Error & { stdout?: string; stderr?: string }; | |
| throw new Error( | |
| `${failed.message}\nstdout:\n${failed.stdout ?? ""}\nstderr:\n${failed.stderr ?? ""}`, | |
| { cause: error }, | |
| ); | |
| } | |
| } | |
| async function readConfig(configPath: string): Promise<OpenClawConfig> { | |
| return JSON5.parse(await fs.readFile(configPath, "utf8")) as OpenClawConfig; | |
| } | |
| async function createHookPackFixture() { | |
| const rootDir = tempDirs.make("openclaw-hooks-lifecycle-e2e-"); | |
| const homeDir = path.join(rootDir, "home"); | |
| const stateDir = path.join(rootDir, "state"); | |
| const workspaceDir = path.join(rootDir, "workspace"); | |
| const managedHooksDir = path.join(stateDir, "hooks"); | |
| const bundledHooksDir = path.join(rootDir, "bundled-hooks"); | |
| const packageRoot = path.join(rootDir, "archive-root"); | |
| const packageDir = path.join(packageRoot, "package"); | |
| const hookDir = path.join(packageDir, "hooks", hookName); | |
| const archivePath = path.join(rootDir, "qa-lifecycle-hooks.tgz"); | |
| const configPath = path.join(stateDir, "openclaw.json"); | |
| await Promise.all([ | |
| fs.mkdir(homeDir, { recursive: true }), | |
| fs.mkdir(stateDir, { recursive: true }), | |
| fs.mkdir(workspaceDir, { recursive: true }), | |
| fs.mkdir(bundledHooksDir, { recursive: true }), | |
| fs.mkdir(hookDir, { recursive: true }), | |
| ]); | |
| const initialConfig: OpenClawConfig = { | |
| agents: { defaults: { workspace: workspaceDir } }, | |
| hooks: { internal: { enabled: true } }, | |
| }; | |
| await fs.writeFile(configPath, `${JSON.stringify(initialConfig, null, 2)}\n`, "utf8"); | |
| await fs.writeFile( | |
| path.join(packageDir, "package.json"), | |
| `${JSON.stringify( | |
| { | |
| name: "@openclaw/qa-lifecycle-hooks", | |
| version: "1.0.0", | |
| openclaw: { hooks: [`./hooks/${hookName}`] }, | |
| }, | |
| null, | |
| 2, | |
| )}\n`, | |
| "utf8", | |
| ); | |
| await fs.writeFile( | |
| path.join(hookDir, "HOOK.md"), | |
| [ | |
| "---", | |
| `name: ${hookName}`, | |
| "description: Records a Gateway startup lifecycle event", | |
| 'metadata: {"openclaw":{"events":["gateway:startup"]}}', | |
| "---", | |
| "", | |
| "# QA lifecycle recorder", | |
| "", | |
| ].join("\n"), | |
| "utf8", | |
| ); | |
| await fs.writeFile( | |
| path.join(hookDir, "handler.js"), | |
| `export default async function recordLifecycle(event) { event.messages.push("${hookMarker}"); }\n`, | |
| "utf8", | |
| ); | |
| await tar.c({ cwd: packageRoot, file: archivePath, gzip: true }, ["package"]); | |
| const env: NodeJS.ProcessEnv = { | |
| ...process.env, | |
| HOME: homeDir, | |
| USERPROFILE: homeDir, | |
| OPENCLAW_CONFIG_PATH: configPath, | |
| OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", | |
| OPENCLAW_HOME: homeDir, | |
| OPENCLAW_STATE_DIR: stateDir, | |
| OPENCLAW_TEST_FAST: "1", | |
| VITEST: "", | |
| }; | |
| return { | |
| archivePath, | |
| bundledHooksDir, | |
| configPath, | |
| env, | |
| initialConfig, | |
| managedHooksDir, | |
| workspaceDir, | |
| }; | |
| } | |
| describe("internal hook lifecycle", () => { | |
| it("installs, discovers, toggles, and dispatches an authored hook pack", async () => { | |
| const fixture = await createHookPackFixture(); | |
| const install = await installHooksFromPath({ | |
| config: fixture.initialConfig, | |
| hooksDir: fixture.managedHooksDir, | |
| path: fixture.archivePath, | |
| }); | |
| expect(install).toMatchObject({ | |
| ok: true, | |
| hookPackId: "qa-lifecycle-hooks", | |
| hooks: [hookName], | |
| packageKind: "hook-only", | |
| }); | |
| if (!install.ok) { | |
| throw new Error(install.error); | |
| } | |
| await expect( | |
| fs.readFile(path.join(install.targetDir, "hooks", hookName, "HOOK.md"), "utf8"), | |
| ).resolves.toContain("gateway:startup"); | |
| const discovered = loadWorkspaceHookEntries(fixture.workspaceDir, { | |
| bundledHooksDir: fixture.bundledHooksDir, | |
| config: fixture.initialConfig, | |
| managedHooksDir: fixture.managedHooksDir, | |
| }); | |
| expect(discovered).toEqual([ | |
| expect.objectContaining({ | |
| hook: expect.objectContaining({ | |
| name: hookName, | |
| source: "openclaw-managed", | |
| }), | |
| metadata: expect.objectContaining({ | |
| events: ["gateway:startup"], | |
| }), | |
| }), | |
| ]); | |
| const disabled = await runHooksCli(["disable", hookName], fixture.env); | |
| expect(disabled.stdout).toContain(hookName); | |
| const disabledConfig = await readConfig(fixture.configPath); | |
| expect(disabledConfig.hooks?.internal?.entries?.[hookName]?.enabled).toBe(false); | |
| clearInternalHooks(); | |
| const disabledHooks = await prepareInternalHooks(disabledConfig, fixture.workspaceDir, { | |
| bundledHooksDir: fixture.bundledHooksDir, | |
| managedHooksDir: fixture.managedHooksDir, | |
| }); | |
| expect(disabledHooks.loadedCount).toBe(0); | |
| disabledHooks.commit(); | |
| const disabledEvent = createInternalHookEvent("gateway", "startup", "gateway:startup", { | |
| cfg: disabledConfig, | |
| workspaceDir: fixture.workspaceDir, | |
| }); | |
| await triggerInternalHook(disabledEvent); | |
| expect(disabledEvent.messages).not.toContain(hookMarker); | |
| const enabled = await runHooksCli(["enable", hookName], fixture.env); | |
| expect(enabled.stdout).toContain(hookName); | |
| const enabledConfig = await readConfig(fixture.configPath); | |
| expect(enabledConfig.hooks?.internal?.entries?.[hookName]?.enabled).toBe(true); | |
| clearInternalHooks(); | |
| const enabledHooks = await prepareInternalHooks(enabledConfig, fixture.workspaceDir, { | |
| bundledHooksDir: fixture.bundledHooksDir, | |
| managedHooksDir: fixture.managedHooksDir, | |
| }); | |
| expect(enabledHooks.loadedCount).toBe(1); | |
| enabledHooks.commit(); | |
| const event = createInternalHookEvent("gateway", "startup", "gateway:startup", { | |
| cfg: enabledConfig, | |
| workspaceDir: fixture.workspaceDir, | |
| }); | |
| await triggerInternalHook(event); | |
| expect(event.messages).toContain(hookMarker); | |
| }, 180_000); | |
| }); | |