Files
PaperClipAI/server/src/services/native-runtime/runtime-context.test.ts
T
Devin FoleyandPaperclip 24758b9da1 Avoid loading tool schemas for effective assignments (#16969)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Tool profiles define which tools an agent can use.
> - Run preparation and connection checks resolve these assignments from
the database.
> - These callers need tool IDs and names, but the existing lookup also
reads complete schemas and provider metadata.
> - Repeated lookups transfer data that these callers discard.
> - This pull request adds a small assignment projection through the
same permission resolver.
> - The benefit is less database transfer and JSON work with current
permission checks and the same public response.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Effective tool assignment reads during run preparation, native runtime
setup, and connection policy checks.

**Subsystem affected**

server/ — tool access and orchestration services.

**Current behavior**

Internal callers use the public effective-profile lookup. Its catalog
query reads complete tool rows, including input schemas, output schemas,
annotations, and provider metadata. These callers only use catalog IDs
and names.

**Proposed behavior**

Internal callers use an assignment lookup that selects catalog identity
and selector fields. Both lookups share one resolver. The public
effective-profile endpoint keeps its complete definitions.

**Reason and benefit**

Large catalogs increase database transfer and JSON work for every
assignment lookup. The smaller query avoids that payload without caching
permissions or changing query frequency.

**Breaking changes**

None. No route, schema, migration, permission rule, or public response
changes.

Related: Refs #16761, Refs #16723. The former narrows profile summary
reads. The latter changes how the gateway presents tool definitions.
Neither narrows this internal effective-assignment lookup.

## What Changed

- Share effective-profile resolution between full definitions and
assignment records.
- Select the existing catalog identity and selector projection for
internal assignments.
- Use the assignment lookup in run preparation, native runtime setup,
and connection policy checks.
- Add real PostgreSQL tests for selectors, exclusions, current profile
and binding state, company boundaries, and returned payload size.
- Document the database read contract.

## Verification

- Five focused test files passed: 521 tests. They cover tool access,
connection intents, runtime preparation, native runtime context, and the
new assignment lookup.
- `pnpm -r typecheck` passed.
- `pnpm check:module-boundaries` passed.
- The PostgreSQL fixture compares the full authorization result with the
assignment result. It checks all selector types, excludes removed tools,
and observes catalog, profile, and binding changes on later calls.
- The payload test measures rows returned by PostgreSQL before
application mapping. With large schema and annotation fields, the
assignment result is less than 1% of the full catalog payload. This is a
synthetic fixture result, not a fleet savings estimate.
- `pnpm build` passed.
- The full local suite is in progress. It found two Git fixture failures
that also reproduce on unchanged master with `init.defaultBranch=main`.
Both pass with a process-only `init.defaultBranch=master` setting. The
global Git configuration was not changed. This earlier failure remains
recorded; it is not counted as a clean first pass.
- Apex review: 5/5, no actionable comments. All 54 CI checks passed; two
conditional jobs were skipped.
- The broader local run also reached two UI test timeouts and one
adapter test timeout. All three passed when rerun in isolation with
unchanged source. The exact cause of these timeouts is not established.
No UI or adapter implementation is changed by this PR. The CLI
import/export journey passes its assertions but its local teardown fails
with `ENOTEMPTY` while a runtime context asset is written. Its CI check
passed. The complete local suite is not reported as a clean pass.

## Risks

- Both lookups share permission resolution. Existing tool access tests
and result parity checks cover that extraction.
- Future assignment callers must use the fields in the smaller result.
Typechecking rejects attempts to read full schemas from it.
- The lookup still scans the active company catalog and resolves current
connections on every call. This change reduces returned bytes; it does
not reduce query count or introduce a cache.
- No database migration or operational configuration change is required.

## Model Used

- OpenAI GPT-6 through Codex, with tool-assisted source inspection,
implementation, and test execution. The exact model variant and context
window are not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-20 08:42:59 -07:00

489 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { mkdtemp, mkdir, readFile, readdir, chmod, lstat, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import type { Db } from "@paperclipai/db";
import {
PAPERCLIP_OPERATIONAL_SKILL_KEY,
type PaperclipSkillEntry,
} from "@paperclipai/adapter-utils/server-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const serviceMocks = vi.hoisted(() => ({
exportFiles: vi.fn(),
readCommittedForRuntime: vi.fn(),
getEffectiveToolAssignmentsForAgent: vi.fn(),
githubBotConnectionIdsForRun: vi.fn(),
spekoToolsForSession: vi.fn(),
}));
vi.mock("../voice/speko-agent-tools.js", () => ({ spekoToolsForSession: serviceMocks.spekoToolsForSession }));
vi.mock("../chat-github-tools.js", () => ({
githubBotConnectionIdsForRun: serviceMocks.githubBotConnectionIdsForRun,
}));
vi.mock("../agent-instructions.js", () => ({
agentInstructionsService: () => ({ exportFiles: serviceMocks.exportFiles }),
agentInstructionsBundleMode: (agent: { adapterConfig?: { instructionsBundleMode?: string } }) => agent.adapterConfig?.instructionsBundleMode ?? null,
}));
vi.mock("../agent-instruction-revisions.js", () => ({
agentInstructionRevisionService: () => ({ readCommittedForRuntime: serviceMocks.readCommittedForRuntime }),
}));
vi.mock("../tool-access.js", () => ({
toolAccessService: () => ({
getEffectiveToolAssignmentsForAgent: serviceMocks.getEffectiveToolAssignmentsForAgent,
}),
}));
import { createHash } from "node:crypto";
import { context as otelContext, trace } from "@opentelemetry/api";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
import { InMemorySpanExporter, SimpleSpanProcessor, TracerProvider } from "@opentelemetry/sdk-trace";
import { withRunExecutionTiming } from "../execution-preparation-timing.js";
import { buildNativeRuntimeContext, resolveNativeRuntimeMcpSnapshot } from "./runtime-context.js";
const temporaryRoots: string[] = [];
let previousPaperclipHome: string | undefined;
let previousInstanceId: string | undefined;
async function makeTreeWritable(target: string): Promise<void> {
const metadata = await lstat(target).catch(() => null);
if (!metadata) return;
if (metadata.isDirectory()) {
await chmod(target, 0o700);
for (const entry of await readdir(target)) {
await makeTreeWritable(path.join(target, entry));
}
} else {
await chmod(target, 0o600);
}
}
beforeEach(async () => {
vi.clearAllMocks();
serviceMocks.readCommittedForRuntime.mockResolvedValue(null);
serviceMocks.githubBotConnectionIdsForRun.mockResolvedValue(new Set());
serviceMocks.spekoToolsForSession.mockResolvedValue([]);
previousPaperclipHome = process.env.PAPERCLIP_HOME;
previousInstanceId = process.env.PAPERCLIP_INSTANCE_ID;
const root = await mkdtemp(path.join(tmpdir(), "paperclip-native-context-"));
temporaryRoots.push(root);
process.env.PAPERCLIP_HOME = root;
process.env.PAPERCLIP_INSTANCE_ID = "runtime_context_test";
serviceMocks.getEffectiveToolAssignmentsForAgent.mockResolvedValue({
agentId: "agent-1",
profiles: [],
entries: [{ effect: "include", connectionId: "connection-1" }],
bindings: [],
allowedTools: [{ id: "tool-1", connectionId: "connection-1" }],
allowedToolNames: ["issues.read"],
installedConnections: [{
id: "connection-1",
transport: "mcp_remote",
enabled: true,
status: "active",
healthStatus: "healthy",
}],
});
});
afterEach(async () => {
if (previousPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME;
else process.env.PAPERCLIP_HOME = previousPaperclipHome;
if (previousInstanceId === undefined) delete process.env.PAPERCLIP_INSTANCE_ID;
else process.env.PAPERCLIP_INSTANCE_ID = previousInstanceId;
// A rejected Promise.all does not cancel the other materializers. Let their
// bounded local writes settle before removing the read-only asset tree.
await new Promise((resolve) => setTimeout(resolve, 25));
while (temporaryRoots.length) {
const root = temporaryRoots.pop()!;
await makeTreeWritable(root);
await rm(root, { recursive: true, force: true });
}
});
describe("buildNativeRuntimeContext", () => {
it("uses committed instructions when the disk projection is stale", async () => {
serviceMocks.exportFiles.mockResolvedValue({
entryFile: "AGENTS.md",
files: { "AGENTS.md": "Old disk projection\n", "reference.md": "Sibling reference\n" },
});
serviceMocks.readCommittedForRuntime.mockResolvedValue({
revision: { entryFile: "AGENTS.md" }, content: "Saved canonical instructions\n",
});
const context = await buildNativeRuntimeContext({
db: {} as Db,
agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: { instructionsBundleMode: "managed" } },
runId: "run-1", runtimeConfig: {}, runtimeSkillEntries: [],
});
expect(await readFile(path.join(context.instructions.bundle.rootPath, context.instructions.entryPath), "utf8"))
.toBe("Saved canonical instructions\n");
expect(await readFile(path.join(context.instructions.bundle.rootPath, "reference.md"), "utf8"))
.toBe("Sibling reference\n");
expect(serviceMocks.readCommittedForRuntime).toHaveBeenCalledWith({ companyId: "company-1", agentId: "agent-1" });
});
it("does not substitute canonical managed instructions into an external bundle", async () => {
serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "External instructions\n" } });
const context = await buildNativeRuntimeContext({
db: {} as Db,
agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterConfig: { instructionsBundleMode: "external" } },
runId: "run-1", runtimeConfig: {}, runtimeSkillEntries: [],
});
expect(await readFile(path.join(context.instructions.bundle.rootPath, context.instructions.entryPath), "utf8"))
.toBe("External instructions\n");
expect(serviceMocks.readCommittedForRuntime).not.toHaveBeenCalled();
});
it("keeps the pinned prompt immutable while exposing the registered editable instruction copy", async () => {
serviceMocks.exportFiles.mockResolvedValue({
entryFile: "instructions/CHARter.md",
files: { "instructions/CHARter.md": "Original instructions.\n" },
});
const workingRoot = path.join(temporaryRoots.at(-1)!, "private-run-copy");
await mkdir(path.join(workingRoot, "instructions"), { recursive: true });
await writeFile(path.join(workingRoot, "instructions/CHARter.md"), "Original instructions.\n");
const context = await buildNativeRuntimeContext({
db: {} as Db,
agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: {} },
runId: "run-1",
runtimeConfig: {},
runtimeSkillEntries: [],
instructionWorkingCopy: { rootPath: workingRoot, entryPath: "instructions/CHARter.md" },
});
expect(context.instructions.workingCopy).toEqual({ rootPath: workingRoot, entryPath: "instructions/CHARter.md" });
await writeFile(path.join(workingRoot, "instructions/CHARter.md"), "Persist this next time.\n");
expect(await readFile(path.join(context.instructions.bundle.rootPath, context.instructions.entryPath), "utf8"))
.toBe("Original instructions.\n");
});
it.each(["disabled", "degraded"] as const)(
"omits an unavailable native MCP connection when it is %s without aborting runtime context creation",
async (unavailableState) => {
serviceMocks.exportFiles.mockResolvedValue({
entryFile: "AGENTS.md",
files: { "AGENTS.md": "Continue work without unavailable apps.\n" },
});
serviceMocks.getEffectiveToolAssignmentsForAgent.mockResolvedValue({
agentId: "agent-1",
profiles: [],
entries: [{ effect: "include", connectionId: "connection-1" }],
bindings: [],
allowedTools: [{ id: "tool-1", connectionId: "connection-1" }],
allowedToolNames: ["issues.read"],
installedConnections: [{
id: "connection-1",
transport: "mcp_remote",
enabled: unavailableState !== "disabled",
status: unavailableState === "disabled" ? "disabled" : "active",
healthStatus: unavailableState === "degraded" ? "degraded" : "healthy",
}],
});
const context = await buildNativeRuntimeContext({
db: {} as Db,
agent: {
id: "agent-1",
companyId: "company-1",
name: "Reviewer",
adapterType: "paperclip_runner",
adapterConfig: {},
},
runId: "run-1",
runtimeConfig: {},
runtimeSkillEntries: [],
});
expect(context.mcp.bindingId).toBeNull();
expect(context.mcp.assignmentSetId).toMatch(/^sha256:[a-f0-9]{64}$/);
},
);
it("keeps healthy native MCP connections when another assigned connection is unavailable", async () => {
serviceMocks.exportFiles.mockResolvedValue({
entryFile: "AGENTS.md",
files: { "AGENTS.md": "Continue work with the apps that are available.\n" },
});
serviceMocks.getEffectiveToolAssignmentsForAgent.mockResolvedValue({
agentId: "agent-1",
profiles: [],
entries: [
{ effect: "include", connectionId: "connection-expired" },
{ effect: "include", connectionId: "connection-healthy" },
],
bindings: [],
allowedTools: [
{ id: "tool-expired", connectionId: "connection-expired" },
{ id: "tool-healthy", connectionId: "connection-healthy" },
],
allowedToolNames: ["expired.read", "healthy.read"],
installedConnections: [
{
id: "connection-expired",
transport: "mcp_remote",
enabled: true,
status: "active",
healthStatus: "degraded",
},
{
id: "connection-healthy",
transport: "mcp_remote",
enabled: true,
status: "active",
healthStatus: "healthy",
},
],
});
const context = await buildNativeRuntimeContext({
db: {} as Db,
agent: {
id: "agent-1",
companyId: "company-1",
name: "Reviewer",
adapterType: "paperclip_runner",
adapterConfig: {},
},
runId: "run-1",
runtimeConfig: {},
runtimeSkillEntries: [],
});
expect(context.mcp.bindingId).toBe("native-mcp:run-1");
expect(context.mcp.assignmentSetId).toMatch(/^sha256:[a-f0-9]{64}$/);
});
it("materializes every instruction and selected-skill file as immutable, content-addressed context", async () => {
serviceMocks.exportFiles.mockResolvedValue({
entryFile: "AGENTS.md",
files: {
"AGENTS.md": "Follow the agent instructions.\n",
"references/policy.md": "Company policy sibling.\n",
},
});
const sourceRoot = await mkdtemp(path.join(tmpdir(), "paperclip-native-skill-"));
temporaryRoots.push(sourceRoot);
const selectedRoot = path.join(sourceRoot, "reviewer");
const unselectedRoot = path.join(sourceRoot, "unused");
await mkdir(path.join(selectedRoot, "references"), { recursive: true });
await mkdir(unselectedRoot, { recursive: true });
await writeFile(path.join(selectedRoot, "SKILL.md"), "# Reviewer\nUse the checklist.\n");
await writeFile(path.join(selectedRoot, "references", "checklist.md"), "- Verify tests\n");
await writeFile(path.join(unselectedRoot, "SKILL.md"), "# Not selected\n");
const entries: PaperclipSkillEntry[] = [
{ key: "company-1/reviewer", runtimeName: "reviewer", source: selectedRoot, versionId: "version-1" },
{ key: "company-1/unused", runtimeName: "unused", source: unselectedRoot, versionId: "version-2" },
];
const input = {
db: {} as Db,
agent: {
id: "agent-1",
companyId: "company-1",
name: "Reviewer",
adapterType: "paperclip_runner",
adapterConfig: {},
},
runId: "run-1",
runtimeConfig: { paperclipSkillSync: { desiredSkills: ["company-1/reviewer"] } },
runtimeSkillEntries: entries,
};
const context = await buildNativeRuntimeContext(input);
const repeated = await buildNativeRuntimeContext(input);
expect(serviceMocks.exportFiles).toHaveBeenCalledWith(input.agent, { rejectSymlinks: true });
expect(serviceMocks.getEffectiveToolAssignmentsForAgent).toHaveBeenCalledWith("company-1", "agent-1");
expect(context.instructions.entryPath).toBe("AGENTS.md");
expect(await readFile(path.join(context.instructions.bundle.rootPath, "AGENTS.md"), "utf8"))
.toBe("Follow the agent instructions.\n");
expect(await readFile(path.join(context.instructions.bundle.rootPath, "references", "policy.md"), "utf8"))
.toBe("Company policy sibling.\n");
const unselected = await buildNativeRuntimeContext({ ...input, runtimeConfig: {} });
expect(unselected.skills).toEqual([]);
expect(context.skills).toHaveLength(1);
expect(context.skills[0]).toMatchObject({
key: "company-1/reviewer",
runtimeName: "reviewer",
versionId: "version-1",
});
expect(await readFile(path.join(context.skills[0]!.bundle.rootPath, "SKILL.md"), "utf8"))
.toContain("Use the checklist");
expect(await readFile(path.join(context.skills[0]!.bundle.rootPath, "references", "checklist.md"), "utf8"))
.toBe("- Verify tests\n");
expect((await stat(context.instructions.bundle.rootPath)).mode & 0o222).toBe(0);
expect((await stat(path.join(context.instructions.bundle.rootPath, "AGENTS.md"))).mode & 0o222).toBe(0);
expect((await stat(context.skills[0]!.bundle.rootPath)).mode & 0o222).toBe(0);
expect(context.mcp).toMatchObject({
bindingId: "native-mcp:run-1",
assignmentSetId: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
digest: expect.stringMatching(/^[a-f0-9]{64}$/),
});
expect(repeated.aggregateDigest).toBe(context.aggregateDigest);
expect(repeated.instructions.bundle.rootPath).toBe(context.instructions.bundle.rootPath);
expect(repeated.skills[0]!.bundle.rootPath).toBe(context.skills[0]!.bundle.rootPath);
});
it("does not materialize assigned skills or external MCP in preconfigured mode", async () => {
serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "Test\n" } });
const context = await buildNativeRuntimeContext({ db: {} as Db,
agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: {} },
runId: "run-1", runtimeConfig: { homeMode: "preconfigured", paperclipSkillSync: { desiredSkills: ["missing"] } },
runtimeSkillEntries: [{ key: "missing", runtimeName: "missing", source: "/does/not/exist", sourceStatus: "missing" }],
});
expect(context.skills).toEqual([]);
expect(context.mcp.bindingId).toBeNull();
expect(serviceMocks.getEffectiveToolAssignmentsForAgent).not.toHaveBeenCalled();
});
it("fails closed for a missing assigned skill and omits a stale legacy operational skill", async () => {
serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "Test\n" } });
const base = {
db: {} as Db,
agent: {
id: "agent-1",
companyId: "company-1",
name: "Reviewer",
adapterType: "paperclip_runner",
adapterConfig: {},
},
runId: "run-1",
};
await expect(buildNativeRuntimeContext({
...base,
runtimeConfig: { paperclipSkillSync: { desiredSkills: ["company-1/missing"] } },
runtimeSkillEntries: [{
key: "company-1/missing",
runtimeName: "missing",
source: "/does/not/exist",
sourceStatus: "missing",
missingDetail: "assigned skill checkout is unavailable",
}],
})).rejects.toThrow("assigned skill checkout is unavailable");
const supportedRoot = await mkdtemp(path.join(tmpdir(), "paperclip-native-supported-skill-"));
temporaryRoots.push(supportedRoot);
await writeFile(path.join(supportedRoot, "SKILL.md"), "# Supported\n");
const context = await buildNativeRuntimeContext({
...base,
runtimeConfig: {
paperclipSkillSync: {
desiredSkills: [PAPERCLIP_OPERATIONAL_SKILL_KEY, "company-1/supported"],
},
},
runtimeSkillEntries: [
{
key: PAPERCLIP_OPERATIONAL_SKILL_KEY,
runtimeName: "paperclip",
source: "/unused",
},
{
key: "company-1/supported",
runtimeName: "supported",
source: supportedRoot,
},
],
});
expect(context.skills.map((skill) => skill.key)).toEqual(["company-1/supported"]);
});
});
it("pins both permitted GitHub tool catalogs even when another user’s health probe reports missing credentials", async () => {
const connections = ["github-A", "github-B"].map((id) => ({
id, status: "active", enabled: true, healthStatus: "missing_secret", transport: "mcp_remote",
config: { sourceTemplateKey: "github" }, transportConfig: {},
}));
serviceMocks.getEffectiveToolAssignmentsForAgent.mockResolvedValue({
entries: connections.map((connection) => ({ effect: "include", connectionId: connection.id })),
installedConnections: connections,
allowedTools: connections.map((connection) => ({ id: `${connection.id}-get_me`, connectionId: connection.id })),
});
const limit = vi.fn(async () => [{ responsibleUserId: "A", activeIdentityContextId: "context-A" }]);
const db = { select: () => ({ from: () => ({ where: () => ({ limit }) }) }) } as unknown as Db;
const snapshot = await resolveNativeRuntimeMcpSnapshot({ db, agent: { id: "agent-1", companyId: "company-1" }, runId: "run-1" });
const expected = createHash("sha256").update(JSON.stringify({
version: 1, agentId: "agent-1", connections: ["github-A", "github-B"],
tools: ["github-A-get_me", "github-B-get_me"],
})).digest("hex");
expect(snapshot.digest).toBe(expected);
expect(snapshot.bindingId).toBe("native-mcp:run-1");
expect(limit).toHaveBeenCalledOnce();
});
it("pins channel tools only when the current task run is bound to that bot", async () => {
serviceMocks.getEffectiveToolAssignmentsForAgent.mockResolvedValue({
entries: [{ effect: "include", connectionId: "bot-connection" }],
installedConnections: [{
id: "bot-connection", status: "active", enabled: true, healthStatus: "healthy",
transport: "chat_sdk", config: { sourceTemplateKey: "github-chat" }, transportConfig: {},
}],
allowedTools: [{ id: "bot-read-pr", connectionId: "bot-connection" }],
});
const input = { db: {} as Db, agent: { id: "agent-1", companyId: "company-1" }, runId: "run-1" };
expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBeNull();
serviceMocks.githubBotConnectionIdsForRun.mockResolvedValue(new Set(["bot-connection"]));
expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBe("native-mcp:run-1");
expect(serviceMocks.githubBotConnectionIdsForRun).toHaveBeenLastCalledWith(input.db, "company-1", "agent-1", "run-1");
});
it("pins a voice tool only when the current requester can call through that assigned endpoint", async () => {
serviceMocks.getEffectiveToolAssignmentsForAgent.mockResolvedValue({ entries: [{ effect: "include", connectionId: "voice-connection" }], installedConnections: [{ id: "voice-connection", status: "active", enabled: true, healthStatus: "healthy", transport: "voice", config: { provider: "speko" }, transportConfig: {} }], allowedTools: [{ id: "phone-tool", connectionId: "voice-connection" }] });
const limit = vi.fn().mockResolvedValue([{ issueId: "task-1", activeIdentityContextId: "identity-1" }]);
const db = { select: () => ({ from: () => ({ where: () => ({ limit }) }) }) } as unknown as Db;
const input = { db, agent: { id: "agent-1", companyId: "company-1" }, runId: "run-1" };
expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBeNull();
serviceMocks.spekoToolsForSession.mockResolvedValue([{ connectionId: "voice-connection" }]);
expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBe("native-mcp:run-1");
expect(serviceMocks.spekoToolsForSession).toHaveBeenLastCalledWith(db, { companyId: "company-1", agentId: "agent-1", runId: "run-1", issueId: "task-1", identityContextId: "identity-1" });
});
it("keeps instructions, selected skills, and MCP concurrent and distinguishes cold materialization from warm verification", async () => {
vi.stubEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector.test");
const exporter = new InMemorySpanExporter();
const provider = new TracerProvider({ spanProcessors: [new SimpleSpanProcessor({ exporter })] });
const manager = new AsyncLocalStorageContextManager().enable();
expect(otelContext.setGlobalContextManager(manager)).toBe(true);
expect(trace.setGlobalTracerProvider(provider)).toBe(true);
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
serviceMocks.exportFiles.mockImplementation(async () => {
await gate;
return { entryFile: "AGENTS.md", files: { "AGENTS.md": "Private fixture instructions" } };
});
const source = path.join(temporaryRoots.at(-1)!, "selected-skill");
await mkdir(source);
await writeFile(path.join(source, "SKILL.md"), "Private skill fixture");
const input = {
db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterConfig: {} },
runId: "concurrent-context", runtimeConfig: { paperclipSkillSync: { desiredSkills: ["selected"] } },
runtimeSkillEntries: [{ key: "selected", runtimeName: "selected", source }],
};
const pending = withRunExecutionTiming(input.runId, () => buildNativeRuntimeContext(input));
try {
await vi.waitFor(() => {
expect(exporter.getFinishedSpans().map(span => span.name)).toEqual(expect.arrayContaining(["runtime_context.skills", "runtime_context.mcp"]));
});
expect(exporter.getFinishedSpans().some(span => span.name === "runtime_context.instructions")).toBe(false);
release();
const cold = await pending;
expect(cold.skills).toHaveLength(1);
expect(exporter.getFinishedSpans().some(span => span.name === "runtime_context.asset_materialize")).toBe(true);
exporter.reset();
const warm = await withRunExecutionTiming(input.runId, () => buildNativeRuntimeContext(input));
expect(warm.aggregateDigest).toBe(cold.aggregateDigest);
const spans = exporter.getFinishedSpans();
expect(spans.some(span => span.name === "runtime_context.asset_verify")).toBe(true);
expect(spans.some(span => span.name === "runtime_context.asset_materialize")).toBe(false);
expect(JSON.stringify(spans.map(span => span.attributes))).not.toContain("Private");
const siblings = spans.filter(span => ["runtime_context.instructions", "runtime_context.skills", "runtime_context.mcp"].includes(span.name));
expect(siblings).toHaveLength(3);
expect(new Set(siblings.map(span => span.parentSpanContext?.spanId)).size).toBe(1);
} finally {
release();
await pending.catch(() => undefined);
trace.disable(); otelContext.disable(); manager.disable();
await provider.shutdown();
vi.unstubAllEnvs();
}
});