Files
wehub-resource-sync 979fb22d7c
CI / test (20, macos-latest) (push) Waiting to run
CI / test (20, ubuntu-latest) (push) Waiting to run
CI / test (22, macos-latest) (push) Waiting to run
CI / test (22, ubuntu-latest) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:01:18 +08:00

378 lines
14 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { FilesystemWatcher, configFromEnv } from "../integrations/filesystem-watcher/watcher.mjs";
function tempDir(): string {
return mkdtempSync(join(tmpdir(), "fs-watch-"));
}
function wait(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
describe("FilesystemWatcher", { retry: 2 }, () => {
let root: string;
const originalFetch = globalThis.fetch;
let captured: Array<{ url: string; body: unknown; headers: Record<string, string> }>;
beforeEach(() => {
root = tempDir();
captured = [];
(globalThis as { fetch: typeof fetch }).fetch = (async (
url: string | URL,
init?: RequestInit,
) => {
captured.push({
url: url.toString(),
body: init?.body ? JSON.parse(init.body as string) : null,
headers: (init?.headers || {}) as Record<string, string>,
});
return new Response("{}", { status: 200 });
}) as unknown as typeof fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
try {
rmSync(root, { recursive: true, force: true });
} catch {}
});
it("emits a post_tool_use observation with HookPayload shape on write", async () => {
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
w.start();
try {
writeFileSync(join(root, "notes.md"), "hello world\n");
await wait(1500);
expect(captured.length).toBeGreaterThanOrEqual(1);
const obs = captured[captured.length - 1];
expect(obs.url).toBe("http://localhost:3111/agentmemory/observe");
const body = obs.body as {
hookType: string;
sessionId: string;
project: string;
cwd: string;
timestamp: string;
data: { changeKind: string; files: string[]; content: string; source: string };
};
expect(body.hookType).toBe("post_tool_use");
expect(typeof body.sessionId).toBe("string");
expect(body.sessionId.length).toBeGreaterThan(0);
expect(typeof body.project).toBe("string");
expect(body.project.length).toBeGreaterThan(0);
expect(body.cwd).toBe(root);
expect(body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(body.data.source).toBe("filesystem-watcher");
expect(body.data.changeKind).toBe("file_change");
expect(body.data.files).toContain("notes.md");
expect(body.data.content).toContain("hello world");
} finally {
w.stop();
}
});
it("emits changeKind=file_delete when a watched file is removed", async () => {
writeFileSync(join(root, "old.md"), "bye\n");
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
w.start();
try {
unlinkSync(join(root, "old.md"));
await wait(1500);
const deletes = captured.filter(
(c) => (c.body as { data: { changeKind: string } }).data?.changeKind === "file_delete",
);
expect(deletes.length).toBeGreaterThanOrEqual(1);
} finally {
w.stop();
}
});
it("throws if no watched roots could be attached", () => {
const w = new FilesystemWatcher({
roots: ["/definitely/does/not/exist/xyz123"],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
expect(() => w.start()).toThrow(/could not watch any of the configured roots/);
});
it("ignores paths that match the default ignore set", async () => {
mkdirSync(join(root, "node_modules"), { recursive: true });
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
w.start();
try {
writeFileSync(join(root, "node_modules", "ignored.js"), "x");
await wait(1500);
const matches = captured.filter((c) =>
(c.body as { data: { files: string[] } }).data?.files?.some((f) => f.includes("ignored.js")),
);
expect(matches).toHaveLength(0);
} finally {
w.stop();
}
});
it("attaches Bearer auth when a secret is configured", async () => {
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
secret: "shhh",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
w.start();
try {
writeFileSync(join(root, "secret.md"), "bearer test\n");
await wait(1500);
expect(captured.length).toBeGreaterThanOrEqual(1);
const headers = captured[captured.length - 1].headers as Record<string, string>;
expect(headers.authorization).toBe("Bearer shhh");
} finally {
w.stop();
}
});
it("redacts sensitive dotenv preview values before sending observations", async () => {
writeFileSync(
join(root, ".env"),
[
"OPENAI_API_KEY=sk-test-secret-value",
"PUBLIC_FLAG=enabled",
"AUTHORIZATION=Bearer live-token-value",
].join("\n"),
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await w.flush(root, ".env");
expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain("OPENAI_API_KEY=[REDACTED]");
expect(content).toContain("PUBLIC_FLAG=enabled");
expect(content).toContain("AUTHORIZATION=[REDACTED]");
expect(content).not.toContain("sk-test-secret-value");
expect(content).not.toContain("live-token-value");
});
it("redacts quoted JSON-style sensitive keys before sending observations", async () => {
writeFileSync(
join(root, "settings.json"),
[
'{',
' "api_key": "json-preview-secret",',
' "public_flag": "enabled"',
'}',
].join("\n"),
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await w.flush(root, "settings.json");
expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain('"api_key": [REDACTED]');
expect(content).toContain('"public_flag": "enabled"');
expect(content).not.toContain("json-preview-secret");
});
it("redacts bearer tokens from regular text previews before sending observations", async () => {
writeFileSync(join(root, "request.txt"), "Authorization: Bearer plaintext-token-value\n");
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await w.flush(root, "request.txt");
expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain("Authorization: Bearer [REDACTED]");
expect(content).not.toContain("plaintext-token-value");
});
it("collapses multi-line PEM private-key blocks while keeping BEGIN/END markers", async () => {
const dashes = "-".repeat(5);
const rsaBegin = `${dashes}BEGIN RSA PRIVATE KEY${dashes}`;
const rsaEnd = `${dashes}END RSA PRIVATE KEY${dashes}`;
const sshBegin = `${dashes}BEGIN OPENSSH PRIVATE KEY${dashes}`;
const sshEnd = `${dashes}END OPENSSH PRIVATE KEY${dashes}`;
writeFileSync(
join(root, "id_rsa.txt"),
[
rsaBegin,
"MIIEowIBAAKCAQEAuRFakeRsaBodyLine1ShouldNeverLeakToObservationPipeline",
"MoreFakeBase64BodyForRsaKeyMaterialThatMustStayRedacted",
"YetAnotherSecretLineOfBase64KeyContentNoOneShouldRead",
rsaEnd,
"",
sshBegin,
"b3BlbnNzaC1mYWtlLWtleS1ib2R5LWxpbmUtb25l",
"b3BlbnNzaC1mYWtlLWtleS1ib2R5LWxpbmUtdHdv",
sshEnd,
"",
].join("\n"),
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await w.flush(root, "id_rsa.txt");
expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain(rsaBegin);
expect(content).toContain(rsaEnd);
expect(content).toContain(sshBegin);
expect(content).toContain(sshEnd);
expect(content).toContain("[REDACTED]");
expect(content).not.toContain("MIIEowIBAAKCAQEAuRFakeRsaBodyLine1");
expect(content).not.toContain("MoreFakeBase64BodyForRsaKeyMaterial");
expect(content).not.toContain("YetAnotherSecretLineOfBase64KeyContent");
expect(content).not.toContain("b3BlbnNzaC1mYWtlLWtleS1ib2R5LWxpbmUtb25l");
expect(content).not.toContain("b3BlbnNzaC1mYWtlLWtleS1ib2R5LWxpbmUtdHdv");
});
it("redacts inline PEM blocks embedded in single-line JSON values", async () => {
const dashes = "-".repeat(5);
const pemBegin = `${dashes}BEGIN PRIVATE KEY${dashes}`;
const pemEnd = `${dashes}END PRIVATE KEY${dashes}`;
const inlinePem = `${pemBegin}\\nMIIEvgIBADANBgkqhkiG9w0FakeServiceAccountBody\\n${pemEnd}`;
writeFileSync(
join(root, "service-account.json"),
`{\n "type": "service_account",\n "private_key": "${inlinePem}",\n "client_email": "demo@example.com"\n}\n`,
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await w.flush(root, "service-account.json");
expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain(pemBegin);
expect(content).toContain(pemEnd);
expect(content).toContain("[REDACTED]");
expect(content).not.toContain("MIIEvgIBADANBgkqhkiG9w0FakeServiceAccountBody");
expect(content).toContain('"client_email": "demo@example.com"');
});
it("redacts standalone JWT-looking strings outside Bearer context", async () => {
const jwt =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
writeFileSync(
join(root, "notes.txt"),
["session token below:", jwt, "end of token"].join("\n"),
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await w.flush(root, "notes.txt");
expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain("[REDACTED]");
expect(content).not.toContain(jwt);
expect(content).toContain("end of token");
});
it("does not redact base64-looking words that are not three-segment JWTs of sufficient length", async () => {
const notJwt = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const shortThreeSegment = "eyJabc.def.ghi";
expect(notJwt.length).toBe(62);
expect(shortThreeSegment.length).toBeLessThan(100);
writeFileSync(
join(root, "fixture.txt"),
["random base64-ish word:", notJwt, "tiny segmented thing:", shortThreeSegment].join("\n"),
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await w.flush(root, "fixture.txt");
expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain(notJwt);
expect(content).toContain(shortThreeSegment);
expect(content).not.toContain("[REDACTED]");
});
it("debounces rapid writes to a single observation", async () => {
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
w.start();
try {
const target = join(root, "burst.md");
writeFileSync(target, "1\n");
writeFileSync(target, "2\n");
writeFileSync(target, "3\n");
writeFileSync(target, "4\n");
await wait(900);
const hits = captured.filter((c) =>
(c.body as { data: { files: string[] } }).data?.files?.[0] === "burst.md",
);
expect(hits.length).toBeLessThanOrEqual(2);
} finally {
w.stop();
}
});
});
describe("configFromEnv", () => {
it("parses comma-separated dirs and ignore patterns", () => {
const cfg = configFromEnv({
AGENTMEMORY_FS_WATCH_DIRS: " /a , /b ",
AGENTMEMORY_FS_WATCH_IGNORE: "foo$, ^bar",
AGENTMEMORY_URL: "http://localhost:3111",
AGENTMEMORY_SECRET: "tok",
AGENTMEMORY_PROJECT: "demo",
});
expect(cfg.roots).toEqual(["/a", "/b"]);
expect(cfg.baseUrl).toBe("http://localhost:3111");
expect(cfg.secret).toBe("tok");
expect(cfg.project).toBe("demo");
expect(cfg.ignorePatterns).toHaveLength(2);
expect(cfg.ignorePatterns[0].test("abcfoo")).toBe(true);
expect(cfg.ignorePatterns[1].test("barbaz")).toBe(true);
});
it("returns empty roots when the env var is missing", () => {
const cfg = configFromEnv({});
expect(cfg.roots).toEqual([]);
});
});