File size: 3,201 Bytes
d197cf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { withTempDirSync } from "../test-helpers/temp-dir.js";
import { withEnv } from "../test-utils/env.js";
import { shouldIncludeHook } from "./config.js";
import { buildWorkspaceHookStatus } from "./hooks-status.js";
import type { HookEntry } from "./types.js";

const ENV_NAME = "OPENCLAW_TEST_HOOK_REQUIRED_ENV";
const HOOK_NAME = "required-env-hook";

const entry: HookEntry = {
  hook: {
    name: HOOK_NAME,
    description: "Requires an environment variable",
    source: "openclaw-bundled",
    filePath: "/tmp/HOOK.md",
    baseDir: "/tmp",
    handlerPath: "/tmp/handler.js",
  },
  frontmatter: {},
  metadata: {
    events: ["command:new"],
    requires: { env: [ENV_NAME] },
  },
};

function configWithEnv(value: string): OpenClawConfig {
  return {
    hooks: {
      internal: {
        entries: {
          [HOOK_NAME]: { env: { [ENV_NAME]: value } },
        },
      },
    },
  };
}

function evaluate(config?: OpenClawConfig) {
  const runtimeIncluded = shouldIncludeHook({ entry, config });
  const status = buildWorkspaceHookStatus("/tmp", { entries: [entry], config }).hooks[0];
  return { runtimeIncluded, status };
}

it("includes a hook after an accepted binary is installed on unchanged PATH", () => {
  withTempDirSync({ prefix: "openclaw-hook-binary-" }, (binDir) => {
    const binaryEntry: HookEntry = {
      ...entry,
      metadata: { events: ["command:new"], requires: { anyBins: ["fixture-hook-tool"] } },
    };
    withEnv({ PATH: binDir }, () => {
      const included = () => shouldIncludeHook({ entry: binaryEntry });
      expect(included()).toBe(false);
      const executable = path.join(binDir, "fixture-hook-tool");
      fs.writeFileSync(executable, "#!/bin/sh\nexit 0\n");
      fs.chmodSync(executable, 0o755);
      expect(process.env.PATH).toBe(binDir);
      expect(included()).toBe(true);
    });
  });
});

describe("hook environment requirements", () => {
  it.each([
    { name: "missing values", hostEnv: undefined, configEnv: undefined, satisfied: false },
    { name: "blank host env", hostEnv: " \t ", configEnv: undefined, satisfied: false },
    { name: "blank config env", hostEnv: undefined, configEnv: " \n ", satisfied: false },
    { name: "valid host env", hostEnv: " host-token ", configEnv: undefined, satisfied: true },
    { name: "valid config env", hostEnv: undefined, configEnv: " config-token ", satisfied: true },
    {
      name: "valid config env after blank host env",
      hostEnv: "   ",
      configEnv: " config-token ",
      satisfied: true,
    },
  ])("keeps runtime and status aligned for $name", ({ hostEnv, configEnv, satisfied }) => {
    const { runtimeIncluded, status } = withEnv({ [ENV_NAME]: hostEnv }, () =>
      evaluate(configEnv === undefined ? undefined : configWithEnv(configEnv)),
    );

    expect(runtimeIncluded).toBe(satisfied);
    expect(status?.requirementsSatisfied).toBe(satisfied);
    expect(status?.loadable).toBe(satisfied);
    expect(status?.missing.env).toEqual(satisfied ? [] : [ENV_NAME]);
  });
});