Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions lib/chat/runs/__tests__/provisionRunSession.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

import { provisionRunSession } from "../provisionRunSession";
import { createSessionWithInitialChat } from "@/lib/sessions/createSessionWithInitialChat";
import { connectSandbox } from "@/lib/sandbox/factory";
import { markSessionSandboxActive } from "@/lib/sandbox/markSessionSandboxActive";
import { discoverSkills } from "@/lib/skills/discoverSkills";
import { installSessionGlobalSkills } from "@/lib/sandbox/installSessionGlobalSkills";

vi.mock("@/lib/sessions/createSessionWithInitialChat", () => ({
createSessionWithInitialChat: vi.fn(),
}));
vi.mock("@/lib/sandbox/factory", () => ({ connectSandbox: vi.fn() }));
vi.mock("@/lib/sandbox/getSessionSandboxName", () => ({
getSessionSandboxName: vi.fn(() => "sandbox-name"),
}));
vi.mock("@/lib/sandbox/resolveGitUser", () => ({
resolveGitUser: vi.fn(async () => ({ name: "x", email: "y" })),
}));
vi.mock("@/lib/github/getServiceGithubToken", () => ({
getServiceGithubToken: vi.fn(() => "gh-token"),
}));
vi.mock("@/lib/sandbox/markSessionSandboxActive", () => ({
markSessionSandboxActive: vi.fn(),
}));
vi.mock("@/lib/skills/discoverSkills", () => ({ discoverSkills: vi.fn(async () => []) }));
vi.mock("@/lib/skills/getSandboxSkillDirectories", () => ({
getSandboxSkillDirectories: vi.fn(async () => ["/skills"]),
}));
vi.mock("@/lib/sandbox/installSessionGlobalSkills", () => ({
installSessionGlobalSkills: vi.fn(async () => undefined),
}));

const session = {
id: "session-1",
clone_url: "https://github.com/org/repo",
sandbox_state: { type: "vercel" },
};
const updated = { ...session };
const chat = { id: "chat-1" };
const sandbox = {
getState: () => ({ type: "vercel" }),
workingDirectory: "/work",
};

describe("provisionRunSession", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(createSessionWithInitialChat).mockResolvedValue({
ok: true,
session,
chat,
} as never);
vi.mocked(connectSandbox).mockResolvedValue(sandbox as never);
vi.mocked(markSessionSandboxActive).mockResolvedValue(updated as never);
});

it("installs global skills into the sandbox before discovering them", async () => {
await provisionRunSession({ accountId: "account-1", title: "t" });

// Headless runs must PROVISION skills, not just discover them (chat#1822).
expect(installSessionGlobalSkills).toHaveBeenCalledWith({
sessionRow: updated,
sandbox,
});

const installOrder = vi.mocked(installSessionGlobalSkills).mock.invocationCallOrder[0];
const discoverOrder = vi.mocked(discoverSkills).mock.invocationCallOrder[0];
expect(installOrder).toBeLessThan(discoverOrder);
});

it("still completes the run when skill install fails (best-effort)", async () => {
vi.mocked(installSessionGlobalSkills).mockRejectedValueOnce(new Error("install boom"));

const result = await provisionRunSession({ accountId: "account-1", title: "t" });

expect(result.session).toEqual(updated);
expect(discoverSkills).toHaveBeenCalled();
});
});
13 changes: 13 additions & 0 deletions lib/chat/runs/provisionRunSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSessionSandboxName } from "@/lib/sandbox/getSessionSandboxName";
import { resolveGitUser } from "@/lib/sandbox/resolveGitUser";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Enforce Clear Code Style and Maintainability Practices

File provisionRunSession.ts exceeds the 100-line limit (111 lines) after this change, violating the maintainability rule's file-length and single-responsibility constraints.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/runs/provisionRunSession.ts, line 80:

<comment>File `provisionRunSession.ts` exceeds the 100-line limit (111 lines) after this change, violating the maintainability rule's file-length and single-responsibility constraints.</comment>

<file context>
@@ -76,6 +77,18 @@ export async function provisionRunSession({
   const updated = await markSessionSandboxActive(session, sandbox.getState() as Json);
   if (!updated) throw new Error("Failed to activate session sandbox");
 
+  // Install global skills BEFORE discovery — the headless run path must
+  // provision skills, not just discover them. Without this the sandbox skills
+  // dir is empty, `discoverSkills` returns [], the agent gets no `skill` tool,
</file context>

import { getServiceGithubToken } from "@/lib/github/getServiceGithubToken";
import { markSessionSandboxActive } from "@/lib/sandbox/markSessionSandboxActive";
import { installSessionGlobalSkills } from "@/lib/sandbox/installSessionGlobalSkills";
import { discoverSkills } from "@/lib/skills/discoverSkills";
import { getSandboxSkillDirectories } from "@/lib/skills/getSandboxSkillDirectories";
import { DEFAULT_WORKING_DIRECTORY } from "@/lib/sandbox/vercel/sandbox/constants";
Expand Down Expand Up @@ -76,6 +77,18 @@ export async function provisionRunSession({
const updated = await markSessionSandboxActive(session, sandbox.getState() as Json);
if (!updated) throw new Error("Failed to activate session sandbox");

// Install global skills BEFORE discovery — the headless run path must
// provision skills, not just discover them. Without this the sandbox skills
// dir is empty, `discoverSkills` returns [], the agent gets no `skill` tool,
// and it falls back to guessing API endpoints + fabricating (chat#1822).
// Best-effort: a failed install must not block the run (mirrors
// `createSandboxHandler`).
try {
await installSessionGlobalSkills({ sessionRow: updated, sandbox });
} catch (error) {
console.error("[provisionRunSession] installSessionGlobalSkills failed:", error);
}

// Best-effort skill + working-directory discovery from the live handle —
// a failure falls back to defaults so the run can still start (tools surface
// the underlying issue when they reconnect). Mirrors handleChatWorkflowStream.
Expand Down
Loading