From bf6f5d661955957a16e8fe4c2f582c5e2af2dcb0 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 15:58:46 +0000 Subject: [PATCH 1/2] test(pi-tools): cover CLI search skill and log helper --- .../pi-tools/test/cli-search-tools.test.ts | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 packages/pi-tools/test/cli-search-tools.test.ts diff --git a/packages/pi-tools/test/cli-search-tools.test.ts b/packages/pi-tools/test/cli-search-tools.test.ts new file mode 100644 index 0000000..a8075fb --- /dev/null +++ b/packages/pi-tools/test/cli-search-tools.test.ts @@ -0,0 +1,313 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const PACKAGE_ROOT = path.resolve(import.meta.dir, ".."); +const SKILL_PATH = path.join(PACKAGE_ROOT, "skills", "cli-search-tools", "SKILL.md"); +const SCRIPT_PATH = path.join( + PACKAGE_ROOT, + "skills", + "cli-search-tools", + "scripts", + "search-pi-logs.sh", +); + +describe("cli-search-tools package contract", () => { + const pkg = JSON.parse( + fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"), + ); + + test("registers and publishes the skill with its helper", () => { + expect(pkg.pi.skills).toEqual(["./skills/cli-search-tools/SKILL.md"]); + expect(pkg.files).toEqual( + expect.arrayContaining([ + "skills/cli-search-tools/SKILL.md", + "skills/cli-search-tools/scripts/search-pi-logs.sh", + ]), + ); + + const packed = Bun.spawnSync({ + cmd: ["npm", "pack", "--dry-run", "--ignore-scripts", "--json"], + cwd: PACKAGE_ROOT, + stdout: "pipe", + stderr: "pipe", + }); + expect(packed.exitCode).toBe(0); + + const files = JSON.parse(packed.stdout.toString())[0].files.map( + ({ path: packedPath }: { path: string }) => packedPath, + ); + expect(files).toEqual( + expect.arrayContaining([ + "skills/cli-search-tools/SKILL.md", + "skills/cli-search-tools/scripts/search-pi-logs.sh", + ]), + ); + }); + + test("keeps valid skill metadata and the FFF-first guidance", () => { + const skill = fs.readFileSync(SKILL_PATH, "utf8"); + const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/); + + expect(frontmatter).not.toBeNull(); + expect(frontmatter![1]).toMatch(/^name:\s*cli-search-tools\s*$/m); + expect(frontmatter![1]).toMatch(/^description:\s*\S.+$/m); + expect(skill).toContain("Prefer the FFF-backed extension tools"); + expect(skill).toContain("`fff_find`, `fff_grep`, `tgrep`, `@`-completion"); + }); + + test("ships the helper as an executable Bash script", () => { + expect(fs.readFileSync(SCRIPT_PATH, "utf8")).toStartWith("#!/usr/bin/env bash\n"); + expect(fs.statSync(SCRIPT_PATH).mode & 0o111).not.toBe(0); + + const syntax = Bun.spawnSync({ + cmd: ["bash", "-n", SCRIPT_PATH], + stdout: "pipe", + stderr: "pipe", + }); + expect(syntax.exitCode).toBe(0); + }); +}); + +describe("search-pi-logs.sh", () => { + let fixtureRoot: string; + let sessionDir: string; + let fzfArgsPath: string; + + beforeEach(() => { + fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "search-pi-logs-")); + sessionDir = path.join(fixtureRoot, "sessions"); + fzfArgsPath = path.join(fixtureRoot, "fzf-args"); + const binDir = path.join(fixtureRoot, "bin"); + + fs.mkdirSync(sessionDir); + fs.mkdirSync(binDir); + const fzfPath = path.join(binDir, "fzf"); + fs.writeFileSync( + fzfPath, + '#!/usr/bin/env bash\nprintf "%s\\n" "$@" > "$FZF_ARGS_PATH"\ncat\n', + ); + fs.chmodSync(fzfPath, 0o755); + }); + + afterEach(() => { + fs.rmSync(fixtureRoot, { recursive: true, force: true }); + }); + + test("prints help without reading the session directory", () => { + fs.rmSync(sessionDir, { recursive: true, force: true }); + + const result = runScript(["--help"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.toString()).toContain( + "Usage: " + SCRIPT_PATH + " [--zoekterm term] [--type error|user|all]", + ); + expect(result.stderr.toString()).toBe(""); + }); + + test("formats all messages with defaults and sorts them by timestamp", () => { + writeJsonl("nested/later.jsonl", [ + { + timestamp: "2026-08-03T11:22:33.999Z", + message: { + role: "assistant", + model: "gpt-5", + provider: "openai", + stopReason: "stop", + content: [ + { type: "text", text: "finished" }, + { type: "toolCall", name: "ignored" }, + ], + }, + }, + ]); + writeJsonl("earlier.jsonl", [ + { + timestamp: "2026-08-03T09:00:00Z", + message: { role: "user", content: [{ type: "text", text: "hello" }] }, + }, + ]); + + const result = runScript([]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.toString().trim().split("\n")).toEqual([ + "2026-08-03T09:00:00 | - | - | user | | hello", + "2026-08-03T11:22:33 | gpt-5 | openai | assistant | stop | finished", + ]); + expect(fs.existsSync(fzfArgsPath)).toBe(false); + }); + + test("filters error messages and includes their error details", () => { + writeJsonl("errors.jsonl", [ + { + timestamp: "2026-08-03T10:00:00Z", + message: { + role: "assistant", + model: "glm-5", + provider: "zai", + stopReason: "error", + errorMessage: "Rate limit exceeded", + }, + }, + { + timestamp: "2026-08-03T10:01:00Z", + message: { role: "assistant", stopReason: "stop" }, + }, + ]); + + const result = runScript(["--type", "error"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.toString().trim()).toBe( + "2026-08-03T10:00:00 | glm-5 | zai | assistant | error Rate limit exceeded |", + ); + }); + + test("supports user, model, and provider filters through short options", () => { + writeJsonl("messages.jsonl", [ + { + timestamp: "2026-08-03T10:00:00Z", + message: { + role: "user", + model: "GLM-5", + provider: "Z.AI", + content: [{ type: "text", text: "matching" }], + }, + }, + { + timestamp: "2026-08-03T10:01:00Z", + message: { + role: "user", + model: "gpt-5", + provider: "openai", + content: [{ type: "text", text: "wrong backend" }], + }, + }, + { + timestamp: "2026-08-03T10:02:00Z", + message: { + role: "assistant", + model: "GLM-5", + provider: "Z.AI", + content: [{ type: "text", text: "wrong role" }], + }, + }, + ]); + + const result = runScript(["-t", "user", "-m", "^glm", "-p", "z\\.ai$"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.toString()).toContain("matching"); + expect(result.stdout.toString()).not.toContain("wrong backend"); + expect(result.stdout.toString()).not.toContain("wrong role"); + }); + + test("matches a search term in error text or text content case-insensitively", () => { + writeJsonl("search.jsonl", [ + { + timestamp: "2026-08-03T10:00:00Z", + message: { + role: "assistant", + errorMessage: "Needle in an ERROR", + content: [], + }, + }, + { + timestamp: "2026-08-03T10:01:00Z", + message: { + role: "user", + content: [ + { type: "toolCall", text: "needle should be ignored here" }, + { type: "text", text: "a NEEDLE in user text" }, + ], + }, + }, + { + timestamp: "2026-08-03T10:02:00Z", + message: { + role: "user", + content: [{ type: "text", text: "unrelated" }], + }, + }, + ]); + + const result = runScript(["--zoekterm", "needle"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.toString().trim().split("\n")).toHaveLength(2); + expect(result.stdout.toString()).not.toContain("unrelated"); + expect(fs.readFileSync(fzfArgsPath, "utf8").trim().split("\n")).toEqual([ + "--filter", + "needle", + "--no-sort", + ]); + }); + + test("accepts a positional search term", () => { + writeJsonl("search.jsonl", [ + { + timestamp: "2026-08-03T10:00:00Z", + message: { + role: "user", + content: [{ type: "text", text: "find this phrase" }], + }, + }, + ]); + + const result = runScript(["this phrase"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.toString()).toContain("find this phrase"); + expect(fs.readFileSync(fzfArgsPath, "utf8")).toContain("this phrase"); + }); + + test("returns no output for an empty session directory", () => { + const result = runScript([]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.toString()).toBe(""); + expect(result.stderr.toString()).toBe(""); + }); + + test("fails for malformed JSONL instead of silently returning partial data", () => { + fs.writeFileSync(path.join(sessionDir, "broken.jsonl"), "{not-json}\n"); + + const result = runScript([]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr.toString()).toContain("parse error"); + }); + + test("fails when an option value is missing", () => { + const result = runScript(["--model"]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr.toString()).toContain("unbound variable"); + }); + + function writeJsonl(relativePath: string, records: unknown[]): void { + const target = path.join(sessionDir, relativePath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync( + target, + records.map((record) => JSON.stringify(record)).join("\n") + "\n", + ); + } + + function runScript(args: string[]) { + return Bun.spawnSync({ + cmd: ["bash", SCRIPT_PATH, ...args], + env: { + ...process.env, + FZF_ARGS_PATH: fzfArgsPath, + PATH: `${path.join(fixtureRoot, "bin")}:${process.env.PATH ?? ""}`, + PI_SESSION_DIR: sessionDir, + }, + stdout: "pipe", + stderr: "pipe", + }); + } +}); From e87d63599be5a9248d883fadcb45130f138f5c1e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 18:37:12 +0000 Subject: [PATCH 2/2] test(fff): give cwd content search longer to index The file scan can finish before the content index sees a just-written file. A 2s poll flaked on linux CI; wait up to 10s, matching the re-index budget. Co-authored-by: OnlineChef --- tests/programmatic_search_spec.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/programmatic_search_spec.lua b/tests/programmatic_search_spec.lua index c8b72c4..bad3681 100644 --- a/tests/programmatic_search_spec.lua +++ b/tests/programmatic_search_spec.lua @@ -218,10 +218,9 @@ describe('programmatic search APIs', function() local before = fff.content_search(marker) assert.are.equal(0, #before.items, 'marker leaked into primary fff tree') - -- Poll instead of asserting on the first grep: the index of the new root - -- can lag a mkdir by a few ms on CI, which flaked on linux too. + -- Content index can lag the file scan after a cwd swap. 2s flaked on linux CI. local result - vim.wait(2000, function() + vim.wait(10000, function() result = fff.content_search(marker, { cwd = sandbox_root }) return #result.items > 0 end, 50)