diff --git a/packages/lina-core/src/agents/persona-schema.ts b/packages/lina-core/src/agents/persona-schema.ts index df9bcec..e5781a9 100644 --- a/packages/lina-core/src/agents/persona-schema.ts +++ b/packages/lina-core/src/agents/persona-schema.ts @@ -283,7 +283,11 @@ function profileFor( if (identity === null) return null; for (const profile of parseIdentityPolicy(identity).profiles) if (profile.agentId === agentId) return profile; - return null; + // A supplied snapshot is a claim about every participant, so a missing target + // is bad data, not an absent policy. Collapsing it to null would unlock every + // axis and produce the same digest as identity: null. The LIFE paths already + // reject this: world/views.ts, world/growth.ts, world/autonomy-rules.ts. + throw Error("Missing persona identity policy"); } function lockedFor( diff --git a/packages/lina-core/test/persona-schema.test.ts b/packages/lina-core/test/persona-schema.test.ts index e591a4e..63a4fbd 100644 --- a/packages/lina-core/test/persona-schema.test.ts +++ b/packages/lina-core/test/persona-schema.test.ts @@ -256,27 +256,61 @@ test("derivation digest is idempotent and ignores v2-only identity fields", () = const fromV2 = derive(identityV2); expect(fromV1.digest).toBe(fromV2.digest); expect(fromV1.digest).toBe(first.digest); +}); - const missing = personaSchemaFromLifeDefinition({ - agentId: "lina", - revision: 1, - definition, - identity: { - version: 2 as const, - profiles: [ - { - ...lockProfile, - agentId: "mira", - evolution: "manual", - personalBehavior: null, - sourceStamp: null, - }, - ], - }, +test("supplied identity without the target agent is rejected", () => { + const before = derive(); + const otherAgent = { + ...lockProfile, + agentId: "mira", + evolution: "manual" as const, + personalBehavior: null, + sourceStamp: null, + }; + for (const profiles of [[], [otherAgent]]) { + expect(() => + personaSchemaFromLifeDefinition({ + agentId: "lina", + revision: 1, + definition, + identity: { version: 2 as const, profiles }, + }), + ).toThrow("Missing persona identity policy"); + } + expect(derive()).toEqual(before); +}); + +test("absent policy, manual lock and selective lock stay separate cases", () => { + const absent = derive(null); + expect(absent.sourceIdentity).toBeNull(); + expect(absent.dimensions.every((row) => row.locked === false)).toBe(true); + + const selective = derive(); + expect(selective.sourceIdentity).toEqual({ profileRevision: 4 }); + expect(selective.dimensions.map((row) => [row.id, row.locked])).toEqual([ + ["curiosity", false], + ["warmth", true], + ["tea", false], + ["trust", false], + ]); + expect(selective.digest).not.toBe(absent.digest); + + const manual = derive({ + version: 2 as const, + profiles: [ + { + ...lockProfile, + evolution: "manual" as const, + lockedTraitIds: [], + personalBehavior: null, + sourceStamp: null, + }, + ], }); - expect(missing.sourceIdentity).toBeNull(); - expect(missing.dimensions.every((row) => row.locked === false)).toBe(true); - expect(missing.digest).toBe(unlocked.digest); + expect(manual.dimensions.every((row) => row.locked === true)).toBe(true); + expect(new Set([absent.digest, selective.digest, manual.digest]).size).toBe( + 3, + ); }); test.each([identityV1, identityV2])(