mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
test(e2e): bind installed team to selected OpenCode runtime
This commit is contained in:
1 parent
1c6021a4e6
commit
250ba62791
2 files changed
+108
-2
No files matched your search
@@ -1,5 +1,9 @@
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import type { Agent } from "../../../packages/shared/src/types/agent.js";
|
||||
import { CHAT_OPENCODE_MODEL } from "../chat-runtime-profile.js";
|
||||
import { catalogTeamInstallSchema } from "../../../packages/shared/src/validators/teams-catalog.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { teamInstallExternalCases } from "./external-team-install.js";
|
||||
import { teamInstallExternalCases, selectedTeamInstallRuntime, teamInstallOverrides, verifyInstalledTeamRuntime } from "./external-team-install.js";
|
||||
function valid() {
|
||||
const team = {
|
||||
id: "team",
|
||||
@@ -10,6 +14,8 @@ function valid() {
|
||||
const agent = (id: string) => ({
|
||||
id,
|
||||
companyId: "company",
|
||||
adapterType: "paperclip_runner",
|
||||
adapterConfig: { provider: "opencode", model: CHAT_OPENCODE_MODEL, timeoutSec: 0, env: { OPENROUTER_API_KEY: { type: "secret_ref", secretId: "account", version: "latest" } } },
|
||||
urlKey: id,
|
||||
metadata: {
|
||||
paperclip: { catalogTeam: { catalogId: "team", originHash: "hash" } },
|
||||
@@ -96,6 +102,8 @@ function valid() {
|
||||
localSkillStillOccupiesSlug: true,
|
||||
},
|
||||
team,
|
||||
selectedRuntime: { profileId: "runner-opencode", adapterType: "paperclip_runner", provider: "opencode", model: CHAT_OPENCODE_MODEL, accountRef: { type: "secret_ref", secretId: "account", version: "latest" } },
|
||||
runtimeReadback: { memberIds: ["coder", "cto"], provider: "opencode", model: CHAT_OPENCODE_MODEL, verifiedBeforeImportedWork: true },
|
||||
before,
|
||||
first: structuredClone(after),
|
||||
after,
|
||||
@@ -152,3 +160,55 @@ describe("real catalog installer oracle", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function runtimeFixture() {
|
||||
const o = valid();
|
||||
const ctx = { input: { execution: { profile: { id: "runner-opencode", provider: "opencode", model: CHAT_OPENCODE_MODEL, credential: "OPENROUTER_API_KEY" } }, fixtures: { company: { id: "company" }, secretRefs: { OPENROUTER_API_KEY: { type: "secret_ref", secretId: "account", version: "latest" } } } } } as unknown as ChatCatalogContext;
|
||||
const actor = { ...o.state.after.agents[0]!, id: "ceo", adapterConfig: { ...o.state.after.agents[0]!.adapterConfig, cwd: "/not-a-team-cwd", instructionsFilePath: "/not-a-team-instruction", env: { ...o.state.after.agents[0]!.adapterConfig.env, UNRELATED_VALUE: "must-not-copy" } } } as unknown as Agent;
|
||||
return { o, ctx, actor };
|
||||
}
|
||||
it("the actual public per-slug override schema accepts only the selected runtime/account configuration, not CEO instructions or cwd", () => {
|
||||
const f = runtimeFixture(), runtime = selectedTeamInstallRuntime(f.ctx, f.actor), overrides = teamInstallOverrides(f.o.state.team as never, runtime);
|
||||
const payload = catalogTeamInstallSchema.parse({ targetManagerAgentId: "ceo", adapterOverrides: overrides });
|
||||
expect(Object.keys(payload.adapterOverrides!)).toEqual(["cto", "coder"]);
|
||||
for (const entry of Object.values(payload.adapterOverrides!)) {
|
||||
expect(entry).toEqual({ adapterType: "paperclip_runner", adapterConfig: { provider: "opencode", model: CHAT_OPENCODE_MODEL, timeoutSec: 0, lifecycleMode: "per_turn", opencodePermissionMode: "allow", env: { OPENROUTER_API_KEY: { type: "secret_ref", secretId: "account", version: "latest" } } } });
|
||||
}
|
||||
expect(JSON.stringify(payload)).not.toContain("must-not-copy"); expect(JSON.stringify(payload)).not.toContain("not-a-team");
|
||||
});
|
||||
it.each(["provider", "model", "account", "plaintext", "company"])("rejects a wrong selected %s before exposing installer context", invalid => {
|
||||
const f = runtimeFixture();
|
||||
if (invalid === "provider") f.actor.adapterConfig.provider = "codex";
|
||||
if (invalid === "model") f.actor.adapterConfig.model = "openrouter/other/model";
|
||||
if (invalid === "account") (f.actor.adapterConfig.env as Record<string, unknown>).OPENROUTER_API_KEY = { type: "secret_ref", secretId: "foreign", version: "latest" };
|
||||
if (invalid === "plaintext") (f.actor.adapterConfig.env as Record<string, unknown>).OPENROUTER_API_KEY = "must-not-copy";
|
||||
if (invalid === "company") f.actor.companyId = "foreign";
|
||||
expect(() => selectedTeamInstallRuntime(f.ctx, f.actor)).toThrow("actual configured");
|
||||
});
|
||||
it("independently reads every actual member before another human request or imported native work", async () => {
|
||||
const f = runtimeFixture(), seen: string[] = [];
|
||||
Object.assign(f.ctx.input, { api: { get: async (route: string) => { seen.push(route); return f.o.state.after.agents.find(agent => route.endsWith(agent.id)); } } });
|
||||
Object.assign(f.ctx, { allRuns: async () => [{ id: "source", agentId: "ceo", status: "succeeded", startedAt: "2026-10-28T01:00:00Z" }] });
|
||||
const proof = await verifyInstalledTeamRuntime(f.ctx, f.o.state.team as never, f.o.state.first as never, selectedTeamInstallRuntime(f.ctx, f.actor));
|
||||
expect(seen).toEqual(["/api/agents/cto", "/api/agents/coder"]); expect(proof).toMatchObject({ memberIds: ["coder", "cto"], verifiedBeforeImportedWork: true, model: CHAT_OPENCODE_MODEL });
|
||||
});
|
||||
it.each(["model", "account", "wrong returned identity", "already started"])("fails actual member runtime readback for %s", async invalid => {
|
||||
const f = runtimeFixture();
|
||||
const saved = structuredClone(f.o.state.after.agents);
|
||||
if (invalid === "model") saved[0]!.adapterConfig.model = "openrouter/other/model";
|
||||
if (invalid === "account") saved[0]!.adapterConfig.env.OPENROUTER_API_KEY.secretId = "foreign";
|
||||
if (invalid === "wrong returned identity") saved[0]!.id = "other";
|
||||
Object.assign(f.ctx.input, { api: { get: async (route: string) => saved[route.endsWith("cto") ? 0 : 1] } });
|
||||
Object.assign(f.ctx, { allRuns: async () => invalid === "already started" ? [{ id: "work", agentId: "cto", status: "running", startedAt: "2026-10-28T01:00:00Z" }] : [] });
|
||||
await expect(verifyInstalledTeamRuntime(f.ctx, f.o.state.team as never, f.o.state.first as never, selectedTeamInstallRuntime(f.ctx, f.actor))).rejects.toThrow();
|
||||
});
|
||||
it("real resource counts and actor attribution cannot conceal wrong member providers/models/accounts or missing pre-work readback", () => {
|
||||
for (const change of [
|
||||
(o: ReturnType<typeof valid>) => { o.state.after.agents[0]!.adapterConfig.provider = "codex"; },
|
||||
(o: ReturnType<typeof valid>) => { o.state.first.agents[0]!.adapterConfig.model = "openrouter/other/model"; },
|
||||
(o: ReturnType<typeof valid>) => { o.state.after.agents[0]!.adapterConfig.env.OPENROUTER_API_KEY.secretId = "foreign"; },
|
||||
(o: ReturnType<typeof valid>) => { o.state.runtimeReadback.verifiedBeforeImportedWork = false; },
|
||||
(o: ReturnType<typeof valid>) => { o.state.runtimeReadback.memberIds = ["other", "cto"]; },
|
||||
(o: ReturnType<typeof valid>) => { o.state.selectedRuntime.model = "openrouter/other/model"; o.state.after.agents.forEach(agent => { agent.adapterConfig.model = "openrouter/other/model"; }); },
|
||||
]) { const o = valid(); change(o); expect(teamInstallExternalCases[0]!.grade(o).passed).toBe(false); }
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { CHAT_OPENCODE_MODEL, CHAT_OPENCODE_PROFILE } from "../chat-runtime-profile.js";
|
||||
import type { SecretReference } from "../types.js";
|
||||
import {
|
||||
prepareTeamConditions,
|
||||
observeTeamConditions,
|
||||
@@ -54,6 +57,39 @@ export function catalogTeamMembers(
|
||||
(agent) => catalogTeamProvenance(agent)?.catalogId === teamId,
|
||||
);
|
||||
}
|
||||
interface TeamInstallRuntime {
|
||||
profileId: string; adapterType: "paperclip_runner"; provider: "opencode"; model: string; accountRef: SecretReference;
|
||||
}
|
||||
export function selectedTeamInstallRuntime(ctx: ChatCatalogContext, actor: Agent) {
|
||||
const profile = ctx.input.execution.profile, config = actor.adapterConfig;
|
||||
const ref = ctx.input.fixtures.secretRefs.OPENROUTER_API_KEY;
|
||||
const bound = (config.env as Record<string, unknown> | undefined)?.OPENROUTER_API_KEY as SecretReference | undefined;
|
||||
if (profile.id !== CHAT_OPENCODE_PROFILE || profile.provider !== "opencode" || profile.model !== CHAT_OPENCODE_MODEL || profile.credential !== "OPENROUTER_API_KEY"
|
||||
|| actor.companyId !== ctx.input.fixtures.company.id || actor.adapterType !== "paperclip_runner" || config.provider !== "opencode" || config.model !== CHAT_OPENCODE_MODEL || config.timeoutSec !== 0
|
||||
|| !ref || ref.type !== "secret_ref" || !ref.secretId || ref.version !== "latest" || Object.keys(ref).sort().join() !== "secretId,type,version"
|
||||
|| bound?.type !== ref.type || bound.secretId !== ref.secretId || bound.version !== ref.version)
|
||||
throw new Error("Selected team installer must use its actual configured OpenCode OpenRouter1017 account");
|
||||
return { profileId: profile.id, adapterType: "paperclip_runner", provider: "opencode", model: profile.model, accountRef: { type: ref.type, secretId: ref.secretId, version: ref.version } } as TeamInstallRuntime;
|
||||
}
|
||||
export function teamInstallOverrides(team: Pick<CatalogTeam, "agentSlugs">, runtime: TeamInstallRuntime) {
|
||||
return Object.fromEntries(team.agentSlugs.map(slug => [slug, { adapterType: runtime.adapterType, adapterConfig: { provider: runtime.provider, model: runtime.model, timeoutSec: 0, lifecycleMode: "per_turn", opencodePermissionMode: "allow", env: { OPENROUTER_API_KEY: runtime.accountRef } } }]));
|
||||
}
|
||||
export function teamMemberRuntimeMatches(agent: Agent, expected: TeamInstallRuntime, companyId: string) {
|
||||
const config = agent.adapterConfig, ref = (config?.env as Record<string, unknown> | undefined)?.OPENROUTER_API_KEY as SecretReference | undefined;
|
||||
return expected?.profileId === CHAT_OPENCODE_PROFILE && expected.adapterType === "paperclip_runner" && expected.provider === "opencode" && expected.model === CHAT_OPENCODE_MODEL
|
||||
&& expected.accountRef?.type === "secret_ref" && !!expected.accountRef.secretId && expected.accountRef.version === "latest"
|
||||
&& agent.companyId === companyId && agent.adapterType === expected.adapterType && config?.provider === expected.provider && config.model === expected.model && config.timeoutSec === 0
|
||||
&& ref?.type === "secret_ref" && ref.secretId === expected.accountRef.secretId && ref.version === "latest";
|
||||
}
|
||||
export async function verifyInstalledTeamRuntime(ctx: ChatCatalogContext, team: CatalogTeam, first: TeamResourceSnapshot, runtime: TeamInstallRuntime) {
|
||||
const members = catalogTeamMembers(first, team.id);
|
||||
if (members.length !== team.agentSlugs.length) throw new Error("Installed team runtime readback requires the complete actual package");
|
||||
const saved = await Promise.all(members.map(member => ctx.input.api.get<Agent>(`/api/agents/${member.id}`)));
|
||||
if (JSON.stringify(ids(saved)) !== JSON.stringify(ids(members)) || saved.some(member => !teamMemberRuntimeMatches(member, runtime, ctx.input.fixtures.company.id))) throw new Error("Actual saved installed-member runtime differs from selected OpenCode1017");
|
||||
const memberIds = new Set(members.map(member => member.id));
|
||||
if ((await ctx.allRuns()).some(run => memberIds.has(run.agentId) && !!run.startedAt)) throw new Error("Imported member work started before independent runtime readback");
|
||||
return { memberIds: saved.map(member => member.id).sort(), provider: runtime.provider, model: runtime.model, verifiedBeforeImportedWork: true };
|
||||
}
|
||||
const ids = (rows: Array<{ id: string }>) => rows.map((row) => row.id).sort();
|
||||
function hasResourceLink(
|
||||
links: string[],
|
||||
@@ -131,6 +167,12 @@ export function gradeTeamInstall(value: unknown) {
|
||||
after.agents.length - before.agents.length === team.counts.agents,
|
||||
"Exactly the package's agents exist in this company with immutable catalog provenance; no unrelated or duplicate hires satisfy the count.",
|
||||
);
|
||||
const runtime = o.state.selectedRuntime as TeamInstallRuntime, runtimeReadback = o.state.runtimeReadback as { memberIds?: string[]; provider?: string; model?: string; verifiedBeforeImportedWork?: boolean } | undefined;
|
||||
c.add("selected-runtime-preserved-for-every-installed-member", !!runtime && !!first && !!after && !!team
|
||||
&& runtimeReadback?.verifiedBeforeImportedWork === true && runtimeReadback.provider === runtime.provider && runtimeReadback.model === runtime.model && JSON.stringify(runtimeReadback.memberIds) === JSON.stringify(ids(members))
|
||||
&& catalogTeamMembers(first, team.id).length === team.agentSlugs.length && catalogTeamMembers(first, team.id).every(member => teamMemberRuntimeMatches(member, runtime, o.companyId))
|
||||
&& members.every(member => teamMemberRuntimeMatches(member, runtime, o.companyId)),
|
||||
"Every actual imported member retains the selected OpenCode/OpenRouter1017 and exact existing company account reference; public saved configuration is independently read before any imported work.");
|
||||
c.add(
|
||||
"intended-project-and-routine-resources",
|
||||
!!team &&
|
||||
@@ -252,6 +294,8 @@ export const teamInstallExternalCases = [
|
||||
throw new Error(
|
||||
"Setup blocker: actual selected installer actor lacks a ready account or creation authority",
|
||||
);
|
||||
const selectedRuntime = selectedTeamInstallRuntime(ctx, actor);
|
||||
const adapterOverrides = teamInstallOverrides(team, selectedRuntime);
|
||||
const conditional = await prepareTeamConditions(ctx, team);
|
||||
const preview = conditional.conditionalPreview;
|
||||
if (preview.errors.length || preview.portabilityPreview.errors.length)
|
||||
@@ -266,10 +310,11 @@ export const teamInstallExternalCases = [
|
||||
await ctx.input.api.post(`${root(ctx)}/chats/${actor.id}`);
|
||||
await ctx.refreshIssue();
|
||||
await ctx.input.api.patch(`/api/issues/${ctx.issue()!.id}`, {
|
||||
description: `The accepted website team preview is ${team.name}, catalog key ${team.key}, content hash ${team.contentHash}. Install the complete selected package under the current CEO using this company's existing eligible runtime account. No team resource is installed yet. The selected runtime uses ${actor.adapterType} in environment ${ctx.input.fixtures.environment.id}.`,
|
||||
description: `The accepted website team preview is ${team.name}, catalog key ${team.key}, content hash ${team.contentHash}. Install the complete selected package under the current CEO using this company's existing eligible runtime account. No team resource is installed yet. The selected runtime uses ${actor.adapterType} in environment ${ctx.input.fixtures.environment.id}. The existing eligible runtime is OpenCode/OpenRouter ${selectedRuntime.model}; its credential stays in the existing same-company secret reference. The supported installer takes these per-slug adapterOverrides for this accepted runtime selection: ${JSON.stringify(adapterOverrides)}.`,
|
||||
});
|
||||
return {
|
||||
...conditional,
|
||||
selectedRuntime,
|
||||
team,
|
||||
before,
|
||||
companyPrefix: ctx.input.fixtures.company.issuePrefix,
|
||||
@@ -303,6 +348,7 @@ export const teamInstallExternalCases = [
|
||||
async act(ctx, state) {
|
||||
await waitForReply(ctx);
|
||||
state.first = await observeTeamResources(ctx);
|
||||
state.runtimeReadback = await verifyInstalledTeamRuntime(ctx, state.team as CatalogTeam, state.first as TeamResourceSnapshot, state.selectedRuntime as TeamInstallRuntime);
|
||||
state.initialRunId = (await ctx.allRuns())[0]?.id;
|
||||
state.firstConditional = await observeTeamConditions(ctx, state);
|
||||
const firstObservation = await observeExternal(ctx, state);
|
||||
|
||||
Reference in new issue
Block a user