Advertisement

← octto

tests/hooks/fragment-injector.test.ts

7.3 KB Download file
// tests/hooks/fragment-injector.test.ts
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import {
  formatFragmentsBlock,
  levenshteinDistance,
  loadProjectFragments,
  mergeFragments,
  warnUnknownAgents,
} from "../../src/hooks/fragment-injector";

describe("formatFragmentsBlock", () => {
  it("should return empty string for undefined fragments", () => {
    expect(formatFragmentsBlock(undefined)).toBe("");
  });

  it("should return empty string for empty array", () => {
    expect(formatFragmentsBlock([])).toBe("");
  });

  it("should format single fragment as XML block", () => {
    const result = formatFragmentsBlock(["Custom instruction"]);
    expect(result).toBe("<user-instructions>\n- Custom instruction\n</user-instructions>\n\n");
  });

  it("should format multiple fragments as XML block with bullet points", () => {
    const result = formatFragmentsBlock(["Instruction 1", "Instruction 2", "Instruction 3"]);
    expect(result).toBe(
      "<user-instructions>\n- Instruction 1\n- Instruction 2\n- Instruction 3\n</user-instructions>\n\n",
    );
  });
});

describe("mergeFragments", () => {
  it("should return empty object when both are undefined", () => {
    expect(mergeFragments(undefined, undefined)).toEqual({});
  });

  it("should return global fragments when project is undefined", () => {
    const global = { octto: ["global instruction"] };
    expect(mergeFragments(global, undefined)).toEqual({ octto: ["global instruction"] });
  });

  it("should return project fragments when global is undefined", () => {
    const project = { octto: ["project instruction"] };
    expect(mergeFragments(undefined, project)).toEqual({ octto: ["project instruction"] });
  });

  it("should merge fragments for same agent (global first, project appended)", () => {
    const global = { octto: ["global instruction"] };
    const project = { octto: ["project instruction"] };
    expect(mergeFragments(global, project)).toEqual({
      octto: ["global instruction", "project instruction"],
    });
  });

  it("should keep fragments for different agents separate", () => {
    const global = { octto: ["octto instruction"] };
    const project = { bootstrapper: ["bootstrapper instruction"] };
    expect(mergeFragments(global, project)).toEqual({
      octto: ["octto instruction"],
      bootstrapper: ["bootstrapper instruction"],
    });
  });

  it("should merge complex multi-agent fragments", () => {
    const global = {
      octto: ["octto global 1", "octto global 2"],
      probe: ["probe global"],
    };
    const project = {
      octto: ["octto project"],
      bootstrapper: ["bootstrapper project"],
    };
    expect(mergeFragments(global, project)).toEqual({
      octto: ["octto global 1", "octto global 2", "octto project"],
      probe: ["probe global"],
      bootstrapper: ["bootstrapper project"],
    });
  });
});

describe("loadProjectFragments", () => {
  let tempDir: string;
  let warnSpy: ReturnType<typeof spyOn>;

  beforeEach(async () => {
    tempDir = await mkdtemp(join(tmpdir(), "octto-test-"));
    warnSpy = spyOn(console, "warn").mockImplementation(() => {});
  });

  afterEach(async () => {
    warnSpy.mockRestore();
    await rm(tempDir, { recursive: true });
  });

  it("should return undefined when .octto directory does not exist", async () => {
    const result = await loadProjectFragments(tempDir);
    expect(result).toBeUndefined();
  });

  it("should return undefined when fragments.json does not exist", async () => {
    await mkdir(join(tempDir, ".octto"));
    const result = await loadProjectFragments(tempDir);
    expect(result).toBeUndefined();
  });

  it("should load valid fragments.json", async () => {
    await mkdir(join(tempDir, ".octto"));
    await writeFile(join(tempDir, ".octto", "fragments.json"), JSON.stringify({ octto: ["project instruction"] }));

    const result = await loadProjectFragments(tempDir);
    expect(result).toEqual({ octto: ["project instruction"] });
  });

  it("should return undefined for invalid JSON", async () => {
    await mkdir(join(tempDir, ".octto"));
    await writeFile(join(tempDir, ".octto", "fragments.json"), "not valid json");

    const result = await loadProjectFragments(tempDir);
    expect(result).toBeUndefined();
  });

  it("should return undefined for invalid schema (wrong type)", async () => {
    await mkdir(join(tempDir, ".octto"));
    await writeFile(join(tempDir, ".octto", "fragments.json"), JSON.stringify({ octto: "not an array" }));

    const result = await loadProjectFragments(tempDir);
    expect(result).toBeUndefined();
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid fragments.json schema"));
  });
});

describe("levenshteinDistance", () => {
  it("should return 0 for identical strings", () => {
    expect(levenshteinDistance("octto", "octto")).toBe(0);
  });

  it("should return length of b for empty a", () => {
    expect(levenshteinDistance("", "octto")).toBe(5);
  });

  it("should return length of a for empty b", () => {
    expect(levenshteinDistance("octto", "")).toBe(5);
  });

  it("should calculate single character substitution", () => {
    expect(levenshteinDistance("octto", "octta")).toBe(1);
  });

  it("should calculate single character insertion", () => {
    expect(levenshteinDistance("octto", "octtoo")).toBe(1);
  });

  it("should calculate single character deletion", () => {
    expect(levenshteinDistance("octto", "octt")).toBe(1);
  });

  it("should handle typical typos", () => {
    expect(levenshteinDistance("octto", "octo")).toBe(1); // missing t
    expect(levenshteinDistance("bootstrapper", "bootsrapper")).toBe(1); // typo
  });
});

describe("warnUnknownAgents", () => {
  let consoleSpy: ReturnType<typeof spyOn>;

  beforeEach(() => {
    consoleSpy = spyOn(console, "warn").mockImplementation(() => {});
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it("should not warn for valid agent names", () => {
    warnUnknownAgents({ octto: ["instruction"], bootstrapper: ["instruction"] });
    expect(consoleSpy).not.toHaveBeenCalled();
  });

  it("should not warn for undefined fragments", () => {
    warnUnknownAgents(undefined);
    expect(consoleSpy).not.toHaveBeenCalled();
  });

  it("should not warn for empty fragments", () => {
    warnUnknownAgents({});
    expect(consoleSpy).not.toHaveBeenCalled();
  });

  it("should warn for unknown agent name", () => {
    warnUnknownAgents({ unknown_agent: ["instruction"] } as any);
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('[octto] Unknown agent "unknown_agent" in fragments'),
    );
  });

  it("should suggest similar agent name for typos", () => {
    warnUnknownAgents({ octo: ["instruction"] } as any);
    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Did you mean "octto"?'));
  });

  it("should suggest multiple similar names when applicable", () => {
    // "prob" is close to "probe"
    warnUnknownAgents({ prob: ["instruction"] } as any);
    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Did you mean "probe"?'));
  });
});