tests: remove useless assertations
This commit is contained in:
+1
-1
@@ -92,7 +92,7 @@
|
||||
"build": "bun run --workspaces --if-present build",
|
||||
"build:native": "bun --cwd=packages/natives run build",
|
||||
"test": "bun run --parallel test:ts test:rs",
|
||||
"test:ts": "GITHUB_ACTIONS=0 bun run --workspaces --if-present test -- --only-failures",
|
||||
"test:ts": "GITHUB_ACTIONS= bun run --workspaces --if-present test -- --only-failures",
|
||||
"test:rs": "bun scripts/run-rs-task.ts test:rs",
|
||||
"check": "bun run --parallel check:ts check:rs",
|
||||
"check:ts": "bun run check:tools && bun run --workspaces --if-present check",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed llama.cpp/OpenAI Responses parallel tool calls losing arguments when `function_call_arguments.done` events omit `output_index` and `item_id`, by routing those identifierless final-argument events through the open function calls in item order. ([#1970](https://github.com/can1357/oh-my-pi/issues/1970))
|
||||
- Fixed local Ollama (`openai-responses`) turns failing with HTTP 400 `invalid reasoning value: "minimal"` when a discovered model ran with `minimal` (or `xhigh`) thinking. Ollama's OpenAI-compatible `reasoning.effort` only accepts `high|medium|low|max|none`, so discovered reasoning-capable Ollama models now carry a `compat.reasoningEffortMap` remapping `minimal → low` and `xhigh → max`; non-reasoning models are left untouched.
|
||||
|
||||
## [15.9.2] - 2026-06-05
|
||||
|
||||
|
||||
@@ -53,6 +53,48 @@ describe("ollama local provider discovery", () => {
|
||||
expect(model?.thinking).toEqual({ mode: "effort", minLevel: Effort.Minimal, maxLevel: Effort.High });
|
||||
expect(model?.input).toEqual(["text", "image"]);
|
||||
});
|
||||
|
||||
test("remaps Ollama's unsupported reasoning levels and skips non-reasoning models", async () => {
|
||||
global.fetch = vi.fn(async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url === "http://127.0.0.1:11434/v1/models") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
object: "list",
|
||||
data: [
|
||||
{ id: "gemma4:e4b", object: "model" },
|
||||
{ id: "llama-plain:latest", object: "model" },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "http://127.0.0.1:11434/api/show") {
|
||||
const body = JSON.parse(String(init?.body ?? "{}")) as { model?: string };
|
||||
const thinking = body.model === "gemma4:e4b";
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
capabilities: thinking ? ["completion", "tools", "thinking"] : ["completion", "tools"],
|
||||
model_info: {},
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const models = await ollamaModelManagerOptions().fetchDynamicModels?.();
|
||||
const reasoningModel = models?.find(candidate => candidate.id === "gemma4:e4b");
|
||||
const plainModel = models?.find(candidate => candidate.id === "llama-plain:latest");
|
||||
|
||||
// Ollama's OpenAI-compatible endpoint rejects "minimal"/"xhigh" with HTTP 400;
|
||||
// reasoning models must remap them onto accepted levels (low/max).
|
||||
expect(reasoningModel?.reasoning).toBe(true);
|
||||
expect(reasoningModel?.compat?.reasoningEffortMap).toMatchObject({ minimal: "low", xhigh: "max" });
|
||||
// Non-reasoning models never send an effort, so they carry no remap.
|
||||
expect(plainModel?.reasoning).toBe(false);
|
||||
expect(plainModel?.compat?.reasoningEffortMap).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ollama tool forcing", () => {
|
||||
|
||||
@@ -87,12 +87,6 @@ describe("hostHasInheritableConsole", () => {
|
||||
__resetWindowsConsoleProbeCache();
|
||||
});
|
||||
|
||||
it("returns a boolean (the integration boundary always commits to a decision)", () => {
|
||||
// Whatever the runtime is, the function must yield a concrete
|
||||
// boolean: kernel spawn cannot take an indeterminate windowsHide.
|
||||
expect(typeof hostHasInheritableConsole()).toBe("boolean");
|
||||
});
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
it("matches the TTY-OR fallback off-Windows", () => {
|
||||
// Off-Windows, `windowsHide` is a no-op anyway, but we still
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
extractRetryHint,
|
||||
getAgentDbPath,
|
||||
getInstallId,
|
||||
isBunTestRuntime,
|
||||
isEnoent,
|
||||
isUnexpectedSocketCloseMessage,
|
||||
logger,
|
||||
@@ -1036,6 +1037,7 @@ export class AgentSession {
|
||||
|
||||
#acquirePowerAssertion(): void {
|
||||
if (process.platform !== "darwin") return;
|
||||
if (isBunTestRuntime()) return;
|
||||
if (this.#powerAssertion) return;
|
||||
const idle = this.settings.get("power.preventIdleSleep");
|
||||
const system = this.settings.get("power.preventSystemSleep");
|
||||
|
||||
@@ -492,20 +492,6 @@ describe("wave 3 commands", () => {
|
||||
expect(output[0]).toContain("Usage: /memory");
|
||||
});
|
||||
|
||||
it("/memory view: outputs memory payload (or empty message)", async () => {
|
||||
const { output, runtime } = createRuntime();
|
||||
const result = await executeAcpBuiltinSlashCommand("/memory view", runtime);
|
||||
expect(result).toEqual({ consumed: true });
|
||||
expect(output.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("/memory (no args): defaults to view", async () => {
|
||||
const { output, runtime } = createRuntime();
|
||||
const result = await executeAcpBuiltinSlashCommand("/memory", runtime);
|
||||
expect(result).toEqual({ consumed: true });
|
||||
expect(output.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// /todo start fuzzy match
|
||||
it("/todo start: finds pending task by substring and starts it", async () => {
|
||||
const { output, session, runtime } = createRuntime();
|
||||
@@ -667,37 +653,6 @@ describe("wave 4 commands", () => {
|
||||
});
|
||||
|
||||
// /plugins
|
||||
it("/plugins list: outputs without throwing when registries are empty", async () => {
|
||||
const { MarketplaceManager } = await import("../src/extensibility/plugins/marketplace");
|
||||
const { PluginManager } = await import("../src/extensibility/plugins");
|
||||
const listInstalledSpy = spyOn(MarketplaceManager.prototype, "listInstalledPlugins").mockResolvedValue([]);
|
||||
const npmListSpy = spyOn(PluginManager.prototype, "list").mockResolvedValue([]);
|
||||
try {
|
||||
const { output, runtime } = createRuntime();
|
||||
const result = await executeAcpBuiltinSlashCommand("/plugins list", runtime);
|
||||
expect(result).toEqual({ consumed: true });
|
||||
expect(output.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
listInstalledSpy.mockRestore();
|
||||
npmListSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("/plugins (no args): defaults to list", async () => {
|
||||
const { MarketplaceManager } = await import("../src/extensibility/plugins/marketplace");
|
||||
const { PluginManager } = await import("../src/extensibility/plugins");
|
||||
const listInstalledSpy = spyOn(MarketplaceManager.prototype, "listInstalledPlugins").mockResolvedValue([]);
|
||||
const npmListSpy = spyOn(PluginManager.prototype, "list").mockResolvedValue([]);
|
||||
try {
|
||||
const { output, runtime } = createRuntime();
|
||||
const result = await executeAcpBuiltinSlashCommand("/plugins", runtime);
|
||||
expect(result).toEqual({ consumed: true });
|
||||
expect(output.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
listInstalledSpy.mockRestore();
|
||||
npmListSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
// /todo start with in_progress status in fuzzy list
|
||||
it("/todo start: resolves ambiguous matches by preferring active tasks", async () => {
|
||||
|
||||
@@ -164,7 +164,6 @@ describe("ACP stdout hygiene", () => {
|
||||
proc.stdin.flush();
|
||||
|
||||
const firstLine = await readFirstFrame(proc.stdout);
|
||||
expect(firstLine.length).toBeGreaterThan(0);
|
||||
expect(firstLine[0]).toBe("{");
|
||||
|
||||
const message = JSON.parse(firstLine) as {
|
||||
|
||||
@@ -115,31 +115,6 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("AgentSession compaction e2e",
|
||||
expect(firstMsg.role).toBe("compactionSummary");
|
||||
}, 120000);
|
||||
|
||||
it("should maintain valid session state after compaction", async () => {
|
||||
await createSession();
|
||||
|
||||
// Build up history
|
||||
await session.prompt("What is the capital of France? One word answer.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
await session.prompt("What is the capital of Germany? One word answer.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
// Compact
|
||||
await session.compact();
|
||||
|
||||
// Session should still be usable
|
||||
await session.prompt("What is the capital of Italy? One word answer.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
// Should have messages after compaction
|
||||
expect(session.messages.length).toBeGreaterThan(0);
|
||||
|
||||
// The agent should have responded
|
||||
const assistantMessages = session.messages.filter(m => m.role === "assistant");
|
||||
expect(assistantMessages.length).toBeGreaterThan(0);
|
||||
}, 180000);
|
||||
|
||||
it("should persist compaction to session file", async () => {
|
||||
await createSession();
|
||||
|
||||
@@ -206,9 +181,5 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("AgentSession compaction e2e",
|
||||
);
|
||||
// Manual compaction doesn't emit auto_compaction events
|
||||
expect(autoCompactionEvents.length).toBe(0);
|
||||
|
||||
// Regular events should have been emitted
|
||||
const messageEndEvents = events.filter(e => e.type === "message_end");
|
||||
expect(messageEndEvents.length).toBeGreaterThan(0);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
||||
await waitFor(() => session.isStreaming);
|
||||
|
||||
// steer should work while streaming
|
||||
expect(() => session.steer("Steering message")).not.toThrow();
|
||||
await session.steer("Steer while streaming");
|
||||
expect(session.queuedMessageCount).toBe(1);
|
||||
|
||||
// Cleanup
|
||||
@@ -147,7 +147,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
||||
await waitFor(() => session.isStreaming);
|
||||
|
||||
// followUp should work while streaming
|
||||
expect(() => session.followUp("Follow-up message")).not.toThrow();
|
||||
await session.followUp("Follow-up while streaming");
|
||||
expect(session.queuedMessageCount).toBe(1);
|
||||
|
||||
// Cleanup
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Agent } from "@oh-my-pi/pi-agent-core";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-ai";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
||||
import { TodoTool } from "@oh-my-pi/pi-coding-agent/tools";
|
||||
import { Snowflake } from "@oh-my-pi/pi-utils";
|
||||
|
||||
/**
|
||||
* Regression test: /new (AgentSession.newSession) must fully switch to a new session file
|
||||
* before the call resolves.
|
||||
*
|
||||
* If it doesn't, UI code that reloads todos immediately after /new will read the old
|
||||
* session artifact dir and keep showing stale todos.
|
||||
*/
|
||||
describe("AgentSession newSession clears todo artifacts", () => {
|
||||
let tempDir: string;
|
||||
let session: AgentSession;
|
||||
let sessionManager: SessionManager;
|
||||
let authStorage: AuthStorage | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = path.join(os.tmpdir(), `pi-new-session-todos-test-${Snowflake.next()}`);
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
sessionManager = SessionManager.create(tempDir, tempDir);
|
||||
const settings = Settings.isolated();
|
||||
authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db"));
|
||||
const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml"));
|
||||
|
||||
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
|
||||
if (!model) {
|
||||
throw new Error("Test model not found in registry");
|
||||
}
|
||||
|
||||
const toolSession: ToolSession = {
|
||||
cwd: tempDir,
|
||||
hasUI: false,
|
||||
getSessionFile: () => sessionManager.getSessionFile() ?? null,
|
||||
getSessionSpawns: () => "*",
|
||||
settings,
|
||||
};
|
||||
|
||||
const agent = new Agent({
|
||||
getApiKey: () => "test",
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: ["test"],
|
||||
tools: [new TodoTool(toolSession)],
|
||||
},
|
||||
});
|
||||
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
sessionManager,
|
||||
settings,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
// Must subscribe to enable session persistence hooks
|
||||
session.subscribe(() => {});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (session) {
|
||||
await session.dispose();
|
||||
}
|
||||
authStorage?.close();
|
||||
authStorage = undefined;
|
||||
if (tempDir && fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("should not carry over todo state to the new session branch", async () => {
|
||||
const oldSessionFile = session.sessionFile;
|
||||
expect(oldSessionFile).toBeDefined();
|
||||
|
||||
session.setTodoPhases([
|
||||
{
|
||||
name: "Tasks",
|
||||
tasks: [{ content: "do the thing", status: "pending" }],
|
||||
},
|
||||
]);
|
||||
expect(session.getTodoPhases()).toHaveLength(1);
|
||||
expect(session.getTodoPhases()[0]?.tasks).toHaveLength(1);
|
||||
await session.newSession();
|
||||
|
||||
const newSessionFile = session.sessionFile;
|
||||
expect(newSessionFile).toBeDefined();
|
||||
expect(newSessionFile).not.toBe(oldSessionFile);
|
||||
|
||||
expect(session.getTodoPhases()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should clear stale todo cache when branching from the first user message", async () => {
|
||||
sessionManager.appendMessage({
|
||||
role: "user",
|
||||
content: "start task",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const branchCandidates = session.getUserMessagesForBranching();
|
||||
expect(branchCandidates).toHaveLength(1);
|
||||
|
||||
session.setTodoPhases([
|
||||
{
|
||||
name: "Execution",
|
||||
tasks: [{ content: "stale from old branch", status: "in_progress" }],
|
||||
},
|
||||
]);
|
||||
expect(session.getTodoPhases()).toHaveLength(1);
|
||||
|
||||
const result = await session.branch(branchCandidates[0].entryId);
|
||||
expect(result.cancelled).toBe(false);
|
||||
expect(result.selectedText).toBe("start task");
|
||||
expect(session.getTodoPhases()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -496,7 +496,6 @@ describe("AgentSession OpenAI Responses replay boundaries", () => {
|
||||
|
||||
await session.reload();
|
||||
|
||||
expect(() => session.sessionManager.captureState()).not.toThrow();
|
||||
expect(session.sessionFile).toBe(originalSessionFile);
|
||||
});
|
||||
|
||||
|
||||
@@ -216,14 +216,6 @@ describe("BashExecutionComponent #clampDisplayLine", () => {
|
||||
expect(output).toContain(`[1 visible columns omitted]`);
|
||||
});
|
||||
|
||||
it("handles string with 0 visible width (empty after ANSI removal)", () => {
|
||||
const onlyAnsi = "\x1b[0m\x1b[1m\x1b[2m";
|
||||
const component = createComponentWithOutput(onlyAnsi);
|
||||
const output = component.getOutput();
|
||||
|
||||
expect(output).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles empty string", () => {
|
||||
const component = createComponentWithOutput("");
|
||||
const output = component.getOutput();
|
||||
|
||||
@@ -157,24 +157,6 @@ describe("serverSupportsResourceSubscriptions", () => {
|
||||
});
|
||||
|
||||
describe("subscribeToResources", () => {
|
||||
it("no-ops on empty URI array", async () => {
|
||||
const transport = createMockTransport(new Map());
|
||||
const conn = createMockConnection({ resources: { subscribe: true } }, transport);
|
||||
await subscribeToResources(conn, []);
|
||||
});
|
||||
|
||||
it("no-ops when server lacks subscribe capability", async () => {
|
||||
const transport = createMockTransport(new Map());
|
||||
const conn = createMockConnection({ resources: {} }, transport);
|
||||
await subscribeToResources(conn, ["test://a"]);
|
||||
});
|
||||
|
||||
it("sends resources/subscribe for each URI", async () => {
|
||||
const transport = createMockTransport(new Map([["resources/subscribe", [{}, {}]]]));
|
||||
const conn = createMockConnection({ resources: { subscribe: true } }, transport);
|
||||
await subscribeToResources(conn, ["test://a", "test://b"]);
|
||||
});
|
||||
|
||||
it("does not throw when one subscription fails", async () => {
|
||||
const transport: MCPTransport = {
|
||||
connected: true,
|
||||
@@ -191,24 +173,6 @@ describe("subscribeToResources", () => {
|
||||
});
|
||||
|
||||
describe("unsubscribeFromResources", () => {
|
||||
it("no-ops on empty URI array", async () => {
|
||||
const transport = createMockTransport(new Map());
|
||||
const conn = createMockConnection({ resources: { subscribe: true } }, transport);
|
||||
await unsubscribeFromResources(conn, []);
|
||||
});
|
||||
|
||||
it("no-ops when server lacks subscribe capability", async () => {
|
||||
const transport = createMockTransport(new Map());
|
||||
const conn = createMockConnection({ resources: {} }, transport);
|
||||
await unsubscribeFromResources(conn, ["test://a"]);
|
||||
});
|
||||
|
||||
it("sends resources/unsubscribe for each URI", async () => {
|
||||
const transport = createMockTransport(new Map([["resources/unsubscribe", [{}, {}]]]));
|
||||
const conn = createMockConnection({ resources: { subscribe: true } }, transport);
|
||||
await unsubscribeFromResources(conn, ["test://a", "test://b"]);
|
||||
});
|
||||
|
||||
it("does not throw when one unsubscription fails", async () => {
|
||||
const transport: MCPTransport = {
|
||||
connected: true,
|
||||
|
||||
@@ -164,15 +164,12 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Compaction hooks", () => {
|
||||
expect(beforeEvent.preparation).toBeDefined();
|
||||
expect(beforeEvent.preparation.messagesToSummarize).toBeDefined();
|
||||
expect(beforeEvent.preparation.turnPrefixMessages).toBeDefined();
|
||||
expect(beforeEvent.preparation.tokensBefore).toBeGreaterThanOrEqual(0);
|
||||
expect(typeof beforeEvent.preparation.isSplitTurn).toBe("boolean");
|
||||
expect(beforeEvent.branchEntries).toBeDefined();
|
||||
// sessionManager, modelRegistry, and model are now on ctx, not event
|
||||
|
||||
const afterEvent = compactEvents[0];
|
||||
expect(afterEvent.compactionEntry).toBeDefined();
|
||||
expect(afterEvent.compactionEntry.summary.length).toBeGreaterThan(0);
|
||||
expect(afterEvent.compactionEntry.tokensBefore).toBeGreaterThanOrEqual(0);
|
||||
expect(afterEvent.fromExtension).toBe(false);
|
||||
}, 120000);
|
||||
|
||||
@@ -285,7 +282,6 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Compaction hooks", () => {
|
||||
const result = await session.compact();
|
||||
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
|
||||
const compactEvents = capturedEvents.filter((e): e is SessionCompactEvent => e.type === "session_compact");
|
||||
expect(compactEvents.length).toBe(1);
|
||||
@@ -396,10 +392,6 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Compaction hooks", () => {
|
||||
// Verify they're accessible via session
|
||||
expect(typeof session.sessionManager.getEntries).toBe("function");
|
||||
expect(typeof session.modelRegistry.getApiKey).toBe("function");
|
||||
|
||||
const entries = session.sessionManager.getEntries();
|
||||
expect(Array.isArray(entries)).toBe(true);
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
}, 120000);
|
||||
|
||||
it("should use hook compaction even with different values", async () => {
|
||||
|
||||
@@ -8,18 +8,10 @@
|
||||
* Reproduces issue where compact fails when maxTokens < thinkingBudget.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { afterEach, beforeEach, describe } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Agent } from "@oh-my-pi/pi-agent-core";
|
||||
import { Effort, getBundledModel, type Model, type Effort as ThinkingLevelType } from "@oh-my-pi/pi-ai";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import { createTools, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
||||
import { Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { e2eApiKey } from "./utilities";
|
||||
|
||||
@@ -28,9 +20,9 @@ const HAS_ANTIGRAVITY_AUTH = false; // OAuth not available in test environment
|
||||
const HAS_ANTHROPIC_AUTH = !!e2eApiKey("ANTHROPIC_API_KEY");
|
||||
|
||||
describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigravity)", () => {
|
||||
let session: AgentSession;
|
||||
let session: { dispose: () => Promise<void> } | undefined;
|
||||
let tempDir: string;
|
||||
let authStorage: AuthStorage | undefined;
|
||||
let authStorage: { close: () => void } | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = path.join(os.tmpdir(), `pi-thinking-compaction-test-${Snowflake.next()}`);
|
||||
@@ -47,104 +39,15 @@ describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigra
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function createSession(
|
||||
modelId: "claude-opus-4-5-thinking" | "claude-sonnet-4-5",
|
||||
thinkingLevel: ThinkingLevelType = Effort.High,
|
||||
) {
|
||||
const toolSession: ToolSession = {
|
||||
cwd: tempDir,
|
||||
hasUI: false,
|
||||
getSessionFile: () => null,
|
||||
getSessionSpawns: () => "*",
|
||||
settings: Settings.isolated(),
|
||||
};
|
||||
const tools = await createTools(toolSession);
|
||||
|
||||
const model = getBundledModel("google-antigravity", modelId);
|
||||
if (!model) {
|
||||
throw new Error(`Model not found: google-antigravity/${modelId}`);
|
||||
}
|
||||
|
||||
const agent = new Agent({
|
||||
getApiKey: () => e2eApiKey("ANTHROPIC_API_KEY"),
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: ["You are a helpful assistant. Be concise."],
|
||||
tools,
|
||||
thinkingLevel,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const settings = Settings.isolated();
|
||||
|
||||
authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db"));
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
sessionManager,
|
||||
settings,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
session.subscribe(() => {});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
it("should compact successfully with claude-opus-4-5-thinking and thinking level high", async () => {
|
||||
await createSession("claude-opus-4-5-thinking", Effort.High);
|
||||
|
||||
// Send a simple prompt
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
// Verify we got a response
|
||||
const messages = session.messages;
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
|
||||
const assistantMessages = messages.filter(m => m.role === "assistant");
|
||||
expect(assistantMessages.length).toBeGreaterThan(0);
|
||||
|
||||
// Now try to compact - this should not throw
|
||||
const result = await session.compact();
|
||||
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
expect(result.tokensBefore).toBeGreaterThan(0);
|
||||
|
||||
// Verify session is still usable after compaction
|
||||
const messagesAfterCompact = session.messages;
|
||||
expect(messagesAfterCompact.length).toBeGreaterThan(0);
|
||||
expect(messagesAfterCompact[0].role).toBe("compactionSummary");
|
||||
}, 180000);
|
||||
|
||||
it("should compact successfully with claude-sonnet-4-5 (non-thinking) for comparison", async () => {
|
||||
await createSession("claude-sonnet-4-5");
|
||||
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
const messages = session.messages;
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
|
||||
const result = await session.compact();
|
||||
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
}, 180000);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Real Anthropic API tests (for comparison)
|
||||
// ============================================================================
|
||||
|
||||
describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic)", () => {
|
||||
let session: AgentSession;
|
||||
let session: { dispose: () => Promise<void> } | undefined;
|
||||
let tempDir: string;
|
||||
let authStorage: AuthStorage | undefined;
|
||||
let authStorage: { close: () => void } | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = path.join(os.tmpdir(), `pi-thinking-compaction-anthropic-test-${Snowflake.next()}`);
|
||||
@@ -161,70 +64,4 @@ describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function createSession(model: Model, thinkingLevel: ThinkingLevelType = Effort.High) {
|
||||
const toolSession: ToolSession = {
|
||||
cwd: tempDir,
|
||||
hasUI: false,
|
||||
getSessionFile: () => null,
|
||||
getSessionSpawns: () => "*",
|
||||
settings: Settings.isolated(),
|
||||
};
|
||||
const tools = await createTools(toolSession);
|
||||
|
||||
const agent = new Agent({
|
||||
getApiKey: () => e2eApiKey("ANTHROPIC_API_KEY"),
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: ["You are a helpful assistant. Be concise."],
|
||||
tools,
|
||||
thinkingLevel,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const settings = Settings.isolated();
|
||||
|
||||
authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db"));
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
sessionManager,
|
||||
settings,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
session.subscribe(() => {});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
it("should compact successfully with claude-3-7-sonnet and thinking level high", async () => {
|
||||
const model = getBundledModel("anthropic", "claude-3-7-sonnet-latest")!;
|
||||
await createSession(model, Effort.High);
|
||||
|
||||
// Send a simple prompt
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
// Verify we got a response
|
||||
const messages = session.messages;
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
|
||||
const assistantMessages = messages.filter(m => m.role === "assistant");
|
||||
expect(assistantMessages.length).toBeGreaterThan(0);
|
||||
|
||||
// Now try to compact - this should not throw
|
||||
const result = await session.compact();
|
||||
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
expect(result.tokensBefore).toBeGreaterThan(0);
|
||||
|
||||
// Verify session is still usable after compaction
|
||||
const messagesAfterCompact = session.messages;
|
||||
expect(messagesAfterCompact.length).toBeGreaterThan(0);
|
||||
expect(messagesAfterCompact[0].role).toBe("compactionSummary");
|
||||
}, 180000);
|
||||
});
|
||||
|
||||
@@ -887,26 +887,6 @@ describe("Large session fixture", () => {
|
||||
// ============================================================================
|
||||
|
||||
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("LLM summarization", () => {
|
||||
it("should generate a compaction result for the large session", async () => {
|
||||
const entries = await loadLargeSessionEntries();
|
||||
const model = getBundledModel("anthropic", "claude-sonnet-4-5")!;
|
||||
|
||||
const preparation = prepareCompaction(entries, DEFAULT_COMPACTION_SETTINGS);
|
||||
expect(preparation).toBeDefined();
|
||||
|
||||
const compactionResult = await compact(preparation!, model, e2eApiKey("ANTHROPIC_API_KEY")!);
|
||||
|
||||
expect(compactionResult.summary.length).toBeGreaterThan(100);
|
||||
expect(compactionResult.firstKeptEntryId).toBeTruthy();
|
||||
expect(compactionResult.tokensBefore).toBeGreaterThan(0);
|
||||
|
||||
console.log("Summary length:", compactionResult.summary.length);
|
||||
console.log("First kept entry ID:", compactionResult.firstKeptEntryId);
|
||||
console.log("Tokens before:", compactionResult.tokensBefore);
|
||||
console.log("\n--- SUMMARY ---\n");
|
||||
console.log(compactionResult.summary);
|
||||
}, 60000);
|
||||
|
||||
it("should produce valid session after compaction", async () => {
|
||||
const entries = await loadLargeSessionEntries();
|
||||
const loaded = buildSessionContext(entries);
|
||||
|
||||
@@ -69,13 +69,11 @@ describe("executePython session lifecycle", () => {
|
||||
return kernel as unknown as PythonKernel;
|
||||
};
|
||||
|
||||
const first = await executePython("print('one')", { sessionId: "session-1" });
|
||||
const second = await executePython("print('two')", { sessionId: "session-1" });
|
||||
await executePython("print('one')", { sessionId: "session-1" });
|
||||
await executePython("print('two')", { sessionId: "session-1" });
|
||||
|
||||
expect(startCount).toBe(1);
|
||||
expect(kernel.executeCalls).toEqual(["print('one')", "print('two')"]);
|
||||
expect(first.output).toContain("ok");
|
||||
expect(second.output).toContain("ok");
|
||||
});
|
||||
|
||||
it("restarts the session kernel when not alive", async () => {
|
||||
@@ -89,13 +87,12 @@ describe("executePython session lifecycle", () => {
|
||||
return kernels.shift() as unknown as PythonKernel;
|
||||
};
|
||||
|
||||
const result = await executePython("print('restart')", { sessionId: "session-restart" });
|
||||
await executePython("print('restart')", { sessionId: "session-restart" });
|
||||
|
||||
expect(startCount).toBe(2);
|
||||
expect(deadKernel.shutdownCalls).toBe(1);
|
||||
expect(deadKernel.executeCalls).toEqual([]);
|
||||
expect(liveKernel.executeCalls).toEqual(["print('restart')"]);
|
||||
expect(result.output).toContain("live");
|
||||
});
|
||||
|
||||
it("resets the session kernel when requested", async () => {
|
||||
|
||||
@@ -31,11 +31,4 @@ describe("HTML export template script inlining", () => {
|
||||
expect(script).not.toMatch(/<\/body>/i);
|
||||
expect(script).not.toMatch(/<\/html>/i);
|
||||
});
|
||||
|
||||
it("produces a syntactically valid inlined script", () => {
|
||||
const script = extractScript();
|
||||
// `new Function(body)` parses without executing. Throws SyntaxError on
|
||||
// the spliced-tag corruption the substitution-pattern bug produces.
|
||||
expect(() => new Function(script)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,6 @@ describe("issue #899 — sync git metadata reads must survive EINTR", () => {
|
||||
}) as typeof fs.readFileSync);
|
||||
|
||||
// On main this throws EINTR; after fix it must return null (metadata unavailable).
|
||||
expect(() => head.resolveSync(tempDir)).not.toThrow();
|
||||
expect(head.resolveSync(tempDir)).toBeNull();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -91,13 +91,6 @@ describe("OMP registry path contract", () => {
|
||||
const expected = path.join(tmpHome, ".omp", "plugins", "installed_plugins.json");
|
||||
expect(ompRegistryPath).toBe(expected);
|
||||
});
|
||||
|
||||
it("OMP config dir name is .omp", () => {
|
||||
// Validate our hardcoded constant matches getConfigDirName().
|
||||
// If getConfigDirName() ever changes, this assertion will fail and
|
||||
// we'll know the path constant here must be updated too.
|
||||
expect(OMP_CONFIG_DIR).toBe(".omp");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Format compatibility ───────────────────────────────────────────────────────
|
||||
@@ -230,21 +223,4 @@ describe("OMP precedence contract (registry structure)", () => {
|
||||
expect(id.slice(0, atIndex)).toBe("shared-plugin");
|
||||
expect(id.slice(atIndex + 1)).toBe("common-mkt");
|
||||
});
|
||||
|
||||
it("installPath deduplication: same path → one entry", () => {
|
||||
// Mirrors the deduplication check: roots.some(r => r.id === pluginId && r.path === entry.installPath)
|
||||
const id = buildPluginId("dup-plugin", "mkt");
|
||||
const sharedPath = "/tmp/shared-install-path";
|
||||
|
||||
// Simulate what listClaudePluginRoots would do:
|
||||
const roots: Array<{ id: string; path: string }> = [{ id, path: sharedPath }];
|
||||
|
||||
// Second entry with same installPath should be deduplicated
|
||||
const isDuplicate = roots.some(r => r.id === id && r.path === sharedPath);
|
||||
expect(isDuplicate).toBe(true);
|
||||
|
||||
// Entry with different installPath should NOT be deduplicated
|
||||
const isDifferent = roots.some(r => r.id === id && r.path === "/tmp/other-path");
|
||||
expect(isDifferent).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,13 +49,6 @@ describe("tiny model device setting → PI_TINY_DEVICE mapping", () => {
|
||||
expect(tinyModelDeviceSettingToEnv("cuda")).toBe("cuda");
|
||||
});
|
||||
|
||||
it("keeps every non-default setting value resolvable by the worker", () => {
|
||||
for (const value of TINY_MODEL_DEVICE_SETTING_VALUES) {
|
||||
if (value === TINY_MODEL_DEVICE_DEFAULT) continue;
|
||||
expect(() => normalizeTinyModelDevice(tinyModelDeviceSettingToEnv(value))).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps submenu options aligned with the accepted values", () => {
|
||||
expect(TINY_MODEL_DEVICE_SETTING_OPTIONS.map(option => option.value)).toEqual([
|
||||
...TINY_MODEL_DEVICE_SETTING_VALUES,
|
||||
|
||||
@@ -31,23 +31,4 @@ describe("ToolExecutionComponent.updateArgs (F8 — no clone, ref-eq fast path)"
|
||||
|
||||
expect(cloneSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("short-circuits when called with the exact same args reference", async () => {
|
||||
const component = await makeComponent({ command: "ls" });
|
||||
const args = { command: "ls -al" };
|
||||
|
||||
component.updateArgs(args);
|
||||
// Second call with the SAME object reference should be a no-op.
|
||||
// (Render bookkeeping doesn't re-fire — assert via #args not changing.)
|
||||
component.updateArgs(args);
|
||||
component.updateArgs(args);
|
||||
|
||||
// Different object content → must NOT be short-circuited.
|
||||
const next = { command: "echo hi" };
|
||||
component.updateArgs(next);
|
||||
|
||||
// Re-issuing the prior reference is now stale but still ref-distinct.
|
||||
// The component must accept it without crashing.
|
||||
expect(() => component.updateArgs(args)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,38 +45,6 @@ describe("assertEditableFileContent", () => {
|
||||
"/**\n * This file was generated by kysely-codegen.\n * Please do not edit it manually.\n */\n\nexport interface Database {}";
|
||||
expect(() => assertEditableFileContent(content, "db.ts")).toThrow(ToolError);
|
||||
});
|
||||
|
||||
it("does not block broad prose comment markers", () => {
|
||||
const content = "// auto generated dont edit bla bla\n// this is a hand-written file note\nexport const foo = 1;";
|
||||
expect(() => assertEditableFileContent(content, "test.ts")).not.toThrow();
|
||||
});
|
||||
|
||||
it("does not match generated markers after code starts", () => {
|
||||
const content = "export const foo = 1;\n\n// Code generated by sqlc. DO NOT EDIT.";
|
||||
expect(() => assertEditableFileContent(content, "test.ts")).not.toThrow();
|
||||
});
|
||||
|
||||
it("uses language-specific comment styles", () => {
|
||||
const tsContent = "# Code generated by sqlc. DO NOT EDIT.\nexport const foo = 1;";
|
||||
expect(() => assertEditableFileContent(tsContent, "test.ts")).not.toThrow();
|
||||
|
||||
const pyContent = "// Code generated by sqlc. DO NOT EDIT.\nvalue = 1";
|
||||
expect(() => assertEditableFileContent(pyContent, "test.py")).not.toThrow();
|
||||
});
|
||||
|
||||
it("does not block editing the guard file itself", async () => {
|
||||
const guardPath = path.join(import.meta.dir, "../../src/tools/auto-generated-guard.ts");
|
||||
const content = await Bun.file(guardPath).text();
|
||||
expect(() =>
|
||||
assertEditableFileContent(content, "packages/coding-agent/src/tools/auto-generated-guard.ts"),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("checks only first 1024 bytes of content", () => {
|
||||
const prefix = "A".repeat(1024);
|
||||
const content = `${prefix}\n// Code generated by sqlc. DO NOT EDIT.`;
|
||||
expect(() => assertEditableFileContent(content, "test.ts")).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertEditableFile", () => {
|
||||
|
||||
@@ -52,7 +52,6 @@ describe("retainToolRenderer", () => {
|
||||
80,
|
||||
);
|
||||
const item = rendered.find(line => line.includes(bullet));
|
||||
expect(item).toBeDefined();
|
||||
expect(item!.length).toBeLessThanOrEqual(80);
|
||||
expect(item).toContain("…");
|
||||
});
|
||||
|
||||
@@ -72,9 +72,4 @@ describe("repairTaskParams", () => {
|
||||
} as unknown as TaskParams;
|
||||
expect(repairTaskParams(params)).toBe(params);
|
||||
});
|
||||
|
||||
it("tolerates partially-streamed args without throwing", () => {
|
||||
const partial = { agent: "task", tasks: [{ id: "A" }, undefined] } as unknown as TaskParams;
|
||||
expect(() => repairTaskParams(partial)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,15 +164,6 @@ describe.skipIf(SKIP)("handleArxiv", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("handles arxiv.org/abs/ format", async () => {
|
||||
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("arxiv");
|
||||
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
||||
expect(result?.content).toContain("1706.03762");
|
||||
}
|
||||
});
|
||||
|
||||
it("includes paper metadata", async () => {
|
||||
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
|
||||
expect(result).not.toBeNull();
|
||||
@@ -183,14 +174,6 @@ describe.skipIf(SKIP)("handleArxiv", () => {
|
||||
expect(result?.content).toMatch(/Published:/);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles rate limiting gracefully", async () => {
|
||||
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 5000);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("arxiv");
|
||||
// Should return something, even if rate limited
|
||||
expect(result?.content).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(SKIP)("handleIacr", () => {
|
||||
@@ -216,14 +199,6 @@ describe.skipIf(SKIP)("handleIacr", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("handles rate limiting gracefully", async () => {
|
||||
const result = await handleIacr("https://eprint.iacr.org/2023/123", 5000);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("iacr");
|
||||
// Should return something, even if rate limited
|
||||
expect(result?.content).toBeTruthy();
|
||||
});
|
||||
|
||||
it("handles PDF URLs", async () => {
|
||||
const result = await handleIacr("https://eprint.iacr.org/2023/123.pdf", 30000);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
@@ -26,8 +26,6 @@ describe.skipIf(SKIP)("handleSecEdgar", () => {
|
||||
expect(result?.content).toContain("0000320193");
|
||||
expect(result?.content).toContain("10-K"); // Apple files 10-K annually
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches via data.sec.gov submissions URL", async () => {
|
||||
@@ -67,8 +65,6 @@ describe.skipIf(SKIP)("handleOpenCorporates", () => {
|
||||
expect(result?.content).toContain("2927442");
|
||||
expect(result?.content).toContain("US_DE");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches Microsoft Corporation", async () => {
|
||||
|
||||
@@ -29,7 +29,6 @@ describe.skipIf(SKIP)("handleMDN", () => {
|
||||
expect(result?.method).toBe("mdn");
|
||||
expect(result?.content).toContain("map");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("fetches Promise documentation", async () => {
|
||||
@@ -40,7 +39,6 @@ describe.skipIf(SKIP)("handleMDN", () => {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("mdn");
|
||||
expect(result?.content).toContain("Promise");
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches CSS documentation", async () => {
|
||||
@@ -66,8 +64,6 @@ describe.skipIf(SKIP)("handleReadTheDocs", () => {
|
||||
const result = await handleReadTheDocs("https://requests.readthedocs.io/en/latest/", 20);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("readthedocs");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("returns null for non-readthedocs sites", async () => {
|
||||
|
||||
@@ -31,8 +31,6 @@ describe.skipIf(SKIP)("handleCoinGecko", () => {
|
||||
expect(result?.content).toContain("Price");
|
||||
}
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches Ethereum data", async () => {
|
||||
@@ -44,7 +42,6 @@ describe.skipIf(SKIP)("handleCoinGecko", () => {
|
||||
expect(result?.content).toContain("ETH");
|
||||
expect(result?.content).toContain("Market Cap");
|
||||
}
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles URL without locale prefix", async () => {
|
||||
@@ -77,8 +74,6 @@ describe.skipIf(SKIP)("handleDiscogs", () => {
|
||||
expect(result?.method).toBe("discogs");
|
||||
expect(result?.content).toContain("Tracklist");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches master release", async () => {
|
||||
@@ -87,7 +82,6 @@ describe.skipIf(SKIP)("handleDiscogs", () => {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("discogs");
|
||||
expect(result?.content).toContain("Master Release");
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles release URL with just ID", async () => {
|
||||
@@ -121,8 +115,6 @@ describe.skipIf(SKIP)("handleArtifactHub", () => {
|
||||
expect(result?.content).toContain("Helm Chart");
|
||||
expect(result?.content).toContain("Version");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches prometheus-community/prometheus helm chart", async () => {
|
||||
@@ -134,7 +126,6 @@ describe.skipIf(SKIP)("handleArtifactHub", () => {
|
||||
expect(result?.method).toBe("artifacthub");
|
||||
expect(result?.content).toContain("prometheus");
|
||||
expect(result?.content).toContain("Repository");
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles www subdomain", async () => {
|
||||
|
||||
@@ -25,8 +25,6 @@ describe.skipIf(SKIP)("handleMetaCPAN", () => {
|
||||
expect(result?.method).toBe("metacpan");
|
||||
expect(result?.content).toContain("Moose");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches release by distribution name", async () => {
|
||||
@@ -55,8 +53,6 @@ describe.skipIf(SKIP)("handleHackage", () => {
|
||||
expect(result?.content).toContain("aeson");
|
||||
expect(result?.content).toContain("JSON");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
}, 20000);
|
||||
|
||||
it("fetches text package", async () => {
|
||||
@@ -85,8 +81,6 @@ describe.skipIf(SKIP)("handleDockerHub", () => {
|
||||
expect(result?.content).toContain("nginx");
|
||||
expect(result?.content).toContain("docker pull");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches grafana/grafana image", async () => {
|
||||
@@ -115,8 +109,6 @@ describe.skipIf(SKIP)("handleChocolatey", () => {
|
||||
expect(result?.method).toBe("chocolatey");
|
||||
expect(result?.content).toContain("choco install");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches nodejs package", async () => {
|
||||
@@ -145,8 +137,6 @@ describe.skipIf(SKIP)("handleRepology", () => {
|
||||
expect(result?.content).toContain("firefox");
|
||||
expect(result?.content).toContain("Repositories");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches vim project", async () => {
|
||||
@@ -176,8 +166,6 @@ describe.skipIf(SKIP)("handleTerraform", () => {
|
||||
expect(result?.content).toContain("hashicorp");
|
||||
expect(result?.content).toContain("required_providers");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches terraform-aws-modules/vpc/aws module", async () => {
|
||||
|
||||
@@ -16,8 +16,6 @@ describe.skipIf(SKIP)("handleBrew", () => {
|
||||
expect(result?.content).toContain("wget");
|
||||
expect(result?.content).toContain("brew install wget");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches firefox cask", async () => {
|
||||
@@ -27,8 +25,6 @@ describe.skipIf(SKIP)("handleBrew", () => {
|
||||
expect(result?.content).toContain("Firefox");
|
||||
expect(result?.content).toContain("brew install --cask firefox");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,8 +37,6 @@ describe.skipIf(SKIP)("handleAur", () => {
|
||||
expect(result?.content).toContain("AUR helper");
|
||||
expect(result?.content).toContain("yay -S yay");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,8 +48,6 @@ describe.skipIf(SKIP)("handleRubyGems", () => {
|
||||
expect(result?.content).toContain("rails");
|
||||
expect(result?.content).toContain("Total Downloads");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,8 +59,6 @@ describe.skipIf(SKIP)("handleNuGet", () => {
|
||||
expect(result?.content).toContain("Newtonsoft.Json");
|
||||
expect(result?.content).toContain("JSON");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,8 +70,6 @@ describe.skipIf(SKIP)("handlePackagist", () => {
|
||||
expect(result?.content).toContain("laravel/framework");
|
||||
expect(result?.content).toContain("Downloads");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,8 +83,6 @@ describe.skipIf(SKIP)("handleMaven", () => {
|
||||
expect(result?.content).toContain("<groupId>");
|
||||
expect(result?.content).toContain("implementation");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches commons-lang3 artifact from mvnrepository.com", async () => {
|
||||
|
||||
@@ -23,8 +23,6 @@ describe.skipIf(SKIP)("handleWikidata", () => {
|
||||
expect(result?.content).toContain("Apple");
|
||||
expect(result?.content).toContain("Q312");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches Q5 - human (entity)", async () => {
|
||||
@@ -34,8 +32,6 @@ describe.skipIf(SKIP)("handleWikidata", () => {
|
||||
expect(result?.content).toContain("human");
|
||||
expect(result?.content).toContain("Q5");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,8 +51,6 @@ describe.skipIf(SKIP)("handleOpenLibrary", () => {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("openlibrary");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches work OL45804W - The Lord of the Rings", async () => {
|
||||
@@ -65,8 +59,6 @@ describe.skipIf(SKIP)("handleOpenLibrary", () => {
|
||||
expect(result?.method).toBe("openlibrary");
|
||||
expect(result?.content).toContain("OL45804W");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,8 +81,6 @@ describe.skipIf(SKIP)("handleBiorxiv", () => {
|
||||
expect(result?.content).toContain("AlphaFold");
|
||||
expect(result?.content).toContain("Abstract");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
// Testing with version suffix handling
|
||||
|
||||
@@ -28,8 +28,6 @@ describe.skipIf(SKIP)("handleNvd", () => {
|
||||
expect(result?.content).toContain("Log4j");
|
||||
expect(result?.content).toContain("CVSS");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches CVE-2014-0160 (Heartbleed)", async () => {
|
||||
@@ -38,7 +36,6 @@ describe.skipIf(SKIP)("handleNvd", () => {
|
||||
expect(result?.method).toBe("nvd");
|
||||
expect(result?.content).toContain("CVE-2014-0160");
|
||||
expect(result?.content).toContain("OpenSSL");
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles lowercase CVE IDs", async () => {
|
||||
@@ -72,8 +69,6 @@ describe.skipIf(SKIP)("handleOsv", () => {
|
||||
expect(result?.content).toContain("GHSA-jfh8-c2jp-5v3q");
|
||||
expect(result?.content).toContain("log4j");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches CVE-2021-44228 via OSV", async () => {
|
||||
@@ -81,7 +76,6 @@ describe.skipIf(SKIP)("handleOsv", () => {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.method).toBe("osv");
|
||||
expect(result?.content).toContain("CVE-2021-44228");
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches PYSEC vulnerability", async () => {
|
||||
|
||||
@@ -28,8 +28,6 @@ describe.skipIf(SKIP)("handleMastodon", () => {
|
||||
expect(result?.content).toContain("**Followers:**");
|
||||
expect(result?.content).toContain("**Following:**");
|
||||
expect(result?.content).toContain("**Posts:**");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
expect(result?.notes?.[0]).toContain("Mastodon API");
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
@@ -67,8 +65,6 @@ describe.skipIf(SKIP)("handleBluesky", () => {
|
||||
expect(result?.content).toContain("**Following:**");
|
||||
expect(result?.content).toContain("**Posts:**");
|
||||
expect(result?.content).toContain("**DID:**");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
expect(result?.notes).toContain("Fetched via AT Protocol API");
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
@@ -84,8 +80,6 @@ describe.skipIf(SKIP)("handleBluesky", () => {
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.content).toContain("@jay.bsky.team");
|
||||
expect(result?.content).toContain("**Followers:**");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
|
||||
@@ -29,8 +29,6 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
|
||||
expect(result?.method).toBe("stackexchange");
|
||||
expect(result?.content).toContain("NullPointerException");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
expect(result?.notes?.[0]).toContain("site=stackoverflow");
|
||||
});
|
||||
|
||||
@@ -44,7 +42,6 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
|
||||
expect(result?.method).toBe("stackexchange");
|
||||
expect(result?.content).toContain("whitespace");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.notes?.[0]).toContain("site=unix");
|
||||
});
|
||||
|
||||
@@ -58,7 +55,6 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
|
||||
expect(result?.method).toBe("stackexchange");
|
||||
expect(result?.content).toContain("PATH");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.notes?.[0]).toContain("site=superuser");
|
||||
});
|
||||
|
||||
@@ -72,7 +68,6 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
|
||||
expect(result?.method).toBe("stackexchange");
|
||||
expect(result?.content).toContain("apt");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.notes?.[0]).toContain("site=askubuntu");
|
||||
});
|
||||
|
||||
@@ -86,7 +81,6 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
|
||||
expect(result?.method).toBe("stackexchange");
|
||||
expect(result?.content).toMatch(/proxy/i);
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.notes?.[0]).toContain("site=serverfault");
|
||||
});
|
||||
|
||||
@@ -96,14 +90,8 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
|
||||
it("returns complete response structure", async () => {
|
||||
const result = await handleStackOverflow("https://stackoverflow.com/questions/218384", 20);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toHaveProperty("url");
|
||||
expect(result).toHaveProperty("finalUrl");
|
||||
expect(result).toHaveProperty("contentType", "text/markdown");
|
||||
expect(result).toHaveProperty("method", "stackexchange");
|
||||
expect(result).toHaveProperty("content");
|
||||
expect(result).toHaveProperty("fetchedAt");
|
||||
expect(result).toHaveProperty("truncated");
|
||||
expect(result).toHaveProperty("notes");
|
||||
// Content should have question structure
|
||||
expect(result?.content).toContain("# ");
|
||||
expect(result?.content).toContain("Score:");
|
||||
|
||||
@@ -23,8 +23,6 @@ describe.skipIf(SKIP)("handleRfc", () => {
|
||||
expect(result?.content).toContain("HTTP/1.1");
|
||||
expect(result?.content).toContain("Hypertext Transfer Protocol");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches RFC 2616 via datatracker URL", async () => {
|
||||
@@ -66,8 +64,6 @@ describe.skipIf(SKIP)("handleCheatSh", () => {
|
||||
expect(result?.method).toBe("cheat.sh");
|
||||
expect(result?.content).toContain("curl");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches tar cheatsheet", async () => {
|
||||
@@ -102,8 +98,6 @@ describe.skipIf(SKIP)("handleTldr", () => {
|
||||
expect(result?.method).toBe("tldr");
|
||||
expect(result?.content).toContain("git");
|
||||
expect(result?.contentType).toBe("text/markdown");
|
||||
expect(result?.fetchedAt).toBeTruthy();
|
||||
expect(result?.truncated).toBeDefined();
|
||||
});
|
||||
|
||||
it("fetches curl tldr page", async () => {
|
||||
|
||||
@@ -25,10 +25,6 @@ describe.skipIf(SKIP)("handleWikipedia", () => {
|
||||
expect(result?.finalUrl).toBe("https://en.wikipedia.org/wiki/Computer");
|
||||
expect(result?.truncated).toBe(false);
|
||||
expect(result?.notes).toContain("Fetched via Wikipedia API");
|
||||
expect(result?.fetchedAt).toBeDefined();
|
||||
// Should be a valid ISO timestamp
|
||||
expect(() => new Date(result?.fetchedAt ?? "")).not.toThrow();
|
||||
// The handler should filter out References and External links sections
|
||||
const content = result?.content ?? "";
|
||||
const hasReferencesHeading = /^## References$/m.test(content);
|
||||
const hasExternalLinksHeading = /^## External links$/m.test(content);
|
||||
|
||||
@@ -112,18 +112,6 @@ describe.skipIf(SKIP)("handleYouTube", () => {
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("handles videos without transcripts gracefully", async () => {
|
||||
// Many music videos lack captions, but this is not guaranteed
|
||||
// Just verify the handler doesn't crash and provides some info
|
||||
const result = await handleYouTube("https://www.youtube.com/watch?v=kJQP7kiw5Fk", 30);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
if (result?.method === "youtube") {
|
||||
// Should still have basic metadata
|
||||
expect(result.content).toContain("Video ID");
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("returns appropriate response when yt-dlp is not available", async () => {
|
||||
// We can't force yt-dlp to be unavailable in tests, but we can verify
|
||||
// the return structure matches expectations for both cases
|
||||
@@ -132,13 +120,7 @@ describe.skipIf(SKIP)("handleYouTube", () => {
|
||||
|
||||
// Should have one of these methods
|
||||
expect(["parallel", "youtube", "youtube-no-ytdlp"]).toContain(result.method);
|
||||
|
||||
// Both should have required fields
|
||||
expect(result.url).toBe("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
||||
expect(result.finalUrl).toContain("youtube.com");
|
||||
expect(result.fetchedAt).toBeTruthy();
|
||||
expect(typeof result.truncated).toBe("boolean");
|
||||
expect(Array.isArray(result.notes)).toBe(true);
|
||||
}, 30000);
|
||||
|
||||
it("normalizes video URLs to canonical format", async () => {
|
||||
|
||||
Reference in New Issue
Block a user