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 60455bbd498..9da218b475d 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 @@ -506,3 +506,31 @@ it("accepts a current exact-skill unresolved installation disclosure without inv actual + " The doc-maintenance skill is now installed.", ]) expect(truthfulTeamPartial(contradiction, target, true)).toBe(false); }); + +it("accepts a current named attachment-state caveat while retaining missing-state contradictions", () => { + const target = { slug: "doc-maintenance", name: "doc-maintenance" }; + const actual = "One remaining caveat: the `doc-maintenance` skill is still unattached because your company already has a different skill using that slug; I preserved the existing one."; + expect(truthfulTeamPartial(actual, target, true)).toBe(true); + expect(truthfulTeamPartial("The doc-maintenance skill remains not attached due to the occupied local slug.", target)).toBe(true); + const o = valid(); + o.state.blockedSkill = target; + o.phaseReplies[0]!.body = "The existing CTO was preserved and the new role renamed CTO 2. The doc-maintenance skill could not attach because the company already has a different skill using that slug."; + o.phaseReplies[1]!.body = actual; + expect(gradeTeamConditions(o).passed).toBe(true); + for (const invalid of [ + "A different skill is still unattached because its slug conflicts. The team is installed.", + "Earlier, the doc-maintenance skill was unattached because its slug was occupied.", + actual + " All required skills are installed.", + actual + " No unresolved doc-maintenance conflicts remain.", + actual + " The doc-maintenance skill is now installed.", + actual + " The doc-maintenance skill is now attached.", + ]) expect(truthfulTeamPartial(invalid, target, true), invalid).toBe(false); +}); + +it("rejects a current unattached claim when independent saved recovery is proven", () => { + for (const phase of [0, 1]) { + const { o } = recovered(); + o.phaseReplies[phase]!.body = "The existing CTO was preserved and the new role renamed CTO 2. Launch Checklist is still unattached because its local slug conflicts."; + expect(gradeTeamConditions(o, phase === 0 ? "initial" : "complete").passed).toBe(false); + } +}); 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 28bc7b486f8..e2f4eba752e 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-team-conditional.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-team-conditional.ts @@ -219,7 +219,7 @@ function historicalTeamSkillClause(clause: string, skill: Pick): boolean { if (historicalTeamSkillClause(clause, skill)) return false; const subject = `(?:${teamSkillSubject(skill)})`, modifiers = `(?:(?:now|already|currently|successfully|fully|completely)\\s+){0,2}`; - const claims = new RegExp(`\\b${subject}\\s+(?:skill\\s+)?(?:(?:is|are|was|were|(?:has|have)\\s+${modifiers}been)\\s+)?${modifiers}(?:installed|available|resolved|recovered)\\b|\\b(?:installed|resolved|recovered|fixed|cleared)\\s+(?:the\\s+)?${subject}\\b|\\b(?:${subject}\\s+(?:skill\\s+)?(?:installation|install)|(?:the\\s+)?(?:installation|install)\\s+of\\s+${subject})\\s+(?:has\\s+)?${modifiers}(?:succeeded|completed)\\b`, "gi"); + const claims = new RegExp(`\\b${subject}\\s+(?:skill\\s+)?(?:(?:is|are|was|were|(?:has|have)\\s+${modifiers}been)\\s+)?${modifiers}(?:installed|attached|available|resolved|recovered)\\b|\\b(?:installed|attached|resolved|recovered|fixed|cleared)\\s+(?:the\\s+)?${subject}\\b|\\b(?:${subject}\\s+(?:skill\\s+)?(?:installation|install)|(?:the\\s+)?(?:installation|install)\\s+of\\s+${subject})\\s+(?:has\\s+)?${modifiers}(?:succeeded|completed)\\b`, "gi"); return [...clause.matchAll(claims)].some(match => !negatedTeamRecovery(clause.slice(0, match.index))); } function truthfulTeamRecovery(reply: string, skill: Pick): boolean { @@ -227,8 +227,8 @@ function truthfulTeamRecovery(reply: string, skill: Pick): boolean { const subject = `(?:${teamSkillSubject(skill)})`, modifiers = `(?:(?:yet|now|currently|still|successfully|fully|completely)\\s+){0,2}`; - const denied = `(?:(?:(?:is|are|was|were|has|have|had|could|can)\\s+(?:(?:now|currently|still)\\s+)?not|(?:isn't|aren't|wasn't|weren't|hasn't|haven't|couldn't|can't))\\s+${modifiers}(?:(?:be|been)\\s+)?${modifiers}(?:installed|available|usable))`; - const current = new RegExp(`\\b${subject}\\s+(?:skill\\s+)?(?:${denied}|(?:(?:is|are|remains?|still)\\s+)?(?:(?:now|currently|still)\\s+)?(?:not\\s+${modifiers}(?:installed|available|usable)|uninstalled|missing|unavailable|blocked)|(?:installation|install)\\s+(?:has\\s+)?failed)\\b`, "i"); + const denied = `(?:(?:(?:is|are|was|were|has|have|had|could|can)\\s+(?:(?:now|currently|still)\\s+)?not|(?:isn't|aren't|wasn't|weren't|hasn't|haven't|couldn't|can't))\\s+${modifiers}(?:(?:be|been)\\s+)?${modifiers}(?:installed|attached|available|usable))`; + const current = new RegExp(`\\b${subject}\\s+(?:skill\\s+)?(?:${denied}|(?:(?:is|are|remains?|still)\\s+)?(?:(?:now|currently|still)\\s+)?(?:not\\s+${modifiers}(?:installed|attached|available|usable)|uninstalled|unattached|missing|unavailable|blocked)|(?:installation|install)\\s+(?:has\\s+)?failed)\\b`, "i"); const deniedObject = new RegExp(`\\binstalled\\s+(?:the\\s+)?${subject}\\b`, "gi"); return namedTeamSkillClauses(reply, skill, true).some(clause => !historicalTeamSkillClause(clause, skill) && (current.test(clause) || [...clause.matchAll(deniedObject)].some(match => negatedTeamRecovery(clause.slice(0, match.index))))); @@ -299,7 +299,10 @@ export function truthfulTeamPartial( return !!claim && !historicalTeamSkillClause(clause, skill) && !negatedTeamRecovery(clause.slice(0, claim.index)) && /\b(?:installation|install|conflict|collision|slug)\b/i.test(clause); }); - const reported = named && (occupied || contextual || unresolved || clauses.some(clause => !historicalTeamSkillClause(clause, skill) + const unattached = clauses.some(clause => !historicalTeamSkillClause(clause, skill) + && new RegExp(`\\b${teamSkillSubject(skill)}\\s+(?:skill\\s+)?(?:(?:is|remains?)\\s+)?(?:(?:currently|still|now)\\s+)?(?:unattached|not\\s+attached)\\b`, "i").test(clause) + && /\b(?:conflict|collision|slug)\b/i.test(clause)); + const reported = named && (occupied || contextual || unresolved || unattached || clauses.some(clause => !historicalTeamSkillClause(clause, skill) && /could not|couldn't|cannot|failed|conflict|blocked|not installed|missing|unavailable|unable|partial/i.test(clause))); const falseFull = reply.split(/[.!?;\n]+|\bbut\b/i).some((clause) => { const claim =