test(evals): grade edit-profile from the profile-editor link and saved identity

The account popover check failed in strict mode because the profile page now
has its own display-name heading; scope it to the popover. Accept 401 as well
as 403 for the agent's denied profile write (a non-session credential is
unauthenticated on the human-only route). Replace the reply keyword and
negative wording regexes with a required rendered link to the person's
profile editor, which the person follows to save the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-07 20:55:36 -06:00
1 parent f0ddccff71
commit 384fd82892
3 files changed
+45 -8

No files matched your search

@@ -3608,7 +3608,7 @@
"chat-catalog-domains/internal-support.ts",
"chat-scenarios/navigation-preferences.yaml"
],
"oracleVersion": "paperclip.chat-catalog.identity.v1",
"oracleVersion": "paperclip.chat-catalog.identity-profile.v2",
"expectedRunCount": 1,
"turnTimeoutMs": 120000,
"attemptTimeoutMs": 300000,
@@ -8,8 +8,10 @@ function valid(id: string): IdentityObservation {
companies: [], agents: [{ id: "ceo", companyId: "acorn", name: "CEO" }], interactions: [], tasks: [task], projects: [], goals: [], comments: [], activity: [], documents: [], revisions: [], links: [], runs: [], reply: "", humanQueries: [], state: { task: { ...task } } };
if (id === "navigation-preferences.edit-profile") {
Object.assign(o.state, { authenticated: true, personId: "alex", beforeHumanName: "Alex Former", agentWriteStatus: 403, afterUserId: "alex", afterName: "Alex Chen",
profileAfterReload: "Alex Chen", accountFooterVisible: true, companyName: "Acorn", agentName: "CEO", observerNameBefore: "Riley", observerNameAfter: "Riley", observerId: "riley" });
o.reply = "Please open your Profile settings and change the display name there.";
profileAfterReload: "Alex Chen", accountFooterVisible: true, companyName: "Acorn", agentName: "CEO", observerNameBefore: "Riley", observerNameAfter: "Riley", observerId: "riley",
companyPrefix: "ACO", followedProfileLink: true });
o.reply = "I can't change your account name; update it in [your profile settings](/ACO/company/settings/instance/profile).";
o.links = ["/ACO/company/settings/instance/profile"];
} else if (id === "companies.switch-company") {
const foreign = { ...task, id: "birch-task", companyId: "birch" };
Object.assign(o.state, { otherCompany: { id: "birch" }, foreignTask: foreign, foreignAfter: { ...foreign }, foreignMarker: "Hidden Birch account details",
@@ -36,6 +38,23 @@ describe("personal identity and company lifecycle oracles", () => {
expect(definition.grade(wrong).passed).toBe(false); expect(definition.grade(undefined).passed).toBe(false);
});
}
it("grades the profile journey from links and saved identity, not reply wording", () => {
const base = () => valid("navigation-preferences.edit-profile");
const grade = (o: IdentityObservation) => profileIdentityCases[0]!.grade(o).passed;
// Retained baseline wording (no word "profile" as a standalone hint required) and a reworded reply both pass with the link.
for (const reply of ["Your account currently shows Alex Former. Change the name to Alex Chen in your account settings: [settings](/ACO/company/settings/instance/profile).",
"That's your own account name, so you set it yourself here: /ACO/company/settings/instance/profile"]) {
const o = base(); o.reply = reply; expect(grade(o)).toBe(true);
}
const unprefixed = base(); unprefixed.links = ["/company/settings/instance/profile"]; expect(grade(unprefixed)).toBe(true);
const status401 = base(); status401.state.agentWriteStatus = 401; expect(grade(status401)).toBe(true);
const noLink = base(); noLink.links = []; expect(grade(noLink)).toBe(false);
const wrongLink = base(); wrongLink.links = ["/ACO/agents/ceo"]; expect(grade(wrongLink)).toBe(false);
const foreign = base(); foreign.links = ["https://evil.invalid/ACO/company/settings/instance/profile"]; expect(grade(foreign)).toBe(false);
const notFollowed = base(); notFollowed.state.followedProfileLink = false; expect(grade(notFollowed)).toBe(false);
const agentWrote = base(); agentWrote.state.beforeHumanName = "Alex Chen"; expect(grade(agentWrote)).toBe(false);
const agentAllowed = base(); agentAllowed.state.agentWriteStatus = 200; expect(grade(agentAllowed)).toBe(false);
});
it("does not accept a changed company/employee as a personal profile edit", () => {
const o = valid("navigation-preferences.edit-profile"); o.agents[0]!.name = "Alex Chen";
expect(profileIdentityCases[0]!.grade(o).passed).toBe(false);
@@ -8,13 +8,24 @@ import { identityDefinition, identityObservation, observeIdentity, probeAgentPer
interface Session { user: { id: string; name: string; image?: string | null } }
type Person = Awaited<ReturnType<typeof createAuthenticatedPerson>>;
const observers = new WeakMap<ChatCatalogContext, Person>();
/** Unauthenticated (non-session credential) and forbidden are both denials of the human-only profile route. */
const AGENT_DENIED: readonly number[] = [401, 403];
const PROFILE_EDITOR_PATH = "/company/settings/instance/profile";
export function isProfileEditorLink(href: string, origin: string, prefix: string): boolean {
try {
const url = new URL(href, origin);
return url.origin === new URL(origin).origin && [PROFILE_EDITOR_PATH, `/${prefix}${PROFILE_EDITOR_PATH}`].includes(url.pathname.replace(/\/+$/, ""));
} catch { return false; }
}
export function gradeProfile(value: unknown) {
const c = checks(); const o = identityObservation(value);
c.add("real-person-profile-evidence", !!o && o.state.authenticated === true && typeof o.state.personId === "string" && o.state.personId !== "local-board",
"The result belongs to a real authenticated person and an independently enrolled second user.");
if (!o) return c.result();
c.add("agent-guides-human-editor", o.state.beforeHumanName === "Alex Former" && o.state.agentWriteStatus === 403 && /\bprofile\b/i.test(o.reply)
&& !/\bI(?:'ve| have)?\s+(?:changed|updated|renamed)\b/i.test(o.reply), "The agent identifies the personal editor, remains unable to impersonate its user, and leaves the old name until the human acts.");
c.add("agent-cannot-impersonate", o.state.beforeHumanName === "Alex Former" && AGENT_DENIED.includes(o.state.agentWriteStatus as number),
"An agent credential is denied the personal profile write, and the old name remains after the reply until the human acts.");
c.add("agent-links-profile-editor", typeof o.state.companyPrefix === "string" && o.links.some(href => isProfileEditorLink(href, o.origin, o.state.companyPrefix as string))
&& o.state.followedProfileLink === true, "The reply links the person's own profile editor, and the person reaches the editor by following that rendered link.");
c.add("same-user-profile-persisted", o.state.afterUserId === o.state.personId && o.state.afterName === "Alex Chen" && o.state.profileAfterReload === "Alex Chen"
&& o.state.accountFooterVisible === true, "The visible personal form saves Alex Chen on the same human ID and the account footer updates after reload.");
c.add("other-identities-preserved", o.company.name === o.state.companyName && o.agents.find(row => row.id === o.leadId)?.name === o.state.agentName
@@ -33,17 +44,23 @@ export const profileIdentityCases: readonly ChatCatalogCase[] = [identityDefinit
await updateOwnProfile(ctx, "Alex Former");
await ctx.input.page.reload({ waitUntil: "domcontentloaded" });
const company = await ctx.input.api.get<{ name: string }>(`/api/companies/${ctx.input.fixtures.company.id}`);
return { authenticated: true, personId: session.user.id, originalName: session.user.name, observerId: observer.id,
return { authenticated: true, companyPrefix: ctx.input.fixtures.company.issuePrefix, personId: session.user.id, originalName: session.user.name, observerId: observer.id,
observerNameBefore: observer.name, companyName: company.name, agentName: ctx.input.fixtures.agent.name };
} catch (error) { await observer.dispose(); observers.delete(ctx); throw error; }
}, gradeProfile, {
authentication: "human-session",
oracleVersion: "paperclip.chat-catalog.identity-profile.v2",
sourceFiles: ["chat-catalog-domains/identity-profile.ts", "chat-catalog-domains/identity-auth.ts", "chat-catalog-domains/identity-private-browser.ts", "chat-catalog-domains/identity-private-browser-worker.ts", "chat-catalog-domains/identity-wizard.ts", "chat-catalog-domains/identity-support.ts", "chat-catalog-domains/internal-support.ts", "chat-scenarios/navigation-preferences.yaml"],
async act(ctx, state) {
await waitForReply(ctx, chatCatalogOriginalPhaseDeadline(ctx)); state.beforeHumanName = (await ctx.input.api.get<Session>("/api/auth/get-session")).user.name;
state.agentWriteStatus = await probeAgentPermission(ctx, "/api/auth/profile", "PATCH", { name: "Alex Chen" });
const source = ctx.input.page.url();
await ctx.input.page.goto(`/${ctx.input.fixtures.company.issuePrefix}/company/settings/instance/profile`, { waitUntil: "domcontentloaded" });
// The person follows the agent's rendered link when there is one; otherwise the journey still saves the name so the remaining checks report.
const rendered = ctx.input.page.locator(`[id^="comment-"] a[href$="${PROFILE_EDITOR_PATH}"]`).first();
state.followedProfileLink = await rendered.count() > 0;
if (state.followedProfileLink) await rendered.click();
else await ctx.input.page.goto(`/${ctx.input.fixtures.company.issuePrefix}${PROFILE_EDITOR_PATH}`, { waitUntil: "domcontentloaded" });
await expect(ctx.input.page).toHaveURL(new RegExp(`${PROFILE_EDITOR_PATH}$`));
await expect(ctx.input.page.getByRole("heading", { name: "Profile", exact: true })).toBeVisible();
await ctx.input.page.getByLabel("Display name", { exact: true }).fill("Alex Chen");
await ctx.input.page.getByRole("button", { name: "Save profile", exact: true }).click();
@@ -54,7 +71,8 @@ export const profileIdentityCases: readonly ChatCatalogCase[] = [identityDefinit
await expect(account).toBeVisible();
state.accountFooterVisible = await account.getByText("Alex Chen", { exact: true }).count() === 1;
await account.click();
await expect(ctx.input.page.getByRole("heading", { name: "Alex Chen", exact: true })).toBeVisible();
// The profile page has its own "Alex Chen" heading; check the one inside the opened account popover.
await expect(ctx.input.page.locator("[data-radix-popper-content-wrapper]").getByRole("heading", { name: "Alex Chen", exact: true })).toBeVisible();
await ctx.input.page.keyboard.press("Escape");
await ctx.input.evidence("identity-personal-profile-browser.json", { userId: state.personId, savedName: state.profileAfterReload, accountFooterVisible: state.accountFooterVisible });
await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" });