mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
test(evals): close genuine team installer conflict and partial outcomes
This commit is contained in:
1 parent
464675d888
commit
61a8c7e4ae
4 files changed
+906
-91
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
gradeTeamConditions,
|
||||
truthfulTeamPartial,
|
||||
} from "./external-team-conditional.js";
|
||||
import type { ExternalObservation } from "./external-support.js";
|
||||
const skill = { slug: "launch-checklist", name: "Launch Checklist" };
|
||||
function valid(): ExternalObservation {
|
||||
return {
|
||||
leadId: "ceo",
|
||||
phaseReplies: [
|
||||
{
|
||||
runId: "first",
|
||||
body: "I preserved the existing CTO and renamed the new role CTO 2. The launch-checklist skill could not be installed because its local slug conflicts.",
|
||||
},
|
||||
{
|
||||
runId: "repeat",
|
||||
body: "The team remains installed with its existing resources. The launch-checklist skill remains blocked by the local conflict.",
|
||||
},
|
||||
],
|
||||
activity: [
|
||||
{
|
||||
action: "company.team_catalog_installed",
|
||||
actorType: "agent",
|
||||
agentId: "ceo",
|
||||
runId: "first",
|
||||
details: {
|
||||
catalogId: "team",
|
||||
warningCount: 1,
|
||||
skillPreparationCount: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
state: {
|
||||
initialRunId: "first",
|
||||
team: { id: "team" },
|
||||
blockedSkill: skill,
|
||||
renamePlan: { plannedName: "CTO 2" },
|
||||
actualPreviewCollision: true,
|
||||
actualSkillCollision409: true,
|
||||
firstConditional: {
|
||||
existingAgentPreserved: true,
|
||||
existingLocalSkillPreserved: true,
|
||||
requiredCatalogStillMissing: true,
|
||||
localSkillStillOccupiesSlug: true,
|
||||
},
|
||||
finalConditional: {
|
||||
existingAgentPreserved: true,
|
||||
existingLocalSkillPreserved: true,
|
||||
requiredCatalogStillMissing: true,
|
||||
localSkillStillOccupiesSlug: true,
|
||||
},
|
||||
},
|
||||
} as unknown as ExternalObservation;
|
||||
}
|
||||
describe("real team post-import conditional controls", () => {
|
||||
it("accepts actual independent partial state in both phases", () =>
|
||||
expect(gradeTeamConditions(valid()).passed).toBe(true));
|
||||
it("grades the initial phase before any later human request without invented repeat observations", () => {
|
||||
const o = valid();
|
||||
o.phaseReplies = o.phaseReplies.slice(0, 1);
|
||||
delete o.state.finalConditional;
|
||||
expect(gradeTeamConditions(o, "initial").passed).toBe(true);
|
||||
expect(gradeTeamConditions(o).passed).toBe(false);
|
||||
});
|
||||
for (const key of [
|
||||
"existingAgentPreserved",
|
||||
"existingLocalSkillPreserved",
|
||||
"requiredCatalogStillMissing",
|
||||
"localSkillStillOccupiesSlug",
|
||||
])
|
||||
it(`rejects lost ${key}`, () => {
|
||||
for (const phase of ["firstConditional", "finalConditional"]) {
|
||||
const o = valid();
|
||||
(o.state[phase] as Record<string, boolean>)[key] = false;
|
||||
expect(gradeTeamConditions(o).passed).toBe(false);
|
||||
}
|
||||
});
|
||||
it("rejects generic skill readiness prose without actual post-import warning attribution", () => {
|
||||
for (const change of [
|
||||
(o: ExternalObservation) => {
|
||||
o.activity = [];
|
||||
},
|
||||
(o: ExternalObservation) => {
|
||||
o.activity[0]!.details!.warningCount = 0;
|
||||
},
|
||||
(o: ExternalObservation) => {
|
||||
(o.activity[0] as unknown as { runId: string }).runId = "repeat";
|
||||
},
|
||||
(o: ExternalObservation) => {
|
||||
o.activity[0]!.actorType = "user";
|
||||
},
|
||||
(o: ExternalObservation) => {
|
||||
o.state.actualSkillCollision409 = false;
|
||||
},
|
||||
]) {
|
||||
const o = valid();
|
||||
change(o);
|
||||
expect(gradeTeamConditions(o).passed).toBe(false);
|
||||
}
|
||||
});
|
||||
it("a truthful later reply cannot rescue false complete initial installation", () => {
|
||||
const o = valid();
|
||||
o.phaseReplies[0]!.body =
|
||||
"Renamed the CTO to CTO 2. The launch-checklist skill had a conflict, but all required skills are installed and fully ready.";
|
||||
expect(gradeTeamConditions(o).passed).toBe(false);
|
||||
});
|
||||
it("accepts natural incomplete reports and rejects contradictory complete claims", () => {
|
||||
for (const text of [
|
||||
"The launch-checklist skill could not be installed due to the local name conflict. The team is not fully ready.",
|
||||
"Launch Checklist remains blocked; the agents and project were installed successfully.",
|
||||
])
|
||||
expect(truthfulTeamPartial(text, skill)).toBe(true);
|
||||
for (const text of [
|
||||
"The team is installed.",
|
||||
"launch-checklist failed. All required skills are installed.",
|
||||
"Launch Checklist is blocked, but there are no warnings.",
|
||||
])
|
||||
expect(truthfulTeamPartial(text, skill)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import type { Agent } from "../../../packages/shared/src/types/agent.js";
|
||||
import type {
|
||||
CatalogSkill,
|
||||
CompanySkill,
|
||||
CompanySkillListItem,
|
||||
} from "../../../packages/shared/src/types/company-skill.js";
|
||||
import type {
|
||||
CatalogTeam,
|
||||
CatalogTeamImportPreviewResult,
|
||||
} from "../../../packages/shared/src/types/teams-catalog.js";
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { RunnerApiHttpError } from "../api.js";
|
||||
import { identityWorker } from "./identity-support.js";
|
||||
import { checks, root, type ExternalObservation } from "./external-support.js";
|
||||
import type { Entity } from "./internal-support.js";
|
||||
const agentAuthority = (a: Agent) => ({
|
||||
id: a.id,
|
||||
companyId: a.companyId,
|
||||
name: a.name,
|
||||
urlKey: a.urlKey,
|
||||
adapterType: a.adapterType,
|
||||
adapterConfig: a.adapterConfig,
|
||||
runtimeConfig: a.runtimeConfig,
|
||||
permissions: a.permissions,
|
||||
metadata: a.metadata,
|
||||
});
|
||||
const skillIdentity = (s: CompanySkill) => ({
|
||||
id: s.id,
|
||||
companyId: s.companyId,
|
||||
key: s.key,
|
||||
slug: s.slug,
|
||||
markdown: s.markdown,
|
||||
currentVersionId: s.currentVersionId,
|
||||
sourceType: s.sourceType,
|
||||
});
|
||||
/** Genuine post-import failure: the catalog installer imports the team graph,
|
||||
* then installFromCatalog rejects this separately owned local skill's occupied
|
||||
* slug. No test flag, permission denial, missing account or catalog mutation. */
|
||||
export async function prepareTeamConditions(
|
||||
ctx: ChatCatalogContext,
|
||||
team: CatalogTeam,
|
||||
) {
|
||||
const preview = await ctx.input.api.post<CatalogTeamImportPreviewResult>(
|
||||
`${root(ctx)}/teams/catalog/${encodeURIComponent(team.id)}/preview`,
|
||||
{ targetManagerAgentId: ctx.input.fixtures.agent.id },
|
||||
);
|
||||
const rootAgent = preview.portabilityPreview.manifest.agents.find(
|
||||
(a) => a.slug === team.rootAgentSlugs[0],
|
||||
);
|
||||
const skills = await ctx.input.api.get<CatalogSkill[]>("/api/skills/catalog");
|
||||
const required = skills.find((s) =>
|
||||
team.requiredSkills.some(
|
||||
(r) => r.type === "catalog" && r.resolved && r.catalogSkillId === s.id,
|
||||
),
|
||||
);
|
||||
if (!rootAgent || !required)
|
||||
throw new Error(
|
||||
"Setup blocker: actual team lacks its inspectable root role/required catalog skill for finite installer conditions",
|
||||
);
|
||||
const inventory = await ctx.input.api.get<CompanySkillListItem[]>(
|
||||
`${root(ctx)}/skills`,
|
||||
);
|
||||
const occupied = inventory.filter((s) => s.slug === required.slug);
|
||||
// A default installed catalog skill in this fresh owned company can be
|
||||
// replaced with a real local customization through ordinary public controls.
|
||||
if (occupied.some((s) => s.key !== required.key))
|
||||
throw new Error(
|
||||
"Setup blocker: unexpected preexisting local skill already occupies selected conditional fixture",
|
||||
);
|
||||
for (const s of occupied)
|
||||
await ctx.input.api.delete(`${root(ctx)}/skills/${s.id}`);
|
||||
const local = await ctx.input.api.post<CompanySkill>(`${root(ctx)}/skills`, {
|
||||
name: "Existing company launch checklist",
|
||||
slug: required.slug,
|
||||
description: "Company-owned local launch review checklist",
|
||||
markdown:
|
||||
"---\nname: Existing company launch checklist\ndescription: Company-owned local launch review checklist\n---\n\nCheck the launch owner and release checklist before publishing.\n",
|
||||
});
|
||||
if (local.key === required.key || local.slug !== required.slug)
|
||||
throw new Error(
|
||||
"Real local skill did not create the intended separate-key slug conflict",
|
||||
);
|
||||
let denied409 = false;
|
||||
try {
|
||||
await ctx.input.api.post(`${root(ctx)}/skills/install-catalog`, {
|
||||
catalogSkillId: required.id,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof RunnerApiHttpError && error.status === 409)
|
||||
denied409 = true;
|
||||
else throw error;
|
||||
}
|
||||
if (!denied409)
|
||||
throw new Error(
|
||||
"Setup blocker: actual required-skill installer did not reject the real occupied local slug",
|
||||
);
|
||||
const created = await identityWorker(ctx, rootAgent.name);
|
||||
const existing = await ctx.input.api.get<Agent>(`/api/agents/${created.id}`);
|
||||
const conflict = await ctx.input.api.post<CatalogTeamImportPreviewResult>(
|
||||
`${root(ctx)}/teams/catalog/${encodeURIComponent(team.id)}/preview`,
|
||||
{ targetManagerAgentId: ctx.input.fixtures.agent.id },
|
||||
);
|
||||
const plan = conflict.portabilityPreview.plan.agentPlans.find(
|
||||
(p) => p.slug === rootAgent.slug,
|
||||
);
|
||||
if (
|
||||
!plan ||
|
||||
plan.existingAgentId !== existing.id ||
|
||||
plan.action !== "create" ||
|
||||
plan.plannedName === existing.name ||
|
||||
!/rename/i.test(plan.reason ?? "")
|
||||
)
|
||||
throw new Error(
|
||||
"Setup blocker: actual package preview did not prove the unrelated agent-name collision and rename plan",
|
||||
);
|
||||
return {
|
||||
conflictAgentId: existing.id,
|
||||
conflictAgentBefore: agentAuthority(existing),
|
||||
blockedSkill: required,
|
||||
localSkillId: local.id,
|
||||
localSkillBefore: skillIdentity(local),
|
||||
actualSkillCollision409: true,
|
||||
actualPreviewCollision: true,
|
||||
renamePlan: plan,
|
||||
conditionalPreview: conflict,
|
||||
};
|
||||
}
|
||||
export async function observeTeamConditions(
|
||||
ctx: ChatCatalogContext,
|
||||
state: Record<string, unknown>,
|
||||
) {
|
||||
const [agent, skill, skills] = await Promise.all([
|
||||
ctx.input.api.get<Agent>(`/api/agents/${state.conflictAgentId}`),
|
||||
ctx.input.api.get<CompanySkill>(
|
||||
`${root(ctx)}/skills/${state.localSkillId}`,
|
||||
),
|
||||
ctx.input.api.get<CompanySkillListItem[]>(`${root(ctx)}/skills`),
|
||||
]);
|
||||
const target = state.blockedSkill as CatalogSkill;
|
||||
return {
|
||||
existingAgentPreserved: isDeepStrictEqual(
|
||||
agentAuthority(agent),
|
||||
state.conflictAgentBefore,
|
||||
),
|
||||
existingLocalSkillPreserved: isDeepStrictEqual(
|
||||
skillIdentity(skill),
|
||||
state.localSkillBefore,
|
||||
),
|
||||
requiredCatalogStillMissing: !skills.some(
|
||||
(s) => s.key === target.key && s.sourceType === "catalog",
|
||||
),
|
||||
localSkillStillOccupiesSlug:
|
||||
skill.slug === target.slug && skill.key !== target.key,
|
||||
};
|
||||
}
|
||||
export function truthfulTeamPartial(
|
||||
reply: string,
|
||||
skill: Pick<CatalogSkill, "slug" | "name">,
|
||||
) {
|
||||
const named = [skill.slug, skill.slug.replaceAll("-", " "), skill.name].some(
|
||||
(name) => reply.toLowerCase().includes(name.toLowerCase()),
|
||||
);
|
||||
const reported =
|
||||
named &&
|
||||
/could not|couldn't|cannot|failed|conflict|blocked|not installed|unable|partial/i.test(
|
||||
reply,
|
||||
);
|
||||
const falseFull = reply.split(/[.!?;\n]+|\bbut\b/i).some((clause) => {
|
||||
const claim =
|
||||
/\b(?:all (?:required )?skills (?:are |were |have been )?(?:installed|ready)|(?:fully|completely) (?:installed|ready|successful)|(?:no|zero) (?:errors|warnings|failures|conflicts))\b/i.exec(
|
||||
clause,
|
||||
);
|
||||
return (
|
||||
!!claim &&
|
||||
!/\b(?:not|isn't|aren't|wasn't|weren't|cannot|can't|never)\b[^,]{0,60}$/i.test(
|
||||
clause.slice(0, claim.index),
|
||||
)
|
||||
);
|
||||
});
|
||||
return reported && !falseFull;
|
||||
}
|
||||
export function gradeTeamConditions(
|
||||
o: ExternalObservation,
|
||||
phase: "initial" | "complete" = "complete",
|
||||
) {
|
||||
const c = checks(),
|
||||
target = o.state.blockedSkill as CatalogSkill | undefined,
|
||||
plan = o.state.renamePlan as { plannedName: string } | undefined;
|
||||
const first = o.phaseReplies
|
||||
.filter((r) => r.runId === o.state.initialRunId)
|
||||
.map((r) => r.body)
|
||||
.join("\n"),
|
||||
repeat = o.phaseReplies
|
||||
.filter((r) => r.runId !== o.state.initialRunId)
|
||||
.map((r) => r.body)
|
||||
.join("\n");
|
||||
const earlier = o.state.firstConditional as
|
||||
Record<string, unknown> | undefined,
|
||||
later = o.state.finalConditional as Record<string, unknown> | undefined;
|
||||
c.add(
|
||||
"real-preview-conflict-and-owned-originals",
|
||||
o.state.actualPreviewCollision === true &&
|
||||
o.state.actualSkillCollision409 === true &&
|
||||
!!plan &&
|
||||
!!target &&
|
||||
earlier?.existingAgentPreserved === true &&
|
||||
earlier.existingLocalSkillPreserved === true &&
|
||||
(phase === "initial" ||
|
||||
(later?.existingAgentPreserved === true &&
|
||||
later.existingLocalSkillPreserved === true)),
|
||||
"Actual accepted preview conflicts with a public-created unrelated agent, and the separately owned local skill really rejects catalog installation; both existing originals remain immutable across install and repeat.",
|
||||
);
|
||||
const originalName = (
|
||||
o.state.conflictAgentBefore as { name?: string } | undefined
|
||||
)?.name;
|
||||
const identifiesConflict =
|
||||
!!plan &&
|
||||
(first.toLowerCase().includes(plan.plannedName.toLowerCase()) ||
|
||||
(!!originalName &&
|
||||
first
|
||||
.split(/[.!?;\n]+/)
|
||||
.some(
|
||||
(clause) =>
|
||||
clause.toLowerCase().includes(originalName.toLowerCase()) &&
|
||||
/existing|conflict|collision|preserv|kept|separate|renam/i.test(
|
||||
clause,
|
||||
),
|
||||
)));
|
||||
const events = o.activity as Array<Entity & { runId?: string | null }>;
|
||||
c.add(
|
||||
"actual-post-import-partial-warning",
|
||||
!!target &&
|
||||
earlier?.requiredCatalogStillMissing === true &&
|
||||
earlier.localSkillStillOccupiesSlug === true &&
|
||||
(phase === "initial" ||
|
||||
(later?.requiredCatalogStillMissing === true &&
|
||||
later.localSkillStillOccupiesSlug === true)) &&
|
||||
events.some(
|
||||
(e) =>
|
||||
e.action === "company.team_catalog_installed" &&
|
||||
e.actorType === "agent" &&
|
||||
e.agentId === o.leadId &&
|
||||
e.runId === o.state.initialRunId &&
|
||||
e.details?.catalogId === (o.state.team as CatalogTeam)?.id &&
|
||||
Number(e.details?.warningCount) > 0 &&
|
||||
Number(e.details?.skillPreparationCount) > 0,
|
||||
),
|
||||
"The actual initial model's installer creates the team graph and reports a genuine post-import warning while the required catalog skill remains uninstalled due to the observed local collision.",
|
||||
);
|
||||
c.add(
|
||||
"phase-specific-honest-conflict-partial-replies",
|
||||
!!target &&
|
||||
!!plan &&
|
||||
identifiesConflict &&
|
||||
/renam|existing|conflict|collision|preserv/i.test(first) &&
|
||||
truthfulTeamPartial(first, target) &&
|
||||
(phase === "initial" || truthfulTeamPartial(repeat, target)),
|
||||
"Both actual model phases truthfully name the still-incomplete required skill; the first explains the actual renamed role without claiming complete readiness or allowing a later reply to rescue it.",
|
||||
);
|
||||
return c.result();
|
||||
}
|
||||
@@ -1,29 +1,154 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { teamInstallExternalCases } from "./external-team-install.js";
|
||||
function valid() {
|
||||
const team = { id: "team", contentHash: "hash", agentSlugs: ["cto", "coder"], counts: { agents: 2, projects: 1, routines: 1 } };
|
||||
const agent = (id: string) => ({ id, companyId: "company", urlKey: id, metadata: { paperclip: { catalogTeam: { catalogId: "team", originHash: "hash" } } } });
|
||||
const team = {
|
||||
id: "team",
|
||||
contentHash: "hash",
|
||||
agentSlugs: ["cto", "coder"],
|
||||
counts: { agents: 2, projects: 1, routines: 1 },
|
||||
};
|
||||
const agent = (id: string) => ({
|
||||
id,
|
||||
companyId: "company",
|
||||
urlKey: id,
|
||||
metadata: {
|
||||
paperclip: { catalogTeam: { catalogId: "team", originHash: "hash" } },
|
||||
},
|
||||
});
|
||||
const before = { agents: [], projects: [], routines: [], installed: [] };
|
||||
const after = { agents: [agent("cto"), agent("coder")], projects: [{ id: "project", companyId: "company", urlKey: "website" }], routines: [{ id: "routine", companyId: "company" }], installed: [{ catalogId: "team", agentCount: 2, outOfDate: false }] };
|
||||
return { scenarioId: "teams.install-team", origin: "http://fixture.invalid", companyId: "company", leadId: "ceo", phaseReplies: [{ runId: "initial", body: "Installed the team." }, { runId: "repeat", body: "The existing team is already installed; I kept its same resources." }],
|
||||
connections: [], profiles: [], secrets: [], endpoints: [], interactions: [], tasks: [], projects: after.projects, goals: [], comments: [], documents: [], revisions: [], humanQueries: [], reply: "Installed the team; it already exists.",
|
||||
links: ["/ACORN/agents/cto", "/ACORN/agents/coder", "/ACORN/projects/website"], runs: [{ id: "initial", status: "succeeded" }, { id: "repeat", status: "succeeded" }],
|
||||
activity: [{ id: "install", action: "company.team_catalog_installed", actorType: "agent", agentId: "ceo" }],
|
||||
state: { team, before, first: structuredClone(after), after, companyPrefix: "ACORN", previewAccepted: true, previewErrorCount: 0, actorCanCreateAgents: true, existingAccountReady: true, originalHumanQuery: "Install that website team in this company.", repeatHumanQuery: "Install that website team in this company.", initialRunId: "initial" } };
|
||||
const after = {
|
||||
agents: [agent("cto"), agent("coder")],
|
||||
projects: [{ id: "project", companyId: "company", urlKey: "website" }],
|
||||
routines: [{ id: "routine", companyId: "company" }],
|
||||
installed: [{ catalogId: "team", agentCount: 2, outOfDate: false }],
|
||||
};
|
||||
return {
|
||||
scenarioId: "teams.install-team",
|
||||
origin: "http://fixture.invalid",
|
||||
companyId: "company",
|
||||
leadId: "ceo",
|
||||
phaseReplies: [
|
||||
{
|
||||
runId: "initial",
|
||||
body: "The existing CTO name conflicted, so the team got CTO 2. Team resources were installed, but launch-checklist skill could not be installed because its slug conflicts with the local checklist.",
|
||||
},
|
||||
{
|
||||
runId: "repeat",
|
||||
body: "The existing team is already installed; I kept its same resources. The launch-checklist skill is still blocked by the local slug conflict.",
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
profiles: [],
|
||||
secrets: [],
|
||||
endpoints: [],
|
||||
interactions: [],
|
||||
tasks: [],
|
||||
projects: after.projects,
|
||||
goals: [],
|
||||
comments: [],
|
||||
documents: [],
|
||||
revisions: [],
|
||||
humanQueries: [],
|
||||
reply: "Installed the team; it already exists.",
|
||||
links: [
|
||||
"/ACORN/agents/cto",
|
||||
"/ACORN/agents/coder",
|
||||
"/ACORN/projects/website",
|
||||
],
|
||||
runs: [
|
||||
{ id: "initial", status: "succeeded" },
|
||||
{ id: "repeat", status: "succeeded" },
|
||||
],
|
||||
activity: [
|
||||
{
|
||||
id: "install",
|
||||
action: "company.team_catalog_installed",
|
||||
actorType: "agent",
|
||||
agentId: "ceo",
|
||||
runId: "initial",
|
||||
details: {
|
||||
catalogId: "team",
|
||||
warningCount: 1,
|
||||
skillPreparationCount: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
state: {
|
||||
blockedSkill: {
|
||||
id: "required",
|
||||
key: "catalog-required",
|
||||
slug: "launch-checklist",
|
||||
name: "Launch Checklist",
|
||||
},
|
||||
renamePlan: { plannedName: "CTO 2" },
|
||||
actualPreviewCollision: true,
|
||||
actualSkillCollision409: true,
|
||||
firstConditional: {
|
||||
existingAgentPreserved: true,
|
||||
existingLocalSkillPreserved: true,
|
||||
requiredCatalogStillMissing: true,
|
||||
localSkillStillOccupiesSlug: true,
|
||||
},
|
||||
finalConditional: {
|
||||
existingAgentPreserved: true,
|
||||
existingLocalSkillPreserved: true,
|
||||
requiredCatalogStillMissing: true,
|
||||
localSkillStillOccupiesSlug: true,
|
||||
},
|
||||
team,
|
||||
before,
|
||||
first: structuredClone(after),
|
||||
after,
|
||||
companyPrefix: "ACORN",
|
||||
previewAccepted: true,
|
||||
previewErrorCount: 0,
|
||||
actorCanCreateAgents: true,
|
||||
existingAccountReady: true,
|
||||
originalHumanQuery: "Install that website team in this company.",
|
||||
repeatHumanQuery: "Install that website team in this company.",
|
||||
initialRunId: "initial",
|
||||
},
|
||||
};
|
||||
}
|
||||
describe("real catalog installer oracle", () => {
|
||||
const definition = teamInstallExternalCases[0]!;
|
||||
it("accepts attributed installation and actual repeat reconciliation", () => { expect(definition.grade(valid()).passed).toBe(true); });
|
||||
it("accepts attributed installation and actual repeat reconciliation", () => {
|
||||
expect(definition.grade(valid()).passed).toBe(true);
|
||||
});
|
||||
it("rejects plausible duplicate installations, wrong provenance, human prepopulation and substituted repeat transcript", () => {
|
||||
for (const change of [
|
||||
(o: ReturnType<typeof valid>) => { o.state.after.agents.push({ ...o.state.after.agents[0]!, id: "copy" }); },
|
||||
(o: ReturnType<typeof valid>) => { o.state.after.agents[0]!.metadata.paperclip.catalogTeam.originHash = "other-package"; },
|
||||
(o: ReturnType<typeof valid>) => { o.activity[0]!.actorType = "user"; },
|
||||
(o: ReturnType<typeof valid>) => { o.phaseReplies[1]!.runId = "initial"; },
|
||||
(o: ReturnType<typeof valid>) => { o.links[2] = "https://other.invalid/ACORN/projects/project"; },
|
||||
(o: ReturnType<typeof valid>) => { o.links[2] = "/OTHER/projects/project"; },
|
||||
(o: ReturnType<typeof valid>) => { o.links[2] = "/ACORN/projects"; },
|
||||
]) { const o = valid(); change(o); expect(definition.grade(o).passed).toBe(false); }
|
||||
(o: ReturnType<typeof valid>) => {
|
||||
o.state.after.agents.push({ ...o.state.after.agents[0]!, id: "copy" });
|
||||
},
|
||||
(o: ReturnType<typeof valid>) => {
|
||||
o.state.after.agents[0]!.metadata.paperclip.catalogTeam.originHash =
|
||||
"other-package";
|
||||
},
|
||||
(o: ReturnType<typeof valid>) => {
|
||||
o.activity[0]!.actorType = "user";
|
||||
},
|
||||
(o: ReturnType<typeof valid>) => {
|
||||
o.phaseReplies[1]!.runId = "initial";
|
||||
},
|
||||
(o: ReturnType<typeof valid>) => {
|
||||
o.links[2] = "https://other.invalid/ACORN/projects/project";
|
||||
},
|
||||
(o: ReturnType<typeof valid>) => {
|
||||
o.links[2] = "/OTHER/projects/project";
|
||||
},
|
||||
(o: ReturnType<typeof valid>) => {
|
||||
o.links[2] = "/ACORN/projects";
|
||||
},
|
||||
]) {
|
||||
const o = valid();
|
||||
change(o);
|
||||
expect(definition.grade(o).passed).toBe(false);
|
||||
}
|
||||
});
|
||||
it("missing evidence or a successful-sounding reply alone cannot pass", () => {
|
||||
expect(definition.grade(undefined).passed).toBe(false);
|
||||
expect(definition.grade({ reply: "Installed everything." }).passed).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it("missing evidence or a successful-sounding reply alone cannot pass", () => { expect(definition.grade(undefined).passed).toBe(false); expect(definition.grade({ reply: "Installed everything." }).passed).toBe(false); });
|
||||
});
|
||||
@@ -1,84 +1,391 @@
|
||||
import {
|
||||
prepareTeamConditions,
|
||||
observeTeamConditions,
|
||||
gradeTeamConditions,
|
||||
} from "./external-team-conditional.js";
|
||||
import { expect } from "@playwright/test";
|
||||
import type { Agent } from "../../../packages/shared/src/types/agent.js";
|
||||
import type { CatalogTeam, CatalogTeamImportPreviewResult, InstalledCatalogTeam } from "../../../packages/shared/src/types/teams-catalog.js";
|
||||
import type {
|
||||
CatalogTeam,
|
||||
CatalogTeamImportPreviewResult,
|
||||
InstalledCatalogTeam,
|
||||
} from "../../../packages/shared/src/types/teams-catalog.js";
|
||||
import { sendChatMessage } from "../chat-flow.js";
|
||||
import { definition, externalObservation, observeExternal, root, checks, verifyRenderedReply } from "./external-support.js";
|
||||
import {
|
||||
definition,
|
||||
externalObservation,
|
||||
observeExternal,
|
||||
root,
|
||||
checks,
|
||||
verifyRenderedReply,
|
||||
} from "./external-support.js";
|
||||
import { waitForReply } from "./identity-support.js";
|
||||
interface Resource { id: string; companyId: string; name?: string; status?: string; urlKey?: string }
|
||||
export interface TeamResourceSnapshot { agents: Agent[]; projects: Resource[]; routines: Resource[]; installed: InstalledCatalogTeam[] }
|
||||
export function catalogTeamProvenance(agent: Agent) { return (agent.metadata as { paperclip?: { catalogTeam?: { catalogId?: string; originHash?: string; agentSlug?: string } } } | null)?.paperclip?.catalogTeam; }
|
||||
export function catalogTeamMembers(snapshot: TeamResourceSnapshot, teamId: string) { return snapshot.agents.filter(agent => catalogTeamProvenance(agent)?.catalogId === teamId); }
|
||||
const ids = (rows: Array<{id:string}>) => rows.map(row => row.id).sort();
|
||||
function hasResourceLink(links: string[], origin: string, prefix: unknown, kind: "agents" | "projects", resource: { id: string; urlKey?: string }) {
|
||||
return links.some(href => { try { const url = new URL(href, origin), parts = url.pathname.split("/"); const index = parts.indexOf(kind), ref = parts[index + 1]; return url.origin === origin && index === 2
|
||||
&& decodeURIComponent(parts[1]!) === prefix && !!ref && [resource.id, resource.urlKey].includes(ref); } catch { return false; } });
|
||||
interface Resource {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name?: string;
|
||||
status?: string;
|
||||
urlKey?: string;
|
||||
}
|
||||
export async function observeTeamResources(ctx: import("../chat-catalog-contract.js").ChatCatalogContext): Promise<TeamResourceSnapshot> {
|
||||
const [agents, projects, routines, installed] = await Promise.all([ctx.input.api.get<Agent[]>(`${root(ctx)}/agents`), ctx.input.api.get<Resource[]>(`${root(ctx)}/projects`),
|
||||
ctx.input.api.get<Resource[]>(`${root(ctx)}/routines`), ctx.input.api.get<InstalledCatalogTeam[]>(`${root(ctx)}/teams/catalog/installed`)]);
|
||||
export interface TeamResourceSnapshot {
|
||||
agents: Agent[];
|
||||
projects: Resource[];
|
||||
routines: Resource[];
|
||||
installed: InstalledCatalogTeam[];
|
||||
}
|
||||
export function catalogTeamProvenance(agent: Agent) {
|
||||
return (
|
||||
agent.metadata as {
|
||||
paperclip?: {
|
||||
catalogTeam?: {
|
||||
catalogId?: string;
|
||||
originHash?: string;
|
||||
agentSlug?: string;
|
||||
};
|
||||
};
|
||||
} | null
|
||||
)?.paperclip?.catalogTeam;
|
||||
}
|
||||
export function catalogTeamMembers(
|
||||
snapshot: TeamResourceSnapshot,
|
||||
teamId: string,
|
||||
) {
|
||||
return snapshot.agents.filter(
|
||||
(agent) => catalogTeamProvenance(agent)?.catalogId === teamId,
|
||||
);
|
||||
}
|
||||
const ids = (rows: Array<{ id: string }>) => rows.map((row) => row.id).sort();
|
||||
function hasResourceLink(
|
||||
links: string[],
|
||||
origin: string,
|
||||
prefix: unknown,
|
||||
kind: "agents" | "projects",
|
||||
resource: { id: string; urlKey?: string },
|
||||
) {
|
||||
return links.some((href) => {
|
||||
try {
|
||||
const url = new URL(href, origin),
|
||||
parts = url.pathname.split("/");
|
||||
const index = parts.indexOf(kind),
|
||||
ref = parts[index + 1];
|
||||
return (
|
||||
url.origin === origin &&
|
||||
index === 2 &&
|
||||
decodeURIComponent(parts[1]!) === prefix &&
|
||||
!!ref &&
|
||||
[resource.id, resource.urlKey].includes(ref)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
export async function observeTeamResources(
|
||||
ctx: import("../chat-catalog-contract.js").ChatCatalogContext,
|
||||
): Promise<TeamResourceSnapshot> {
|
||||
const [agents, projects, routines, installed] = await Promise.all([
|
||||
ctx.input.api.get<Agent[]>(`${root(ctx)}/agents`),
|
||||
ctx.input.api.get<Resource[]>(`${root(ctx)}/projects`),
|
||||
ctx.input.api.get<Resource[]>(`${root(ctx)}/routines`),
|
||||
ctx.input.api.get<InstalledCatalogTeam[]>(
|
||||
`${root(ctx)}/teams/catalog/installed`,
|
||||
),
|
||||
]);
|
||||
return { agents, projects, routines, installed };
|
||||
}
|
||||
export function gradeTeamInstall(value: unknown) {
|
||||
const c = checks(), o = externalObservation(value); c.add("complete-team-install-evidence", !!o, "Accepted immutable preview, effective creator authority, attributed public installation and repeat reconciliation are required."); if (!o) return c.result();
|
||||
const team = o.state.team as CatalogTeam | undefined, first = o.state.first as TeamResourceSnapshot | undefined, after = o.state.after as TeamResourceSnapshot | undefined, before = o.state.before as TeamResourceSnapshot | undefined;
|
||||
c.add("accepted-supported-preview-and-authority", !!team && o.state.previewAccepted === true && o.state.previewErrorCount === 0 && o.state.actorCanCreateAgents === true && o.state.existingAccountReady === true,
|
||||
"The exact real catalog preview is accepted and the acting CEO has actual installation/resource authority with a ready existing runtime account.");
|
||||
const c = checks(),
|
||||
o = externalObservation(value);
|
||||
c.add(
|
||||
"complete-team-install-evidence",
|
||||
!!o,
|
||||
"Accepted immutable preview, effective creator authority, attributed public installation and repeat reconciliation are required.",
|
||||
);
|
||||
if (!o) return c.result();
|
||||
const team = o.state.team as CatalogTeam | undefined,
|
||||
first = o.state.first as TeamResourceSnapshot | undefined,
|
||||
after = o.state.after as TeamResourceSnapshot | undefined,
|
||||
before = o.state.before as TeamResourceSnapshot | undefined;
|
||||
c.add(
|
||||
"accepted-supported-preview-and-authority",
|
||||
!!team &&
|
||||
o.state.previewAccepted === true &&
|
||||
o.state.previewErrorCount === 0 &&
|
||||
o.state.actorCanCreateAgents === true &&
|
||||
o.state.existingAccountReady === true,
|
||||
"The exact real catalog preview is accepted and the acting CEO has actual installation/resource authority with a ready existing runtime account.",
|
||||
);
|
||||
const members = team && after ? catalogTeamMembers(after, team.id) : [];
|
||||
c.add("intended-agent-resources-once", !!team && !!after && !!before && members.length === team.agentSlugs.length && members.every(agent => agent.companyId === o.companyId
|
||||
&& catalogTeamProvenance(agent)?.originHash === team.contentHash) && new Set(members.map(agent => agent.urlKey)).size === members.length
|
||||
&& 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.");
|
||||
c.add("intended-project-and-routine-resources", !!team && !!after && !!before && after.projects.length - before.projects.length === team.counts.projects
|
||||
&& after.routines.length - before.routines.length === team.counts.routines && after.projects.every(project => project.companyId === o.companyId)
|
||||
&& after.installed.some(row => row.catalogId === team.id && row.agentCount === team.counts.agents && !row.outOfDate),
|
||||
"Actual project/routine deltas and the server-computed installed-team state agree with the preview in the active company.");
|
||||
c.add("actual-agent-installation-attribution", !!team && o.activity.some(row => row.action === "company.team_catalog_installed" && row.actorType === "agent" && row.agentId === o.leadId),
|
||||
"The requested installer operation is independently attributed to the actual model actor, rather than performed as an oracle setup side effect.");
|
||||
c.add("uncertain-repeat-reconciles-identities", !!team && !!first && !!after && first.installed.some(row => row.catalogId === team.id)
|
||||
&& JSON.stringify(ids(first.agents)) === JSON.stringify(ids(after.agents)) && JSON.stringify(ids(first.projects)) === JSON.stringify(ids(after.projects))
|
||||
&& JSON.stringify(ids(first.routines)) === JSON.stringify(ids(after.routines)) && o.state.repeatHumanQuery === o.state.originalHumanQuery,
|
||||
"The actual repeated canonical request preserves existing agent/project/routine identities rather than blindly installing renamed copies.");
|
||||
const second = o.phaseReplies.filter(reply => reply.runId !== o.state.initialRunId);
|
||||
c.add("phase-bound-reconciliation-reply", second.some(reply => o.runs.some(run => run.id === reply.runId && run.status === "succeeded"))
|
||||
&& /already|existing|installed|same|reconcil/i.test(second.map(reply => reply.body).join("\n")), "The repeat outcome belongs to its own real persisted model continuation.");
|
||||
c.add("actual-installed-resource-links", members.length > 0 && members.every(agent => hasResourceLink(o.links, o.origin, o.state.companyPrefix, "agents", agent))
|
||||
&& !!after && !!before && after.projects.filter(project => !before.projects.some(old => old.id === project.id)).every(project => hasResourceLink(o.links, o.origin, o.state.companyPrefix, "projects", project)),
|
||||
"The response links the actual installed members and project, not just the catalog or an intended future installation.");
|
||||
c.add(
|
||||
"intended-agent-resources-once",
|
||||
!!team &&
|
||||
!!after &&
|
||||
!!before &&
|
||||
members.length === team.agentSlugs.length &&
|
||||
members.every(
|
||||
(agent) =>
|
||||
agent.companyId === o.companyId &&
|
||||
catalogTeamProvenance(agent)?.originHash === team.contentHash,
|
||||
) &&
|
||||
new Set(members.map((agent) => agent.urlKey)).size === members.length &&
|
||||
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.",
|
||||
);
|
||||
c.add(
|
||||
"intended-project-and-routine-resources",
|
||||
!!team &&
|
||||
!!after &&
|
||||
!!before &&
|
||||
after.projects.length - before.projects.length === team.counts.projects &&
|
||||
after.routines.length - before.routines.length === team.counts.routines &&
|
||||
after.projects.every((project) => project.companyId === o.companyId) &&
|
||||
after.installed.some(
|
||||
(row) =>
|
||||
row.catalogId === team.id &&
|
||||
row.agentCount === team.counts.agents &&
|
||||
!row.outOfDate,
|
||||
),
|
||||
"Actual project/routine deltas and the server-computed installed-team state agree with the preview in the active company.",
|
||||
);
|
||||
c.add(
|
||||
"actual-agent-installation-attribution",
|
||||
!!team &&
|
||||
o.activity.some(
|
||||
(row) =>
|
||||
row.action === "company.team_catalog_installed" &&
|
||||
row.actorType === "agent" &&
|
||||
row.agentId === o.leadId,
|
||||
),
|
||||
"The requested installer operation is independently attributed to the actual model actor, rather than performed as an oracle setup side effect.",
|
||||
);
|
||||
c.add(
|
||||
"uncertain-repeat-reconciles-identities",
|
||||
!!team &&
|
||||
!!first &&
|
||||
!!after &&
|
||||
first.installed.some((row) => row.catalogId === team.id) &&
|
||||
JSON.stringify(ids(first.agents)) === JSON.stringify(ids(after.agents)) &&
|
||||
JSON.stringify(ids(first.projects)) ===
|
||||
JSON.stringify(ids(after.projects)) &&
|
||||
JSON.stringify(ids(first.routines)) ===
|
||||
JSON.stringify(ids(after.routines)) &&
|
||||
o.state.repeatHumanQuery === o.state.originalHumanQuery,
|
||||
"The actual repeated canonical request preserves existing agent/project/routine identities rather than blindly installing renamed copies.",
|
||||
);
|
||||
const second = o.phaseReplies.filter(
|
||||
(reply) => reply.runId !== o.state.initialRunId,
|
||||
);
|
||||
c.add(
|
||||
"phase-bound-reconciliation-reply",
|
||||
second.some((reply) =>
|
||||
o.runs.some(
|
||||
(run) => run.id === reply.runId && run.status === "succeeded",
|
||||
),
|
||||
) &&
|
||||
/already|existing|installed|same|reconcil/i.test(
|
||||
second.map((reply) => reply.body).join("\n"),
|
||||
),
|
||||
"The repeat outcome belongs to its own real persisted model continuation.",
|
||||
);
|
||||
c.add(
|
||||
"actual-installed-resource-links",
|
||||
members.length > 0 &&
|
||||
members.every((agent) =>
|
||||
hasResourceLink(
|
||||
o.links,
|
||||
o.origin,
|
||||
o.state.companyPrefix,
|
||||
"agents",
|
||||
agent,
|
||||
),
|
||||
) &&
|
||||
!!after &&
|
||||
!!before &&
|
||||
after.projects
|
||||
.filter(
|
||||
(project) => !before.projects.some((old) => old.id === project.id),
|
||||
)
|
||||
.every((project) =>
|
||||
hasResourceLink(
|
||||
o.links,
|
||||
o.origin,
|
||||
o.state.companyPrefix,
|
||||
"projects",
|
||||
project,
|
||||
),
|
||||
),
|
||||
"The response links the actual installed members and project, not just the catalog or an intended future installation.",
|
||||
);
|
||||
for (const check of gradeTeamConditions(o).checks)
|
||||
c.add(check.id, check.passed, check.detail);
|
||||
return c.result();
|
||||
}
|
||||
export const teamInstallExternalCases = [definition("teams.install-team", async ctx => {
|
||||
const teams = await ctx.input.api.get<CatalogTeam[]>("/api/teams/catalog"); const team = teams.find(row => row.slug === "product-engineering" && row.compatibility === "compatible");
|
||||
if (!team) throw new Error("Setup blocker: real compatible Product Engineering team catalog package is unavailable");
|
||||
await ctx.input.api.patch(`/api/agents/${ctx.input.fixtures.agent.id}/permissions`, { canCreateAgents: true });
|
||||
await ctx.input.api.patch(root(ctx), { requireBoardApprovalForNewAgents: false });
|
||||
const actor = await ctx.input.api.get<Agent>(`/api/agents/${ctx.input.fixtures.agent.id}`);
|
||||
if (actor.lifecycleState !== "ready" || actor.permissions?.canCreateAgents !== true) throw new Error("Setup blocker: actual selected installer actor lacks a ready account or creation authority");
|
||||
const preview = await ctx.input.api.post<CatalogTeamImportPreviewResult>(`${root(ctx)}/teams/catalog/${encodeURIComponent(team.id)}/preview`, { targetManagerAgentId: actor.id });
|
||||
if (preview.errors.length || preview.portabilityPreview.errors.length) throw new Error("Setup blocker: accepted team fixture preview has real preparation conflicts");
|
||||
const before = await observeTeamResources(ctx); if (catalogTeamMembers(before, team.id).length) throw new Error("Setup blocker: the selected team is already installed in this disposable company");
|
||||
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}.` });
|
||||
return { team, before, companyPrefix: ctx.input.fixtures.company.issuePrefix, previewAccepted: true, previewErrorCount: preview.errors.length + preview.portabilityPreview.errors.length,
|
||||
actorCanCreateAgents: actor.permissions?.canCreateAgents === true, existingAccountReady: actor.lifecycleState === "ready", originalHumanQuery: ctx.scenario.query };
|
||||
}, gradeTeamInstall, {
|
||||
expectedRunCount: 2, expectedHumanMessageCount: 2, attemptTimeoutMs: 480_000,
|
||||
coverageGaps: ["installer-conflict-and-partial-failure-branches"],
|
||||
sourceFiles: ["chat-catalog-domains/external-team-install.ts", "chat-catalog-domains/external-support.ts", "chat-catalog-domains/internal-support.ts", "chat-catalog-domains/identity-support.ts", "chat-flow.ts", "chat-scenarios/teams.yaml"],
|
||||
async act(ctx, state) {
|
||||
await waitForReply(ctx); state.first = await observeTeamResources(ctx); state.initialRunId = (await ctx.allRuns())[0]?.id;
|
||||
state.repeatHumanQuery = ctx.scenario.query; await sendChatMessage(ctx.input.page, ctx.scenario.query);
|
||||
},
|
||||
async observe(ctx, state) { state.after = await observeTeamResources(ctx); return observeExternal(ctx, state); },
|
||||
async verifyResult(ctx, state, value) {
|
||||
await verifyRenderedReply(ctx, value, String(state.initialRunId)); const team = state.team as CatalogTeam, after = state.after as TeamResourceSnapshot, o = externalObservation(value)!;
|
||||
const source = ctx.input.page.url();
|
||||
for (const member of catalogTeamMembers(after, team.id)) { const href = o.links.find(link => hasResourceLink([link], o.origin, state.companyPrefix, "agents", member)); if (!href) throw new Error("Actual installed member link missing"); await ctx.input.page.goto(new URL(href, o.origin).href, { waitUntil: "domcontentloaded" }); await expect(ctx.input.page.getByRole("main")).toContainText(member.name); }
|
||||
for (const project of after.projects.filter(row => !(state.before as TeamResourceSnapshot).projects.some(old => old.id === row.id))) { const href = o.links.find(link => hasResourceLink([link], o.origin, state.companyPrefix, "projects", project)); if (!href) throw new Error("Actual installed project link missing"); await ctx.input.page.goto(new URL(href, o.origin).href, { waitUntil: "domcontentloaded" }); await expect(ctx.input.page.getByRole("main")).toContainText(project.name ?? ""); }
|
||||
await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" });
|
||||
},
|
||||
async cleanup(ctx, state) {
|
||||
const before = state.before as TeamResourceSnapshot | undefined; if (!before) return;
|
||||
for (const routine of await ctx.input.api.get<Resource[]>(`${root(ctx)}/routines`)) if (!before.routines.some(row => row.id === routine.id)) await ctx.input.api.patch(`/api/routines/${routine.id}`, { status: "paused" });
|
||||
// Complete disposable-company teardown removes imported graph/provenance.
|
||||
},
|
||||
})];
|
||||
export const teamInstallExternalCases = [
|
||||
definition(
|
||||
"teams.install-team",
|
||||
async (ctx) => {
|
||||
const teams =
|
||||
await ctx.input.api.get<CatalogTeam[]>("/api/teams/catalog");
|
||||
const team = teams.find(
|
||||
(row) =>
|
||||
row.slug === "product-engineering" &&
|
||||
row.compatibility === "compatible",
|
||||
);
|
||||
if (!team)
|
||||
throw new Error(
|
||||
"Setup blocker: real compatible Product Engineering team catalog package is unavailable",
|
||||
);
|
||||
await ctx.input.api.patch(
|
||||
`/api/agents/${ctx.input.fixtures.agent.id}/permissions`,
|
||||
{ canCreateAgents: true },
|
||||
);
|
||||
await ctx.input.api.patch(root(ctx), {
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
const actor = await ctx.input.api.get<Agent>(
|
||||
`/api/agents/${ctx.input.fixtures.agent.id}`,
|
||||
);
|
||||
if (
|
||||
actor.lifecycleState !== "ready" ||
|
||||
actor.permissions?.canCreateAgents !== true
|
||||
)
|
||||
throw new Error(
|
||||
"Setup blocker: actual selected installer actor lacks a ready account or creation authority",
|
||||
);
|
||||
const conditional = await prepareTeamConditions(ctx, team);
|
||||
const preview = conditional.conditionalPreview;
|
||||
if (preview.errors.length || preview.portabilityPreview.errors.length)
|
||||
throw new Error(
|
||||
"Setup blocker: accepted team fixture preview has real preparation conflicts",
|
||||
);
|
||||
const before = await observeTeamResources(ctx);
|
||||
if (catalogTeamMembers(before, team.id).length)
|
||||
throw new Error(
|
||||
"Setup blocker: the selected team is already installed in this disposable company",
|
||||
);
|
||||
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}.`,
|
||||
});
|
||||
return {
|
||||
...conditional,
|
||||
team,
|
||||
before,
|
||||
companyPrefix: ctx.input.fixtures.company.issuePrefix,
|
||||
previewAccepted: true,
|
||||
previewErrorCount:
|
||||
preview.errors.length + preview.portabilityPreview.errors.length,
|
||||
actorCanCreateAgents: actor.permissions?.canCreateAgents === true,
|
||||
existingAccountReady: actor.lifecycleState === "ready",
|
||||
originalHumanQuery: ctx.scenario.query,
|
||||
};
|
||||
},
|
||||
gradeTeamInstall,
|
||||
{
|
||||
authentication: "human-session",
|
||||
expectedRunCount: 2,
|
||||
expectedHumanMessageCount: 2,
|
||||
attemptTimeoutMs: 480_000,
|
||||
sourceFiles: [
|
||||
"chat-catalog-domains/external-team-install.ts",
|
||||
"chat-catalog-domains/external-team-conditional.ts",
|
||||
"../../server/src/services/teams-catalog.ts",
|
||||
"../../server/src/services/company-skills.ts",
|
||||
"../../server/src/services/company-portability.ts",
|
||||
"../../packages/teams-catalog/generated/catalog.json",
|
||||
"chat-catalog-domains/external-support.ts",
|
||||
"chat-catalog-domains/internal-support.ts",
|
||||
"chat-catalog-domains/identity-support.ts",
|
||||
"chat-flow.ts",
|
||||
"chat-scenarios/teams.yaml",
|
||||
],
|
||||
async act(ctx, state) {
|
||||
await waitForReply(ctx);
|
||||
state.first = await observeTeamResources(ctx);
|
||||
state.initialRunId = (await ctx.allRuns())[0]?.id;
|
||||
state.firstConditional = await observeTeamConditions(ctx, state);
|
||||
const firstObservation = await observeExternal(ctx, state);
|
||||
// A later correct repeat cannot conceal an unreported initial partial.
|
||||
const firstGrade = gradeTeamConditions(firstObservation, "initial");
|
||||
if (!firstGrade.passed)
|
||||
throw new Error(
|
||||
"Actual initial installer did not truthfully preserve/report its genuine conflict and partial outcome before repeat",
|
||||
);
|
||||
await ctx.input.evidence("chat-catalog-team-initial-conditional.json", {
|
||||
initialRunId: state.initialRunId,
|
||||
condition: state.firstConditional,
|
||||
previewCollision: state.renamePlan,
|
||||
});
|
||||
state.repeatHumanQuery = ctx.scenario.query;
|
||||
await sendChatMessage(ctx.input.page, ctx.scenario.query);
|
||||
},
|
||||
async observe(ctx, state) {
|
||||
state.finalConditional = await observeTeamConditions(ctx, state);
|
||||
state.after = await observeTeamResources(ctx);
|
||||
return observeExternal(ctx, state);
|
||||
},
|
||||
async verifyResult(ctx, state, value) {
|
||||
await verifyRenderedReply(ctx, value, String(state.initialRunId));
|
||||
const team = state.team as CatalogTeam,
|
||||
after = state.after as TeamResourceSnapshot,
|
||||
o = externalObservation(value)!;
|
||||
const source = ctx.input.page.url();
|
||||
for (const member of catalogTeamMembers(after, team.id)) {
|
||||
const href = o.links.find((link) =>
|
||||
hasResourceLink(
|
||||
[link],
|
||||
o.origin,
|
||||
state.companyPrefix,
|
||||
"agents",
|
||||
member,
|
||||
),
|
||||
);
|
||||
if (!href) throw new Error("Actual installed member link missing");
|
||||
await ctx.input.page.goto(new URL(href, o.origin).href, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await expect(ctx.input.page.getByRole("main")).toContainText(
|
||||
member.name,
|
||||
);
|
||||
}
|
||||
for (const project of after.projects.filter(
|
||||
(row) =>
|
||||
!(state.before as TeamResourceSnapshot).projects.some(
|
||||
(old) => old.id === row.id,
|
||||
),
|
||||
)) {
|
||||
const href = o.links.find((link) =>
|
||||
hasResourceLink(
|
||||
[link],
|
||||
o.origin,
|
||||
state.companyPrefix,
|
||||
"projects",
|
||||
project,
|
||||
),
|
||||
);
|
||||
if (!href) throw new Error("Actual installed project link missing");
|
||||
await ctx.input.page.goto(new URL(href, o.origin).href, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await expect(ctx.input.page.getByRole("main")).toContainText(
|
||||
project.name ?? "",
|
||||
);
|
||||
}
|
||||
await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" });
|
||||
},
|
||||
async cleanup(ctx, state) {
|
||||
const before = state.before as TeamResourceSnapshot | undefined;
|
||||
if (!before) return;
|
||||
for (const routine of await ctx.input.api.get<Resource[]>(
|
||||
`${root(ctx)}/routines`,
|
||||
))
|
||||
if (!before.routines.some((row) => row.id === routine.id))
|
||||
await ctx.input.api.patch(`/api/routines/${routine.id}`, {
|
||||
status: "paused",
|
||||
});
|
||||
// Complete disposable-company teardown removes imported graph/provenance.
|
||||
},
|
||||
},
|
||||
),
|
||||
];
|
||||
Reference in new issue
Block a user