@fedify/vocab 2.3.0-dev.1372 → 2.3.0-dev.1404

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/vocab.test.ts CHANGED
@@ -4,13 +4,13 @@ import {
4
4
  LanguageString,
5
5
  parseDecimal,
6
6
  } from "@fedify/vocab-runtime";
7
- import { configure, type LogRecord, reset } from "@logtape/logtape";
8
7
  import {
9
8
  areAllScalarTypes,
10
9
  loadSchemaFiles,
11
10
  type PropertySchema,
12
11
  type TypeSchema,
13
12
  } from "@fedify/vocab-tools";
13
+ import { configure, type LogRecord, reset } from "@logtape/logtape";
14
14
  import { pascalCase } from "es-toolkit";
15
15
  import {
16
16
  deepStrictEqual,
@@ -22,27 +22,36 @@ import {
22
22
  import { assertInstanceOf } from "./utils.ts";
23
23
  import * as vocab from "./vocab.ts";
24
24
  import {
25
+ Accept,
25
26
  Activity,
27
+ Agreement,
26
28
  Announce,
27
29
  Collection,
30
+ Commitment,
28
31
  Create,
29
32
  CryptographicKey,
30
33
  type DataIntegrityProof,
31
34
  Delete,
35
+ Document,
32
36
  Endpoints,
33
37
  Follow,
34
38
  Hashtag,
39
+ Intent,
35
40
  InteractionPolicy,
36
41
  InteractionRule,
37
42
  Link,
43
+ Measure,
38
44
  Note,
39
45
  Object,
46
+ Offer,
40
47
  OrderedCollectionPage,
41
48
  Person,
42
49
  Place,
50
+ Proposal,
43
51
  Question,
44
52
  QuoteAuthorization,
45
53
  QuoteRequest,
54
+ Reject,
46
55
  Source,
47
56
  Tombstone,
48
57
  } from "./vocab.ts";
@@ -2802,6 +2811,338 @@ test(
2802
2811
  },
2803
2812
  );
2804
2813
 
2814
+ test("FEP-0837: Commitment roundtrip preserves satisfies and resourceQuantity", async () => {
2815
+ const commitment = new Commitment({
2816
+ id: new URL("https://market.example/agreements/abc#primary"),
2817
+ satisfies: new URL(
2818
+ "https://market.example/proposals/abc#primary",
2819
+ ),
2820
+ resourceQuantity: new Measure({
2821
+ unit: "one",
2822
+ numericalValue: parseDecimal("1"),
2823
+ }),
2824
+ });
2825
+ const jsonLd = await commitment.toJsonLd({
2826
+ contextLoader: mockDocumentLoader,
2827
+ });
2828
+ const restored = await Commitment.fromJsonLd(jsonLd, {
2829
+ documentLoader: mockDocumentLoader,
2830
+ contextLoader: mockDocumentLoader,
2831
+ });
2832
+ // A finalized Commitment carries a (fragment) id even though Commitment is
2833
+ // not an independently fetchable entity (`entity: false`).
2834
+ deepStrictEqual(restored.id?.href, commitment.id?.href);
2835
+ deepStrictEqual(restored.satisfies, commitment.satisfies);
2836
+ deepStrictEqual(restored.resourceQuantity?.unit, "one");
2837
+ deepStrictEqual(
2838
+ restored.resourceQuantity?.numericalValue,
2839
+ parseDecimal("1"),
2840
+ );
2841
+ });
2842
+
2843
+ test("FEP-0837: Agreement roundtrip with both commitments", async () => {
2844
+ const agreement = new Agreement({
2845
+ id: new URL(
2846
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2",
2847
+ ),
2848
+ attribution: new URL("https://market.example/users/alice"),
2849
+ stipulates: new Commitment({
2850
+ id: new URL(
2851
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#primary",
2852
+ ),
2853
+ satisfies: new URL(
2854
+ "https://market.example/proposals/ddde9d6f#primary",
2855
+ ),
2856
+ resourceQuantity: new Measure({
2857
+ unit: "one",
2858
+ numericalValue: parseDecimal("1"),
2859
+ }),
2860
+ }),
2861
+ stipulatesReciprocal: new Commitment({
2862
+ id: new URL(
2863
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#reciprocal",
2864
+ ),
2865
+ satisfies: new URL(
2866
+ "https://market.example/proposals/ddde9d6f#reciprocal",
2867
+ ),
2868
+ resourceQuantity: new Measure({
2869
+ unit: "currencyAmount",
2870
+ numericalValue: parseDecimal("30.00"),
2871
+ }),
2872
+ }),
2873
+ });
2874
+ const jsonLd = await agreement.toJsonLd({
2875
+ contextLoader: mockDocumentLoader,
2876
+ });
2877
+ const restored = await Agreement.fromJsonLd(jsonLd, {
2878
+ documentLoader: mockDocumentLoader,
2879
+ contextLoader: mockDocumentLoader,
2880
+ });
2881
+ deepStrictEqual(restored.id?.href, agreement.id?.href);
2882
+ deepStrictEqual(
2883
+ restored.stipulates?.id?.href,
2884
+ agreement.stipulates?.id?.href,
2885
+ );
2886
+ deepStrictEqual(
2887
+ restored.stipulates?.satisfies?.href,
2888
+ agreement.stipulates?.satisfies?.href,
2889
+ );
2890
+ deepStrictEqual(
2891
+ restored.stipulates?.resourceQuantity?.numericalValue,
2892
+ parseDecimal("1"),
2893
+ );
2894
+ deepStrictEqual(
2895
+ restored.stipulatesReciprocal?.id?.href,
2896
+ agreement.stipulatesReciprocal?.id?.href,
2897
+ );
2898
+ deepStrictEqual(
2899
+ restored.stipulatesReciprocal?.satisfies?.href,
2900
+ agreement.stipulatesReciprocal?.satisfies?.href,
2901
+ );
2902
+ deepStrictEqual(
2903
+ restored.stipulatesReciprocal?.resourceQuantity?.unit,
2904
+ "currencyAmount",
2905
+ );
2906
+ deepStrictEqual(
2907
+ restored.stipulatesReciprocal?.resourceQuantity?.numericalValue,
2908
+ parseDecimal("30.00"),
2909
+ );
2910
+ });
2911
+
2912
+ test("FEP-0837: Agreement parses Accept-result example adapted from spec", async () => {
2913
+ // Adapted from FEP-0837's "Accepting an agreement" example, to Fedify customization
2914
+ const json = {
2915
+ "@context": [
2916
+ "https://www.w3.org/ns/activitystreams",
2917
+ {
2918
+ vf: "https://w3id.org/valueflows/ont/vf#",
2919
+ Agreement: "vf:Agreement",
2920
+ stipulates: "vf:stipulates",
2921
+ stipulatesReciprocal: "vf:stipulatesReciprocal",
2922
+ Commitment: "vf:Commitment",
2923
+ satisfies: { "@id": "vf:satisfies", "@type": "@id" },
2924
+ resourceQuantity: "vf:resourceQuantity",
2925
+ hasUnit: "om2:hasUnit",
2926
+ hasNumericalValue: "om2:hasNumericalValue",
2927
+ om2: "http://www.ontology-of-units-of-measure.org/resource/om-2/",
2928
+ },
2929
+ ],
2930
+ type: "Agreement",
2931
+ id:
2932
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2",
2933
+ attributedTo: "https://market.example/users/alice",
2934
+ stipulates: {
2935
+ id:
2936
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#primary",
2937
+ type: "Commitment",
2938
+ satisfies:
2939
+ "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#primary",
2940
+ resourceQuantity: {
2941
+ hasUnit: "one",
2942
+ hasNumericalValue: "1",
2943
+ },
2944
+ },
2945
+ stipulatesReciprocal: {
2946
+ id:
2947
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#reciprocal",
2948
+ type: "Commitment",
2949
+ satisfies:
2950
+ "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#reciprocal",
2951
+ resourceQuantity: {
2952
+ hasUnit: "currencyAmount",
2953
+ hasNumericalValue: "30.00",
2954
+ },
2955
+ },
2956
+ };
2957
+ const agreement = await Agreement.fromJsonLd(json, {
2958
+ documentLoader: mockDocumentLoader,
2959
+ contextLoader: mockDocumentLoader,
2960
+ });
2961
+ deepStrictEqual(
2962
+ agreement.id?.href,
2963
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2",
2964
+ );
2965
+ deepStrictEqual(
2966
+ agreement.attributionId?.href,
2967
+ "https://market.example/users/alice",
2968
+ );
2969
+ deepStrictEqual(
2970
+ agreement.stipulates?.id?.href,
2971
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#primary",
2972
+ );
2973
+ deepStrictEqual(
2974
+ agreement.stipulates?.satisfies?.href,
2975
+ "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#primary",
2976
+ );
2977
+ deepStrictEqual(
2978
+ agreement.stipulates?.resourceQuantity?.numericalValue,
2979
+ parseDecimal("1"),
2980
+ );
2981
+ deepStrictEqual(
2982
+ agreement.stipulatesReciprocal?.id?.href,
2983
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#reciprocal",
2984
+ );
2985
+ deepStrictEqual(
2986
+ agreement.stipulatesReciprocal?.satisfies?.href,
2987
+ "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#reciprocal",
2988
+ );
2989
+ deepStrictEqual(
2990
+ agreement.stipulatesReciprocal?.resourceQuantity?.numericalValue,
2991
+ parseDecimal("30.00"),
2992
+ );
2993
+ });
2994
+
2995
+ test("FEP-0837: Full marketplace flow - Proposal => Offer => Accept => Confirmation", async () => {
2996
+ // Stage 1: Alice publishes a Proposal. Its id anchors the intent fragment
2997
+ // URI (`#primary`) that the downstream commitments satisfy.
2998
+ const proposal = new Proposal({
2999
+ id: new URL(
3000
+ "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930",
3001
+ ),
3002
+ attribution: new URL("https://market.example/users/alice"),
3003
+ purpose: "offer",
3004
+ publishes: new Intent({
3005
+ action: "transfer",
3006
+ resourceConformsTo: new URL("https://www.wikidata.org/wiki/Q11442"),
3007
+ resourceQuantity: new Measure({
3008
+ unit: "one",
3009
+ numericalValue: parseDecimal("1"),
3010
+ }),
3011
+ }),
3012
+ to: new URL("https://www.w3.org/ns/activitystreams#Public"),
3013
+ });
3014
+ ok(proposal.purpose === "offer");
3015
+ const primaryIntent = new URL(`${proposal.id?.href}#primary`);
3016
+
3017
+ // Stage 2a: Bob sends Offer(Agreement) whose commitment satisfies the
3018
+ // proposal's primary intent.
3019
+ const offerId = new URL(
3020
+ "https://social.example/objects/fc4af0d2-c3a1-409b-947c-3c5be29f49b0/offer",
3021
+ );
3022
+ const offer = new Offer({
3023
+ id: offerId,
3024
+ actor: new URL("https://social.example/users/bob"),
3025
+ object: new Agreement({
3026
+ stipulates: new Commitment({
3027
+ satisfies: primaryIntent,
3028
+ resourceQuantity: new Measure({
3029
+ unit: "one",
3030
+ numericalValue: parseDecimal("1"),
3031
+ }),
3032
+ }),
3033
+ }),
3034
+ to: new URL("https://market.example/users/alice"),
3035
+ });
3036
+ const offerJson = await offer.toJsonLd({
3037
+ contextLoader: mockDocumentLoader,
3038
+ });
3039
+ const offerRestored = await Offer.fromJsonLd(offerJson, {
3040
+ documentLoader: mockDocumentLoader,
3041
+ contextLoader: mockDocumentLoader,
3042
+ });
3043
+ const restoredAgreement = await offerRestored.getObject({
3044
+ documentLoader: mockDocumentLoader,
3045
+ contextLoader: mockDocumentLoader,
3046
+ });
3047
+ assertInstanceOf(restoredAgreement, Agreement);
3048
+ deepStrictEqual(
3049
+ restoredAgreement.stipulates?.satisfies?.href,
3050
+ primaryIntent.href,
3051
+ );
3052
+ deepStrictEqual(
3053
+ restoredAgreement.stipulates?.resourceQuantity?.numericalValue,
3054
+ parseDecimal("1"),
3055
+ );
3056
+
3057
+ // Stage 2b: Alice sends Accept(Offer) with finalized Agreement in `result`.
3058
+ const agreementId = new URL(
3059
+ "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2",
3060
+ );
3061
+ const accept = new Accept({
3062
+ id: new URL(
3063
+ "https://market.example/activities/059f08fa-31b1-4136-8d76-5987d705a0ab",
3064
+ ),
3065
+ actor: new URL("https://market.example/users/alice"),
3066
+ object: offerId,
3067
+ result: new Agreement({
3068
+ id: agreementId,
3069
+ attribution: new URL("https://market.example/users/alice"),
3070
+ stipulates: new Commitment({
3071
+ satisfies: primaryIntent,
3072
+ resourceQuantity: new Measure({
3073
+ unit: "one",
3074
+ numericalValue: parseDecimal("1"),
3075
+ }),
3076
+ }),
3077
+ }),
3078
+ to: new URL("https://social.example/users/bob"),
3079
+ });
3080
+ const acceptJson = await accept.toJsonLd({
3081
+ contextLoader: mockDocumentLoader,
3082
+ });
3083
+ const acceptRestored = await Accept.fromJsonLd(acceptJson, {
3084
+ documentLoader: mockDocumentLoader,
3085
+ contextLoader: mockDocumentLoader,
3086
+ });
3087
+ deepStrictEqual(acceptRestored.objectId?.href, offerId.href);
3088
+ const acceptResult = await acceptRestored.getResult({
3089
+ documentLoader: mockDocumentLoader,
3090
+ contextLoader: mockDocumentLoader,
3091
+ });
3092
+ assertInstanceOf(acceptResult, Agreement);
3093
+ deepStrictEqual(acceptResult.id?.href, agreementId.href);
3094
+
3095
+ // Stage 2 alt: Reject(Offer) with a reason.
3096
+ const reject = new Reject({
3097
+ actor: new URL("https://market.example/users/alice"),
3098
+ object: offerId,
3099
+ content: "Not available",
3100
+ to: new URL("https://social.example/users/bob"),
3101
+ });
3102
+ const rejectJson = await reject.toJsonLd({
3103
+ contextLoader: mockDocumentLoader,
3104
+ });
3105
+ const rejectRestored = await Reject.fromJsonLd(rejectJson, {
3106
+ documentLoader: mockDocumentLoader,
3107
+ contextLoader: mockDocumentLoader,
3108
+ });
3109
+ deepStrictEqual(rejectRestored.objectId?.href, offerId.href);
3110
+ deepStrictEqual(rejectRestored.content?.toString(), "Not available");
3111
+
3112
+ // Stage 3: Confirmation as Create(Document) with `context` linking to the
3113
+ // finalized Agreement. The Create activity needs an @id at the same origin
3114
+ // as its embedded Document so cross-origin trust preserves the embedded
3115
+ // form (rather than unwinding to a URL reference that would require a fetch).
3116
+ const receipt = new Create({
3117
+ id: new URL(
3118
+ "https://market.example/receipts/ad2f7ee1-6567-413e-a10b-72650cbdc743/create",
3119
+ ),
3120
+ actor: new URL("https://market.example/users/alice"),
3121
+ object: new Document({
3122
+ id: new URL(
3123
+ "https://market.example/receipts/ad2f7ee1-6567-413e-a10b-72650cbdc743",
3124
+ ),
3125
+ name: "Receipt",
3126
+ contexts: [agreementId],
3127
+ published: Temporal.Instant.from("2023-07-03T14:13:41.843794Z"),
3128
+ }),
3129
+ to: new URL("https://social.example/users/bob"),
3130
+ });
3131
+ const receiptJson = await receipt.toJsonLd({
3132
+ contextLoader: mockDocumentLoader,
3133
+ });
3134
+ const receiptRestored = await Create.fromJsonLd(receiptJson, {
3135
+ documentLoader: mockDocumentLoader,
3136
+ contextLoader: mockDocumentLoader,
3137
+ });
3138
+ const receiptObject = await receiptRestored.getObject({
3139
+ documentLoader: mockDocumentLoader,
3140
+ contextLoader: mockDocumentLoader,
3141
+ });
3142
+ assertInstanceOf(receiptObject, Document);
3143
+ deepStrictEqual(receiptObject.contextIds[0]?.href, agreementId.href);
3144
+ });
3145
+
2805
3146
  function getAllProperties(
2806
3147
  type: TypeSchema,
2807
3148
  types: Record<string, TypeSchema>,