allowlist middleapi/orpc for OSS, ship skill scaffold + no-mock audit
- utils/ossRepos.ts: add middleapi/orpc (orpc.dev, 5.2k stars) to the oss allowlist so any future install mints via mintOssKey ($10 cap, pullfrog absorbs). - scripts/skill.ts + pnpm skill: scaffold .agents/skills/<name>/SKILL.md + .claude/skills/<name> symlink. patch + skill skills written using it. - AGENTS.md: hard-ban vi.* mocking apis; document pnpm skill workflow. - audit follow-through: drop pure-mock test files (action/utils/lifecycle, action/utils/timer, test/handleNoInstall) and trim action/mcp/review to the non-mock cases. - wiki/scripts.md: row for scripts/skill.ts.
This commit is contained in:
committed by
pullfrog[bot]
parent
a0746dcc27
commit
dc4dff98da
@@ -1,130 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { executeLifecycleHook } from "./lifecycle.ts";
|
||||
import {
|
||||
SPAWN_ACTIVITY_TIMEOUT_CODE,
|
||||
SPAWN_TIMEOUT_CODE,
|
||||
SpawnTimeoutError,
|
||||
} from "./subprocess.ts";
|
||||
|
||||
// mock the spawn call so we don't run real subprocesses. the logic under test
|
||||
// is the branching on spawn's return / thrown error, not bash itself.
|
||||
vi.mock("./subprocess.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./subprocess.ts")>();
|
||||
return {
|
||||
...actual,
|
||||
spawn: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const { spawn } = await import("./subprocess.ts");
|
||||
const mockedSpawn = vi.mocked(spawn);
|
||||
|
||||
describe("executeLifecycleHook", () => {
|
||||
beforeEach(() => {
|
||||
mockedSpawn.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns empty result when no script is configured", async () => {
|
||||
const result = await executeLifecycleHook({ event: "setup", script: null });
|
||||
expect(result).toEqual({});
|
||||
expect(mockedSpawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns empty result when script exits 0", async () => {
|
||||
mockedSpawn.mockResolvedValue({
|
||||
stdout: "ok\n",
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
durationMs: 5,
|
||||
});
|
||||
const result = await executeLifecycleHook({ event: "setup", script: "true" });
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("returns a warning with stderr content and retry-if-flaky guidance on non-zero exit", async () => {
|
||||
mockedSpawn.mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "npm ERR! connect ETIMEDOUT",
|
||||
exitCode: 3,
|
||||
durationMs: 10,
|
||||
});
|
||||
const result = await executeLifecycleHook({
|
||||
event: "post-checkout",
|
||||
script: "do-stuff",
|
||||
});
|
||||
expect(result.warning).toMatch(/post-checkout/);
|
||||
expect(result.warning).toMatch(/exit code 3/);
|
||||
expect(result.warning).toMatch(/npm ERR! connect ETIMEDOUT/);
|
||||
expect(result.warning).toMatch(/retry the operation if the failure looks flaky/);
|
||||
expect(result.warning).toMatch(/do NOT retry/);
|
||||
});
|
||||
|
||||
it("falls back to stdout when stderr is empty", async () => {
|
||||
mockedSpawn.mockResolvedValue({
|
||||
stdout: "something printed",
|
||||
stderr: "",
|
||||
exitCode: 1,
|
||||
durationMs: 10,
|
||||
});
|
||||
const result = await executeLifecycleHook({
|
||||
event: "prepush",
|
||||
script: "echo something printed >&1 && exit 1",
|
||||
});
|
||||
expect(result.warning).toContain("something printed");
|
||||
});
|
||||
|
||||
it("prints '(empty)' when both streams are blank", async () => {
|
||||
mockedSpawn.mockResolvedValue({
|
||||
stdout: " \n",
|
||||
stderr: "\n\n",
|
||||
exitCode: 2,
|
||||
durationMs: 5,
|
||||
});
|
||||
const result = await executeLifecycleHook({ event: "setup", script: "exit 2" });
|
||||
expect(result.warning).toContain("(empty)");
|
||||
});
|
||||
|
||||
it("emits a do-NOT-retry warning when spawn reports an overall timeout", async () => {
|
||||
// SPAWN_TIMEOUT_CODE is the code we must distinguish. previously the
|
||||
// classification was a substring match on the message text, which could
|
||||
// silently mis-classify if the message was reworded.
|
||||
mockedSpawn.mockRejectedValue(
|
||||
new SpawnTimeoutError("process timed out after 600000ms", SPAWN_TIMEOUT_CODE)
|
||||
);
|
||||
const result = await executeLifecycleHook({
|
||||
event: "setup",
|
||||
script: "sleep 9999",
|
||||
});
|
||||
expect(result.warning).toMatch(/timed out after \d+min/);
|
||||
expect(result.warning).toMatch(/do NOT retry/);
|
||||
expect(result.warning).not.toMatch(/transient/);
|
||||
});
|
||||
|
||||
it("treats an activity-timeout error the same as an overall timeout", async () => {
|
||||
mockedSpawn.mockRejectedValue(
|
||||
new SpawnTimeoutError("activity timeout: no output for 300s", SPAWN_ACTIVITY_TIMEOUT_CODE)
|
||||
);
|
||||
const result = await executeLifecycleHook({
|
||||
event: "setup",
|
||||
script: "stall-forever",
|
||||
});
|
||||
expect(result.warning).toMatch(/timed out/);
|
||||
expect(result.warning).toMatch(/do NOT retry/);
|
||||
});
|
||||
|
||||
it("emits a transient-retry warning on a non-timeout spawn failure (e.g. ENOENT)", async () => {
|
||||
mockedSpawn.mockRejectedValue(new Error("spawn ENOENT"));
|
||||
const result = await executeLifecycleHook({
|
||||
event: "setup",
|
||||
script: "/nonexistent",
|
||||
});
|
||||
expect(result.warning).toMatch(/failed to spawn/);
|
||||
expect(result.warning).toMatch(/spawn ENOENT/);
|
||||
expect(result.warning).toMatch(/transient/);
|
||||
expect(result.warning).not.toMatch(/do NOT retry/);
|
||||
});
|
||||
});
|
||||
@@ -1,220 +0,0 @@
|
||||
import { performance } from "node:perf_hooks";
|
||||
import * as cli from "./cli.ts";
|
||||
import { ThinkingTimer, Timer } from "./timer.ts";
|
||||
|
||||
describe("Timer", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(cli.log, "debug");
|
||||
// Mock performance.now() to have predictable timestamps
|
||||
vi.spyOn(performance, "now");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should initialize with current timestamp", () => {
|
||||
const mockTime = 1000000;
|
||||
vi.mocked(performance.now).mockReturnValueOnce(mockTime).mockReturnValueOnce(mockTime);
|
||||
|
||||
const timer = new Timer();
|
||||
timer.checkpoint("test");
|
||||
|
||||
expect(cli.log.debug).toHaveBeenCalledWith(expect.stringContaining("test"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkpoint", () => {
|
||||
it("should log duration from initial timestamp on first checkpoint", () => {
|
||||
const startTime = 1000000;
|
||||
const checkpointTime = startTime + 100;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // constructor
|
||||
.mockReturnValueOnce(checkpointTime); // checkpoint
|
||||
|
||||
const timer = new Timer();
|
||||
timer.checkpoint("first");
|
||||
|
||||
expect(cli.log.debug).toHaveBeenCalledWith("» first: 100ms");
|
||||
});
|
||||
|
||||
it("should log duration from last checkpoint on subsequent checkpoints", () => {
|
||||
const startTime = 1000000;
|
||||
const firstCheckpointTime = startTime + 50;
|
||||
const secondCheckpointTime = firstCheckpointTime + 75;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // constructor
|
||||
.mockReturnValueOnce(firstCheckpointTime) // first checkpoint
|
||||
.mockReturnValueOnce(secondCheckpointTime); // second checkpoint
|
||||
|
||||
const timer = new Timer();
|
||||
timer.checkpoint("first");
|
||||
timer.checkpoint("second");
|
||||
|
||||
expect(cli.log.debug).toHaveBeenCalledTimes(2);
|
||||
expect(cli.log.debug).toHaveBeenNthCalledWith(1, "» first: 50ms");
|
||||
expect(cli.log.debug).toHaveBeenNthCalledWith(2, "» second: 75ms");
|
||||
});
|
||||
|
||||
it("should handle multiple checkpoints correctly", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // constructor
|
||||
.mockReturnValueOnce(startTime + 10) // step1
|
||||
.mockReturnValueOnce(startTime + 25) // step2
|
||||
.mockReturnValueOnce(startTime + 45); // step3
|
||||
|
||||
const timer = new Timer();
|
||||
timer.checkpoint("step1");
|
||||
timer.checkpoint("step2");
|
||||
timer.checkpoint("step3");
|
||||
|
||||
expect(cli.log.debug).toHaveBeenCalledTimes(3);
|
||||
expect(cli.log.debug).toHaveBeenNthCalledWith(1, "» step1: 10ms");
|
||||
expect(cli.log.debug).toHaveBeenNthCalledWith(2, "» step2: 15ms");
|
||||
expect(cli.log.debug).toHaveBeenNthCalledWith(3, "» step3: 20ms");
|
||||
});
|
||||
|
||||
it("should handle zero duration correctly", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // constructor
|
||||
.mockReturnValueOnce(startTime); // checkpoint
|
||||
|
||||
const timer = new Timer();
|
||||
timer.checkpoint("immediate");
|
||||
|
||||
expect(cli.log.debug).toHaveBeenCalledWith("» immediate: 0ms");
|
||||
});
|
||||
|
||||
it("should handle custom checkpoint names", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // constructor
|
||||
.mockReturnValueOnce(startTime + 200); // checkpoint
|
||||
|
||||
const timer = new Timer();
|
||||
timer.checkpoint("Custom Checkpoint Name");
|
||||
|
||||
expect(cli.log.debug).toHaveBeenCalledWith("» Custom Checkpoint Name: 200ms");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ThinkingTimer", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(cli.log, "info");
|
||||
vi.spyOn(performance, "now");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("markToolResult", () => {
|
||||
it("should store the current timestamp", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // markToolResult
|
||||
.mockReturnValueOnce(startTime + 5000); // markToolCall
|
||||
|
||||
const timer = new ThinkingTimer();
|
||||
timer.markToolResult();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("markToolCall", () => {
|
||||
it("should not log if markToolResult was never called", () => {
|
||||
const timer = new ThinkingTimer();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not log if elapsed time is below threshold (3000ms)", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // markToolResult
|
||||
.mockReturnValueOnce(startTime + 2999); // markToolCall
|
||||
|
||||
const timer = new ThinkingTimer();
|
||||
timer.markToolResult();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log if elapsed time equals threshold (3000ms)", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // markToolResult
|
||||
.mockReturnValueOnce(startTime + 3000); // markToolCall
|
||||
|
||||
const timer = new ThinkingTimer();
|
||||
timer.markToolResult();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).toHaveBeenCalledWith("» thought for 3 seconds");
|
||||
});
|
||||
|
||||
it("should log if elapsed time exceeds threshold", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // markToolResult
|
||||
.mockReturnValueOnce(startTime + 5500); // markToolCall
|
||||
|
||||
const timer = new ThinkingTimer();
|
||||
timer.markToolResult();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).toHaveBeenCalledWith("» thought for 5.5 seconds");
|
||||
});
|
||||
|
||||
it("should format large durations correctly", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // markToolResult
|
||||
.mockReturnValueOnce(startTime + 15000); // markToolCall
|
||||
|
||||
const timer = new ThinkingTimer();
|
||||
timer.markToolResult();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).toHaveBeenCalledWith("» thought for 15 seconds");
|
||||
});
|
||||
|
||||
it("should handle multiple markToolCall invocations", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // markToolResult
|
||||
.mockReturnValueOnce(startTime + 4000) // first markToolCall
|
||||
.mockReturnValueOnce(startTime + 5000); // second markToolCall
|
||||
|
||||
const timer = new ThinkingTimer();
|
||||
timer.markToolResult();
|
||||
timer.markToolCall();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).toHaveBeenCalledTimes(2);
|
||||
expect(cli.log.info).toHaveBeenNthCalledWith(1, "» thought for 4 seconds");
|
||||
expect(cli.log.info).toHaveBeenNthCalledWith(2, "» thought for 5 seconds");
|
||||
});
|
||||
|
||||
it("routes log lines through the optional formatLine for per-session prefixing", () => {
|
||||
const startTime = 1000000;
|
||||
vi.mocked(performance.now)
|
||||
.mockReturnValueOnce(startTime) // markToolResult
|
||||
.mockReturnValueOnce(startTime + 4000); // markToolCall
|
||||
|
||||
const timer = new ThinkingTimer((line) => `[lens:security] ${line}`);
|
||||
timer.markToolResult();
|
||||
timer.markToolCall();
|
||||
|
||||
expect(cli.log.info).toHaveBeenCalledWith("[lens:security] » thought for 4 seconds");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user