@reventlessdev/reventless-seed-aws 1.0.0-alpha.7 → 1.0.0-alpha.8
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/CHANGELOG.md +7 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res +261 -6
- package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +211 -25
- package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmj +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.res +170 -0
- package/lib/bs/tests/ObjectStoreResetTest.res.mjs +211 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmj +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreResetTest.res +170 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.res +261 -6
- package/package.json +1 -1
- package/src/ReventlessSeedAws_Reset.res +261 -6
- package/src/ReventlessSeedAws_Reset.res.mjs +211 -25
- package/tests/ObjectStoreResetTest.res +170 -0
- package/tests/ObjectStoreResetTest.res.mjs +211 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as ReventlessSeedAws_Reset from "../src/ReventlessSeedAws_Reset.res.mjs";
|
|
6
|
+
|
|
7
|
+
function obj(entries) {
|
|
8
|
+
return Object.fromEntries(entries);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function str(prim) {
|
|
12
|
+
return prim;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function storeEntry(bucket, prefix) {
|
|
16
|
+
return Object.fromEntries([
|
|
17
|
+
[
|
|
18
|
+
"bucketName",
|
|
19
|
+
bucket
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"keyPrefix",
|
|
23
|
+
prefix
|
|
24
|
+
]
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function output(entries) {
|
|
29
|
+
return Object.fromEntries(entries);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function store(qualified, bucket, prefix) {
|
|
33
|
+
let match = Stdlib_Option.getOr(ReventlessSeedAws_Reset.splitQualified(qualified), [
|
|
34
|
+
"",
|
|
35
|
+
qualified
|
|
36
|
+
]);
|
|
37
|
+
return {
|
|
38
|
+
qualified: qualified,
|
|
39
|
+
plugin: match[0],
|
|
40
|
+
store: match[1],
|
|
41
|
+
bucketName: bucket,
|
|
42
|
+
keyPrefix: prefix
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
globalThis.describe("parseObjectStores", () => {
|
|
47
|
+
globalThis.test("splits the qualified key into plugin and store", () => {
|
|
48
|
+
let entries = [[
|
|
49
|
+
"Catalog.productImages",
|
|
50
|
+
storeEntry("alpha-stores", "productImages")
|
|
51
|
+
]];
|
|
52
|
+
let other = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
|
|
53
|
+
if (other.TAG === "Ok") {
|
|
54
|
+
let match = other._0;
|
|
55
|
+
if (match.length === 1) {
|
|
56
|
+
let s = match[0];
|
|
57
|
+
globalThis.expect([
|
|
58
|
+
s.plugin,
|
|
59
|
+
s.store,
|
|
60
|
+
s.bucketName,
|
|
61
|
+
s.keyPrefix
|
|
62
|
+
]).toEqual([
|
|
63
|
+
"Catalog",
|
|
64
|
+
"productImages",
|
|
65
|
+
"alpha-stores",
|
|
66
|
+
"productImages"
|
|
67
|
+
]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
JestGlobals.fail(`expected one parsed store, got ` + Stdlib_Option.getOr(JSON.stringify(other), "?"));
|
|
72
|
+
});
|
|
73
|
+
globalThis.test("splits at the first dot only", () => {
|
|
74
|
+
let match = ReventlessSeedAws_Reset.splitQualified("Catalog.product.images");
|
|
75
|
+
if (match !== undefined) {
|
|
76
|
+
globalThis.expect([
|
|
77
|
+
match[0],
|
|
78
|
+
match[1]
|
|
79
|
+
]).toEqual([
|
|
80
|
+
"Catalog",
|
|
81
|
+
"product.images"
|
|
82
|
+
]);
|
|
83
|
+
return;
|
|
84
|
+
} else {
|
|
85
|
+
return JestGlobals.fail("expected a split");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
globalThis.test("absent output yields no stores", () => {
|
|
89
|
+
let stores = ReventlessSeedAws_Reset.parseObjectStores(undefined);
|
|
90
|
+
if (stores.TAG !== "Ok") {
|
|
91
|
+
return JestGlobals.fail(stores._0);
|
|
92
|
+
}
|
|
93
|
+
globalThis.expect(stores._0.length).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
globalThis.test("a malformed entry is an error, not a skip", () => {
|
|
96
|
+
let entries = [[
|
|
97
|
+
"Catalog.productImages",
|
|
98
|
+
Object.fromEntries([[
|
|
99
|
+
"keyPrefix",
|
|
100
|
+
"x"
|
|
101
|
+
]])
|
|
102
|
+
]];
|
|
103
|
+
let message = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
|
|
104
|
+
if (message.TAG === "Ok") {
|
|
105
|
+
return JestGlobals.fail("expected a malformed entry to be refused");
|
|
106
|
+
}
|
|
107
|
+
globalThis.expect(message._0.includes("Catalog.productImages")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
globalThis.test("an unqualified key is an error", () => {
|
|
110
|
+
let entries = [[
|
|
111
|
+
"productImages",
|
|
112
|
+
storeEntry("alpha-stores", "productImages")
|
|
113
|
+
]];
|
|
114
|
+
let message = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
|
|
115
|
+
if (message.TAG === "Ok") {
|
|
116
|
+
return JestGlobals.fail("expected an unqualified key to be refused");
|
|
117
|
+
}
|
|
118
|
+
globalThis.expect(message._0.includes("productImages")).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
globalThis.describe("validateStores", () => {
|
|
123
|
+
globalThis.test("distinct prefixes in one bucket are fine", () => {
|
|
124
|
+
globalThis.expect(ReventlessSeedAws_Reset.validateStores([
|
|
125
|
+
store("Catalog.productImages", "alpha-stores", "productImages"),
|
|
126
|
+
store("Ordering.labels", "alpha-stores", "labels")
|
|
127
|
+
])).toEqual({
|
|
128
|
+
TAG: "Ok",
|
|
129
|
+
_0: undefined
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
globalThis.test("two plugins on one prefix are refused", () => {
|
|
133
|
+
let message = ReventlessSeedAws_Reset.validateStores([
|
|
134
|
+
store("Catalog.productImages", "alpha-stores", "productImages"),
|
|
135
|
+
store("Ordering.productImages", "alpha-stores", "productImages")
|
|
136
|
+
]);
|
|
137
|
+
if (message.TAG === "Ok") {
|
|
138
|
+
return JestGlobals.fail("expected a collision to be refused");
|
|
139
|
+
}
|
|
140
|
+
let message$1 = message._0;
|
|
141
|
+
globalThis.expect(message$1.includes("Catalog.productImages") && message$1.includes("Ordering.productImages")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
globalThis.test("an enclosing prefix is refused", () => {
|
|
144
|
+
let message = ReventlessSeedAws_Reset.validateStores([
|
|
145
|
+
store("Legacy.catalog", "alpha-stores", "Catalog"),
|
|
146
|
+
store("Catalog.productImages", "alpha-stores", "Catalog/productImages")
|
|
147
|
+
]);
|
|
148
|
+
if (message.TAG === "Ok") {
|
|
149
|
+
return JestGlobals.fail("expected an enclosing prefix to be refused");
|
|
150
|
+
}
|
|
151
|
+
globalThis.expect(message._0.includes("encloses")).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
globalThis.test("one prefix in two different buckets is fine", () => {
|
|
154
|
+
globalThis.expect(ReventlessSeedAws_Reset.validateStores([
|
|
155
|
+
store("Catalog.productImages", "catalog-productImages", "productImages"),
|
|
156
|
+
store("Ordering.productImages", "ordering-productImages", "productImages")
|
|
157
|
+
])).toEqual({
|
|
158
|
+
TAG: "Ok",
|
|
159
|
+
_0: undefined
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
globalThis.test("a sibling prefix sharing a character run is fine", () => {
|
|
163
|
+
globalThis.expect(ReventlessSeedAws_Reset.validateStores([
|
|
164
|
+
store("Catalog.images", "alpha-stores", "images"),
|
|
165
|
+
store("Catalog.imagesArchive", "alpha-stores", "images-archive")
|
|
166
|
+
])).toEqual({
|
|
167
|
+
TAG: "Ok",
|
|
168
|
+
_0: undefined
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
globalThis.test("an empty or slash-bearing store name is refused", () => {
|
|
172
|
+
let match = ReventlessSeedAws_Reset.validateStores([store("Catalog.productImages", "alpha-stores", "")]);
|
|
173
|
+
if (match.TAG === "Ok") {
|
|
174
|
+
JestGlobals.fail("expected an empty prefix to be refused");
|
|
175
|
+
}
|
|
176
|
+
let match$1 = ReventlessSeedAws_Reset.validateStores([store("Catalog.a/b", "alpha-stores", "a/b")]);
|
|
177
|
+
if (match$1.TAG === "Ok") {
|
|
178
|
+
return JestGlobals.fail("expected a store name containing a slash to be refused");
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
globalThis.describe("pluginOf", () => {
|
|
184
|
+
globalThis.test("prefers the declared plugin name over the label", () => {
|
|
185
|
+
globalThis.expect(ReventlessSeedAws_Reset.pluginOf({
|
|
186
|
+
projectDir: "../catalog-aws",
|
|
187
|
+
label: "catalog",
|
|
188
|
+
group: "Domain",
|
|
189
|
+
plugin: "Catalog"
|
|
190
|
+
})).toBe("Catalog");
|
|
191
|
+
});
|
|
192
|
+
globalThis.test("falls back to the label when no plugin name is declared", () => {
|
|
193
|
+
globalThis.expect(ReventlessSeedAws_Reset.pluginOf({
|
|
194
|
+
projectDir: ".",
|
|
195
|
+
label: "platform",
|
|
196
|
+
group: "Platform"
|
|
197
|
+
})).toBe("platform");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let Reset;
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
Reset,
|
|
205
|
+
obj,
|
|
206
|
+
str,
|
|
207
|
+
storeEntry,
|
|
208
|
+
output,
|
|
209
|
+
store,
|
|
210
|
+
}
|
|
211
|
+
/* Not a pure module */
|
package/lib/ocaml/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1785624003215)
|
|
2
|
+
#Done(1785624003246)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// How the reset learns which uploaded objects belong to which plugin, and what
|
|
2
|
+
// it refuses.
|
|
3
|
+
//
|
|
4
|
+
// A declared store's bucket is built by the PLATFORM deploy, so its tags say
|
|
5
|
+
// which project built it, not whose data is in it. Ownership therefore comes
|
|
6
|
+
// from the platform's `objectStores` stack output, and these pin the pure half
|
|
7
|
+
// of that: parsing the output, and refusing any store set a prefix-scoped wipe
|
|
8
|
+
// could not separate. The impure half is a `pulumi stack output` subprocess and
|
|
9
|
+
// the S3 calls, and the decisions under test are in neither.
|
|
10
|
+
|
|
11
|
+
open JestGlobals
|
|
12
|
+
|
|
13
|
+
module Reset = ReventlessSeedAws_Reset
|
|
14
|
+
|
|
15
|
+
let obj = entries => JSON.Encode.object(entries->Dict.fromArray)
|
|
16
|
+
let str = JSON.Encode.string
|
|
17
|
+
|
|
18
|
+
let storeEntry = (~bucket, ~prefix) =>
|
|
19
|
+
obj([("bucketName", str(bucket)), ("keyPrefix", str(prefix))])
|
|
20
|
+
|
|
21
|
+
let output = entries => Some(obj(entries))
|
|
22
|
+
|
|
23
|
+
let store = (~qualified, ~bucket, ~prefix): Reset.objectStore => {
|
|
24
|
+
let (plugin, name) = Reset.splitQualified(qualified)->Option.getOr(("", qualified))
|
|
25
|
+
{qualified, plugin, store: name, bucketName: bucket, keyPrefix: prefix}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("parseObjectStores", () => {
|
|
29
|
+
testSync("splits the qualified key into plugin and store", () =>
|
|
30
|
+
switch Reset.parseObjectStores(
|
|
31
|
+
output([("Catalog.productImages", storeEntry(~bucket="alpha-stores", ~prefix="productImages"))]),
|
|
32
|
+
) {
|
|
33
|
+
| Ok([s]) =>
|
|
34
|
+
expect((s.plugin, s.store, s.bucketName, s.keyPrefix))->toEqual((
|
|
35
|
+
"Catalog",
|
|
36
|
+
"productImages",
|
|
37
|
+
"alpha-stores",
|
|
38
|
+
"productImages",
|
|
39
|
+
))
|
|
40
|
+
| other => fail(`expected one parsed store, got ${other->JSON.stringifyAny->Option.getOr("?")}`)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// A store name may contain a dot; a registered plugin name may not — so the
|
|
45
|
+
// split is at the first one, matching how the key is composed.
|
|
46
|
+
testSync("splits at the first dot only", () =>
|
|
47
|
+
switch Reset.splitQualified("Catalog.product.images") {
|
|
48
|
+
| Some((plugin, name)) => expect((plugin, name))->toEqual(("Catalog", "product.images"))
|
|
49
|
+
| None => fail("expected a split")
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// A platform that declares no stores is ordinary, not an error.
|
|
54
|
+
testSync("absent output yields no stores", () =>
|
|
55
|
+
switch Reset.parseObjectStores(None) {
|
|
56
|
+
| Ok(stores) => expect(stores->Array.length)->toBe(0)
|
|
57
|
+
| Error(message) => fail(message)
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// A store the reset cannot read is a store it would silently leave behind, so
|
|
62
|
+
// a malformed entry fails the run rather than being skipped.
|
|
63
|
+
testSync("a malformed entry is an error, not a skip", () =>
|
|
64
|
+
switch Reset.parseObjectStores(output([("Catalog.productImages", obj([("keyPrefix", str("x"))]))])) {
|
|
65
|
+
| Ok(_) => fail("expected a malformed entry to be refused")
|
|
66
|
+
| Error(message) => expect(message->String.includes("Catalog.productImages"))->toBe(true)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
testSync("an unqualified key is an error", () =>
|
|
71
|
+
switch Reset.parseObjectStores(
|
|
72
|
+
output([("productImages", storeEntry(~bucket="alpha-stores", ~prefix="productImages"))]),
|
|
73
|
+
) {
|
|
74
|
+
| Ok(_) => fail("expected an unqualified key to be refused")
|
|
75
|
+
| Error(message) => expect(message->String.includes("productImages"))->toBe(true)
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("validateStores", () => {
|
|
81
|
+
testSync("distinct prefixes in one bucket are fine", () =>
|
|
82
|
+
expect(
|
|
83
|
+
Reset.validateStores([
|
|
84
|
+
store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix="productImages"),
|
|
85
|
+
store(~qualified="Ordering.labels", ~bucket="alpha-stores", ~prefix="labels"),
|
|
86
|
+
]),
|
|
87
|
+
)->toEqual(Ok())
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// The cross-plugin collision: one prefix, two owners, nothing to tell their
|
|
91
|
+
// objects apart.
|
|
92
|
+
testSync("two plugins on one prefix are refused", () =>
|
|
93
|
+
switch Reset.validateStores([
|
|
94
|
+
store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix="productImages"),
|
|
95
|
+
store(~qualified="Ordering.productImages", ~bucket="alpha-stores", ~prefix="productImages"),
|
|
96
|
+
]) {
|
|
97
|
+
| Ok() => fail("expected a collision to be refused")
|
|
98
|
+
| Error(message) =>
|
|
99
|
+
expect(
|
|
100
|
+
message->String.includes("Catalog.productImages") &&
|
|
101
|
+
message->String.includes("Ordering.productImages"),
|
|
102
|
+
)->toBe(true)
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// The same shape one level up: `Catalog` encloses `Catalog/productImages`, so
|
|
107
|
+
// wiping the first would delete the second's objects.
|
|
108
|
+
testSync("an enclosing prefix is refused", () =>
|
|
109
|
+
switch Reset.validateStores([
|
|
110
|
+
store(~qualified="Legacy.catalog", ~bucket="alpha-stores", ~prefix="Catalog"),
|
|
111
|
+
store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix="Catalog/productImages"),
|
|
112
|
+
]) {
|
|
113
|
+
| Ok() => fail("expected an enclosing prefix to be refused")
|
|
114
|
+
| Error(message) => expect(message->String.includes("encloses"))->toBe(true)
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Different buckets cannot reach each other, so an equal prefix there is not a
|
|
119
|
+
// collision — which is what the per-store layout produces in production.
|
|
120
|
+
testSync("one prefix in two different buckets is fine", () =>
|
|
121
|
+
expect(
|
|
122
|
+
Reset.validateStores([
|
|
123
|
+
store(~qualified="Catalog.productImages", ~bucket="catalog-productImages", ~prefix="productImages"),
|
|
124
|
+
store(~qualified="Ordering.productImages", ~bucket="ordering-productImages", ~prefix="productImages"),
|
|
125
|
+
]),
|
|
126
|
+
)->toEqual(Ok())
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// A shared prefix that merely starts with another's characters is not nesting:
|
|
130
|
+
// the delete prefix carries a trailing slash, so `images` cannot reach
|
|
131
|
+
// `images-archive`.
|
|
132
|
+
testSync("a sibling prefix sharing a character run is fine", () =>
|
|
133
|
+
expect(
|
|
134
|
+
Reset.validateStores([
|
|
135
|
+
store(~qualified="Catalog.images", ~bucket="alpha-stores", ~prefix="images"),
|
|
136
|
+
store(~qualified="Catalog.imagesArchive", ~bucket="alpha-stores", ~prefix="images-archive"),
|
|
137
|
+
]),
|
|
138
|
+
)->toEqual(Ok())
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
testSync("an empty or slash-bearing store name is refused", () => {
|
|
142
|
+
switch Reset.validateStores([
|
|
143
|
+
store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix=""),
|
|
144
|
+
]) {
|
|
145
|
+
| Ok() => fail("expected an empty prefix to be refused")
|
|
146
|
+
| Error(_) => ()
|
|
147
|
+
}
|
|
148
|
+
switch Reset.validateStores([
|
|
149
|
+
store(~qualified="Catalog.a/b", ~bucket="alpha-stores", ~prefix="a/b"),
|
|
150
|
+
]) {
|
|
151
|
+
| Ok() => fail("expected a store name containing a slash to be refused")
|
|
152
|
+
| Error(_) => ()
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe("pluginOf", () => {
|
|
158
|
+
// The `objectStores` keys carry the REGISTERED plugin name; the menu carries
|
|
159
|
+
// the operator-facing label. Attribution uses the declared name so the reset
|
|
160
|
+
// never has to guess the relation between the two.
|
|
161
|
+
testSync("prefers the declared plugin name over the label", () =>
|
|
162
|
+
expect(
|
|
163
|
+
Reset.pluginOf({projectDir: "../catalog-aws", label: "catalog", group: Domain, plugin: "Catalog"}),
|
|
164
|
+
)->toBe("Catalog")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
testSync("falls back to the label when no plugin name is declared", () =>
|
|
168
|
+
expect(Reset.pluginOf({projectDir: ".", label: "platform", group: Platform}))->toBe("platform")
|
|
169
|
+
)
|
|
170
|
+
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|