diff --git a/tests/runner-e2e/chat-catalog-domains/external-team-conditional.test.ts b/tests/runner-e2e/chat-catalog-domains/external-team-conditional.test.ts index af125dd9118..874de15fa6d 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-team-conditional.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-team-conditional.test.ts @@ -189,3 +189,16 @@ it("requires the selected catalog version for a pinned binding and rechecks bind (o.state.finalConditional as typeof condition).recovery.agents[0]!.adapterConfig.paperclipSkillSync.desiredSkills = []; expect(gradeTeamConditions(o).passed).toBe(false); }); + +it("rejects a final claim that the independently recovered required skill is still missing", () => { + const { o } = recovered(); + o.phaseReplies[1]!.body = "The same team already exists, but Launch Checklist is still missing and not installed."; + expect(gradeTeamConditions(o, "initial").passed).toBe(true); + expect(gradeTeamConditions(o).passed).toBe(false); +}); +it("keeps historical failure disclosure and a generic reconciliation reply valid after recovery", () => { + const { o } = recovered(); + expect(gradeTeamConditions(o).passed).toBe(true); + o.phaseReplies[1]!.body = "Launch Checklist was blocked before; it is now fixed. Launch Checklist is not missing. Launch Checklist is installed despite a blocked local slug. The same team already exists."; + expect(gradeTeamConditions(o).passed).toBe(true); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/external-team-conditional.ts b/tests/runner-e2e/chat-catalog-domains/external-team-conditional.ts index 93579071f4c..ae369304b3f 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-team-conditional.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-team-conditional.ts @@ -179,6 +179,15 @@ function truthfulTeamRecovery(reply: string, skill: Pick): boolean { + const names = [skill.slug, skill.slug.replaceAll("-", " "), skill.name].map(name => name.toLowerCase()); + return reply.split(/[.!?;\n]+/).some(clause => names.some(name => clause.toLowerCase().includes(name)) + && [...clause.matchAll(/\b(?:not installed|uninstalled|missing|unavailable|blocked)\b/gi)].some(match => { + const before = clause.slice(0, match.index); + return !/\b(?:was|were|had been)\s+(?:still\s+)?$/i.test(before) + && (match[0].toLowerCase() === "not installed" || /\b(?:is|are|remains?|still)\s+(?:currently\s+)?$/i.test(before)); + })); +} export async function observeTeamConditions( ctx: ChatCatalogContext, state: Record, @@ -322,7 +331,7 @@ export function gradeTeamConditions( identifiesConflict && /renam|existing|conflict|collision|preserv/i.test(first) && (resolved(earlier) ? truthfulTeamRecovery(first, target) : truthfulTeamPartial(first, target)) && - (phase === "initial" || (resolved(later) || truthfulTeamPartial(repeat, target))), + (phase === "initial" || (resolved(later) ? !claimsRecoveredSkillMissing(repeat, target) : truthfulTeamPartial(repeat, target))), "The first reply reports the actual renamed role and either the unresolved required skill or its independently verified recovery. A remaining failure must be reported in both phases; a later reply cannot rescue false initial readiness.", ); return c.result();