|
1 | 1 | // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 |
|
3 | 3 | import { describe, it, expect } from 'vitest'; |
4 | | -import { lintDeprecatedAliases, ACTION_TARGET_EXECUTE_CONFLICT } from './deprecated-aliases'; |
| 4 | +import { |
| 5 | + lintDeprecatedAliases, |
| 6 | + ACTION_TARGET_EXECUTE_CONFLICT, |
| 7 | + FIELD_REQUIREDWHEN_CONDITIONALREQUIRED_CONFLICT, |
| 8 | + AGENT_KNOWLEDGE_SOURCES_TOPICS_CONFLICT, |
| 9 | +} from './deprecated-aliases'; |
5 | 10 |
|
6 | 11 | // ── #3743: the discarded `execute` alias now has an author-facing warning ── |
7 | 12 | // |
@@ -156,3 +161,156 @@ describe('lintDeprecatedAliases — action-target-execute-conflict', () => { |
156 | 161 | ]); |
157 | 162 | }); |
158 | 163 | }); |
| 164 | + |
| 165 | +// ── The two sibling fold-and-drop aliases (#3743 follow-up) ────────────────── |
| 166 | +// |
| 167 | +// `execute` was never special: `FieldSchema` folds `conditionalRequired` into |
| 168 | +// `requiredWhen` and `AIKnowledgeSchema` folds `topics` into `sources`, both |
| 169 | +// dropping the alias from the parsed output exactly the same way. That makes |
| 170 | +// all three invisible to every post-parse check, so all three belong to this |
| 171 | +// pre-parse pass — which is the reason the pass exists as a rule SET rather |
| 172 | +// than one hard-coded rule. |
| 173 | + |
| 174 | +describe('lintDeprecatedAliases — field-requiredwhen-conditionalrequired-conflict', () => { |
| 175 | + const objectWith = (field: Record<string, unknown>) => ({ |
| 176 | + objects: [{ |
| 177 | + name: 'crm_task', |
| 178 | + fields: { due_date: { type: 'date', label: 'Due', ...field } }, |
| 179 | + }], |
| 180 | + }); |
| 181 | + |
| 182 | + it('flags a field declaring both predicates with different sources', () => { |
| 183 | + const findings = lintDeprecatedAliases(objectWith({ |
| 184 | + requiredWhen: 'record.stage == "closed"', |
| 185 | + conditionalRequired: 'record.amount > 0', |
| 186 | + })); |
| 187 | + |
| 188 | + expect(findings).toHaveLength(1); |
| 189 | + const [f] = findings; |
| 190 | + expect(f.rule).toBe(FIELD_REQUIREDWHEN_CONDITIONALREQUIRED_CONFLICT); |
| 191 | + expect(f.severity).toBe('warning'); |
| 192 | + expect(f.where).toBe(`field 'due_date' on object 'crm_task'`); |
| 193 | + expect(f.message).toContain('record.stage == "closed"'); |
| 194 | + expect(f.message).toContain('record.amount > 0'); |
| 195 | + expect(f.message).toContain(`'requiredWhen' wins`); |
| 196 | + expect(f.message).toContain('never gates the field'); |
| 197 | + expect(f.hint).toContain(`Delete 'conditionalRequired'`); |
| 198 | + }); |
| 199 | + |
| 200 | + it('stays quiet when only one predicate slot is declared', () => { |
| 201 | + expect(lintDeprecatedAliases(objectWith({ requiredWhen: 'record.paid' }))).toEqual([]); |
| 202 | + expect(lintDeprecatedAliases(objectWith({ conditionalRequired: 'record.paid' }))).toEqual([]); |
| 203 | + }); |
| 204 | + |
| 205 | + it('compares the predicate TEXT, not the wrapper', () => { |
| 206 | + // A bare string and the `{ dialect, source }` envelope it lowers into are |
| 207 | + // the same predicate written two ways — nothing is lost either way. |
| 208 | + expect(lintDeprecatedAliases(objectWith({ |
| 209 | + requiredWhen: 'record.paid', |
| 210 | + conditionalRequired: { dialect: 'cel', source: 'record.paid' }, |
| 211 | + }))).toEqual([]); |
| 212 | + }); |
| 213 | + |
| 214 | + it('reads the envelope form on both slots', () => { |
| 215 | + const [f] = lintDeprecatedAliases(objectWith({ |
| 216 | + requiredWhen: { dialect: 'cel', source: 'record.a' }, |
| 217 | + conditionalRequired: { dialect: 'cel', source: 'record.b' }, |
| 218 | + })); |
| 219 | + expect(f.rule).toBe(FIELD_REQUIREDWHEN_CONDITIONALREQUIRED_CONFLICT); |
| 220 | + expect(f.message).toContain('record.a'); |
| 221 | + expect(f.message).toContain('record.b'); |
| 222 | + }); |
| 223 | + |
| 224 | + it('covers fields added by an object extension', () => { |
| 225 | + // An extension adds/overrides fields on another package's object through |
| 226 | + // the same `FieldSchema`, so it carries the same silent discard. |
| 227 | + const [f] = lintDeprecatedAliases({ |
| 228 | + objectExtensions: [{ |
| 229 | + objectName: 'sys_user', |
| 230 | + fields: { nickname: { type: 'text', requiredWhen: 'record.a', conditionalRequired: 'record.b' } }, |
| 231 | + }], |
| 232 | + }); |
| 233 | + expect(f.where).toBe(`field 'nickname' on object extension 'sys_user'`); |
| 234 | + }); |
| 235 | + |
| 236 | + it('tolerates an object with no fields', () => { |
| 237 | + expect(lintDeprecatedAliases({ objects: [{ name: 'crm_task' }] })).toEqual([]); |
| 238 | + }); |
| 239 | +}); |
| 240 | + |
| 241 | +describe('lintDeprecatedAliases — agent-knowledge-sources-topics-conflict', () => { |
| 242 | + const agentWith = (knowledge: Record<string, unknown>) => ({ |
| 243 | + agents: [{ name: 'support_bot', label: 'Support', knowledge }], |
| 244 | + }); |
| 245 | + |
| 246 | + it('flags an agent declaring both source lists with different members', () => { |
| 247 | + const findings = lintDeprecatedAliases(agentWith({ |
| 248 | + sources: ['faq', 'policies'], |
| 249 | + topics: ['legacy_kb'], |
| 250 | + })); |
| 251 | + |
| 252 | + expect(findings).toHaveLength(1); |
| 253 | + const [f] = findings; |
| 254 | + expect(f.rule).toBe(AGENT_KNOWLEDGE_SOURCES_TOPICS_CONFLICT); |
| 255 | + expect(f.severity).toBe('warning'); |
| 256 | + expect(f.where).toBe(`agent 'support_bot' knowledge`); |
| 257 | + expect(f.message).toContain(`'faq', 'policies'`); |
| 258 | + expect(f.message).toContain(`'legacy_kb'`); |
| 259 | + expect(f.message).toContain(`'sources' wins`); |
| 260 | + expect(f.message).toContain('never recruited into RAG context'); |
| 261 | + expect(f.hint).toContain(`Delete 'topics'`); |
| 262 | + }); |
| 263 | + |
| 264 | + it('stays quiet when only one list is declared', () => { |
| 265 | + expect(lintDeprecatedAliases(agentWith({ sources: ['faq'] }))).toEqual([]); |
| 266 | + expect(lintDeprecatedAliases(agentWith({ topics: ['faq'] }))).toEqual([]); |
| 267 | + }); |
| 268 | + |
| 269 | + it('stays quiet when the two lists have the same members in any order', () => { |
| 270 | + // Order and repetition are not meaningful for either slot, so the same set |
| 271 | + // written twice loses nothing. |
| 272 | + expect(lintDeprecatedAliases(agentWith({ |
| 273 | + sources: ['faq', 'policies'], |
| 274 | + topics: ['policies', 'faq', 'faq'], |
| 275 | + }))).toEqual([]); |
| 276 | + }); |
| 277 | + |
| 278 | + it('treats an empty list as undeclared', () => { |
| 279 | + expect(lintDeprecatedAliases(agentWith({ sources: [], topics: ['faq'] }))).toEqual([]); |
| 280 | + }); |
| 281 | + |
| 282 | + it('tolerates an agent with no knowledge block', () => { |
| 283 | + expect(lintDeprecatedAliases({ agents: [{ name: 'support_bot' }] })).toEqual([]); |
| 284 | + }); |
| 285 | +}); |
| 286 | + |
| 287 | +describe('lintDeprecatedAliases — all three rules together', () => { |
| 288 | + it('reports each rule independently in one pass', () => { |
| 289 | + const stack = { |
| 290 | + objects: [{ |
| 291 | + name: 'crm_task', |
| 292 | + fields: { due_date: { type: 'date', requiredWhen: 'record.a', conditionalRequired: 'record.b' } }, |
| 293 | + actions: [{ name: 'convert', type: 'script', target: 'preferred', execute: 'legacy' }], |
| 294 | + }], |
| 295 | + agents: [{ name: 'support_bot', knowledge: { sources: ['faq'], topics: ['legacy_kb'] } }], |
| 296 | + }; |
| 297 | + |
| 298 | + expect(lintDeprecatedAliases(stack).map((f) => f.rule).sort()).toEqual([ |
| 299 | + ACTION_TARGET_EXECUTE_CONFLICT, |
| 300 | + AGENT_KNOWLEDGE_SOURCES_TOPICS_CONFLICT, |
| 301 | + FIELD_REQUIREDWHEN_CONDITIONALREQUIRED_CONFLICT, |
| 302 | + ].sort()); |
| 303 | + }); |
| 304 | + |
| 305 | + it('returns nothing for a stack that uses only canonical keys', () => { |
| 306 | + const stack = { |
| 307 | + objects: [{ |
| 308 | + name: 'crm_task', |
| 309 | + fields: { due_date: { type: 'date', requiredWhen: 'record.a' } }, |
| 310 | + actions: [{ name: 'convert', type: 'script', target: 'preferred' }], |
| 311 | + }], |
| 312 | + agents: [{ name: 'support_bot', knowledge: { sources: ['faq'] } }], |
| 313 | + }; |
| 314 | + expect(lintDeprecatedAliases(stack)).toEqual([]); |
| 315 | + }); |
| 316 | +}); |
0 commit comments