|
5 | 5 | validateObjectReferences as validateObjectReferencesUnrecorded, |
6 | 6 | OBJECT_REFERENCE_UNKNOWN, |
7 | 7 | OBJECT_REFERENCE_UNREGISTERED_PLATFORM, |
8 | | - USER_FIELD_FOREIGN_REFERENCE, |
9 | 8 | } from './validate-object-references.js'; |
10 | | -import { Field, ObjectSchema, referenceTargetOf } from '@objectstack/spec/data'; |
11 | 9 | import { runAuthoringRules, splitBySeverity } from './authoring-rules.js'; |
12 | 10 | import { PLATFORM_OBJECT_PREFIXES } from '@objectstack/spec/system'; |
13 | 11 | import { explainRule } from './rule-explanations.js'; |
@@ -706,104 +704,20 @@ describe('[#19289] validateObjectReferences — a `user` target comes from the T |
706 | 704 | expect(findings[0].rule).toBe(OBJECT_REFERENCE_UNKNOWN); |
707 | 705 | }); |
708 | 706 |
|
709 | | - it('control: the rule still REPORTS a real miss — an explicit carrier naming an unknown object errors', () => { |
| 707 | + it('control: the rule still REPORTS a real miss — a `lookup` naming an unknown object errors', () => { |
710 | 708 | // Without this, "no findings" above reads equally well as a rule that |
711 | | - // stopped judging `user` fields altogether. |
712 | | - // [#24808] A `user` field naming any object but `sys_user` also draws the |
713 | | - // foreign-target warning, so the unknown-object error is read by rule. |
714 | | - const findings = validateObjectReferences(userField({ reference: 'zzz_nope' })); |
715 | | - expect(findings.filter((f) => f.rule === OBJECT_REFERENCE_UNKNOWN)).toHaveLength(1); |
716 | | - expect(findings.map((f) => f.rule).sort()).toEqual([OBJECT_REFERENCE_UNKNOWN, USER_FIELD_FOREIGN_REFERENCE].sort()); |
| 709 | + // stopped judging fields altogether. [#24952] A `user` field can no longer |
| 710 | + // carry the miss: its target is the type's constant whatever the carrier |
| 711 | + // says (`FieldSchema` refuses a foreign name), so the same field leg is |
| 712 | + // proven on a `lookup`, and a foreign carrier on `user` resolves to |
| 713 | + // `sys_user` and draws nothing. |
| 714 | + const lookup = validateObjectReferences(userField({ type: 'lookup', reference: 'zzz_nope' })); |
| 715 | + expect(lookup).toHaveLength(1); |
| 716 | + expect(lookup[0].rule).toBe(OBJECT_REFERENCE_UNKNOWN); |
| 717 | + expect(validateObjectReferences(userField({ reference: 'zzz_nope' }))).toEqual([]); |
717 | 718 | }); |
718 | 719 | }); |
719 | 720 |
|
720 | | -/** |
721 | | - * [#24808] A `user` field's target is FIXED to `sys_user` by the spec, and |
722 | | - * nothing enforced it: the reproduction below parses through `ObjectSchema` |
723 | | - * and resolves through `referenceTargetOf` to the authored object. Under the |
724 | | - * 「甲」 ruling the 18.x half is this warning (no ADR-0087 conversion can carry |
725 | | - * the refusal: a `lookup` joins the referential delete walk a `user` field |
726 | | - * sits outside), so the parse half below pins that the shape still LOADS. |
727 | | - */ |
728 | | -describe('[#24808] validateObjectReferences — a `user` field naming another object warns', () => { |
729 | | - const stackWith = (assignee: Record<string, unknown>) => ({ |
730 | | - objects: [ |
731 | | - { name: 'probe_account', fields: { name: { type: 'text', label: 'Name' } } }, |
732 | | - { name: 'probe_task', fields: { name: { type: 'text', label: 'Name' }, owner: assignee } }, |
733 | | - ], |
734 | | - }); |
735 | | - const PROBE = { type: 'user', label: 'Owner', reference: 'probe_account' }; |
736 | | - |
737 | | - it('reproduction: the shape parses and the arbiter follows the authored object', () => { |
738 | | - const parsed = ObjectSchema.safeParse({ name: 'probe_task', fields: { owner: PROBE } }); |
739 | | - expect(parsed.success).toBe(true); |
740 | | - expect(referenceTargetOf(PROBE)).toBe('probe_account'); |
741 | | - }); |
742 | | - |
743 | | - it('warns — never errors — and names the field, the target and the lookup spelling', () => { |
744 | | - const findings = validateObjectReferences(stackWith(PROBE)); |
745 | | - expect(findings).toHaveLength(1); |
746 | | - const [f] = findings; |
747 | | - expect(f.rule).toBe(USER_FIELD_FOREIGN_REFERENCE); |
748 | | - expect(f.severity).toBe('warning'); |
749 | | - expect(f.path).toBe('objects[1].fields.owner.reference'); |
750 | | - expect(f.where).toBe('object "probe_task" · field "owner"'); |
751 | | - expect(f.message).toContain('"probe_account"'); |
752 | | - expect(f.hint).toContain("{ type: 'lookup', reference: 'probe_account' }"); |
753 | | - }); |
754 | | - |
755 | | - it('warns on an object extension too — the verdict needs no artifact context', () => { |
756 | | - const findings = validateObjectReferences({ |
757 | | - objectExtensions: [{ extend: 'sys_user_profile', fields: { mentor: { ...PROBE } } }], |
758 | | - }); |
759 | | - expect(findings.filter((f) => f.rule === USER_FIELD_FOREIGN_REFERENCE)).toEqual([ |
760 | | - expect.objectContaining({ path: 'objectExtensions[0].fields.mentor.reference', severity: 'warning' }), |
761 | | - ]); |
762 | | - }); |
763 | | - |
764 | | - it('warns on the map form of `fields` and on the array form of `objects` alike', () => { |
765 | | - const findings = validateObjectReferences({ |
766 | | - objects: { probe_task: { fields: { owner: PROBE } }, probe_account: { fields: {} } }, |
767 | | - }); |
768 | | - expect(findings.map((f) => f.rule)).toEqual([USER_FIELD_FOREIGN_REFERENCE]); |
769 | | - }); |
770 | | - |
771 | | - it('control: the two legal spellings — no `reference`, and `reference: "sys_user"` — stay silent', () => { |
772 | | - expect(validateObjectReferences(stackWith({ type: 'user', label: 'Owner' }))).toEqual([]); |
773 | | - expect(validateObjectReferences(stackWith({ type: 'user', label: 'Owner', reference: 'sys_user' }))).toEqual([]); |
774 | | - expect(validateObjectReferences(stackWith(Field.user({ label: 'Owner' })))).toEqual([]); |
775 | | - }); |
776 | | - |
777 | | - it('control: a `lookup` to the same object is the canonical spelling and draws nothing', () => { |
778 | | - expect(validateObjectReferences(stackWith(Field.lookup('probe_account', { label: 'Owner' })))).toEqual([]); |
779 | | - expect(validateObjectReferences(stackWith({ type: 'lookup', reference: 'probe_account' }))).toEqual([]); |
780 | | - }); |
781 | | - |
782 | | - it('control: an unreadable carrier still REFUSES rather than reading as a foreign target', () => { |
783 | | - expect(() => validateObjectReferences(stackWith({ type: 'user', reference: { object: 'sys_user' } }))) |
784 | | - .toThrow(/`reference` is an object/); |
785 | | - }); |
786 | | - |
787 | | - it('prints one line each, and `os explain` carries the reasoning the verdict leaves out', () => { |
788 | | - const [f] = validateObjectReferences(stackWith(PROBE)); |
789 | | - for (const text of [f.message, f.hint]) { |
790 | | - expect(text).not.toContain('\n'); |
791 | | - expect(text.length, text).toBeLessThanOrEqual(200); |
792 | | - } |
793 | | - const entry = explainRule(USER_FIELD_FOREIGN_REFERENCE); |
794 | | - expect(entry?.rule).toBe(USER_FIELD_FOREIGN_REFERENCE); |
795 | | - expect(entry!.paragraphs.join('\n')).toContain("deleteBehavior: 'set_null'"); |
796 | | - }); |
797 | | - |
798 | | - for (const command of ['validate', 'build', 'lint'] as const) { |
799 | | - it(`reaches \`os ${command}\` through the authoring registry, as an advisory and never a gate`, () => { |
800 | | - const { errors, advisories } = splitBySeverity(runAuthoringRules(command, { normalized: stackWith(PROBE) as never })); |
801 | | - expect(errors.filter((f) => f.rule === USER_FIELD_FOREIGN_REFERENCE)).toEqual([]); |
802 | | - expect(advisories.filter((f) => f.rule === USER_FIELD_FOREIGN_REFERENCE)).toHaveLength(1); |
803 | | - }); |
804 | | - } |
805 | | -}); |
806 | | - |
807 | 721 | /** |
808 | 722 | * [#20216] A view CONTAINER's own `object` — the key `getViewsByObject()` |
809 | 723 | * indexes by. Modelled on an `os init -t app` project (namespace `my_app`), |
|
0 commit comments