@owlmeans/bot 0.1.18-rc.2 → 0.1.18
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/README.md +23 -21
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/bot/SKILL.md +68 -46
- package/build/consts.d.ts +6 -31
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +10 -36
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +3 -1
- package/build/consts.local.d.ts.map +1 -1
- package/build/consts.local.js +3 -1
- package/build/consts.local.js.map +1 -1
- package/build/helpers/index.d.ts +2 -2
- package/build/helpers/index.js +1 -1
- package/build/helpers/text/types.d.ts +35 -0
- package/build/helpers/text/types.d.ts.map +1 -0
- package/build/helpers/{rule → text}/types.js.map +1 -1
- package/build/helpers/text.d.ts +4 -0
- package/build/helpers/text.d.ts.map +1 -0
- package/build/helpers/text.js +77 -0
- package/build/helpers/text.js.map +1 -0
- package/build/protocols/types.d.ts +2 -17
- package/build/protocols/types.d.ts.map +1 -1
- package/build/protocols.d.ts +1 -1
- package/build/protocols.d.ts.map +1 -1
- package/build/protocols.js +2 -7
- package/build/protocols.js.map +1 -1
- package/build/schemas.d.ts +2 -4
- package/build/schemas.d.ts.map +1 -1
- package/build/schemas.js +3 -31
- package/build/schemas.js.map +1 -1
- package/build/types.d.ts +7 -41
- package/build/types.d.ts.map +1 -1
- package/build/utils/aliases/types.d.ts +0 -5
- package/build/utils/aliases/types.d.ts.map +1 -1
- package/build/utils/aliases.d.ts.map +1 -1
- package/build/utils/aliases.js +0 -5
- package/build/utils/aliases.js.map +1 -1
- package/package.json +7 -7
- package/src/consts.local.ts +4 -1
- package/src/consts.ts +6 -37
- package/src/helpers/index.ts +2 -2
- package/src/helpers/text/types.ts +35 -0
- package/src/helpers/text.ts +91 -0
- package/src/protocols/types.ts +2 -12
- package/src/protocols.ts +4 -21
- package/src/schemas.ts +5 -39
- package/src/types.ts +7 -45
- package/src/utils/aliases/types.ts +0 -1
- package/src/utils/aliases.ts +0 -5
- package/tests/protocols.spec.ts +3 -7
- package/tests/schemas.spec.ts +2 -30
- package/tests/text.spec.ts +101 -0
- package/tests/update.spec.ts +4 -10
- package/build/helpers/rule/types.d.ts +0 -26
- package/build/helpers/rule/types.d.ts.map +0 -1
- package/build/helpers/rule.d.ts +0 -4
- package/build/helpers/rule.d.ts.map +0 -1
- package/build/helpers/rule.js +0 -71
- package/build/helpers/rule.js.map +0 -1
- package/src/helpers/rule/types.ts +0 -26
- package/src/helpers/rule.ts +0 -89
- package/tests/rule.spec.ts +0 -129
- /package/build/helpers/{rule → text}/types.js +0 -0
package/build/helpers/rule.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { BOT_KEYWORD_MAX, BOT_KEYWORDS_MAX, BotMatchMode } from '../consts.js';
|
|
2
|
-
import { BOT_SPACE_PATTERN, BOT_TOKEN_PATTERN } from '../consts.local.js';
|
|
3
|
-
export const createBotRuleHelper = () => {
|
|
4
|
-
/**
|
|
5
|
-
* NFKC, then a case fold: upper-casing first maps what lower-casing alone keeps apart (`ß` → `SS`
|
|
6
|
-
* → `ss`, final `ς` → `Σ` → `σ`), and NFKC again recomposes what the case mapping decomposed.
|
|
7
|
-
*/
|
|
8
|
-
const fold = (text) => text.normalize('NFKC').toUpperCase().toLowerCase().normalize('NFKC');
|
|
9
|
-
const tokensOf = (text) => fold(text).match(BOT_TOKEN_PATTERN) ?? [];
|
|
10
|
-
const spacedOf = (text) => fold(text).replace(BOT_SPACE_PATTERN, ' ').trim();
|
|
11
|
-
const hasPhrase = (tokens, phrase) => {
|
|
12
|
-
for (let at = 0; at + phrase.length <= tokens.length; at++) {
|
|
13
|
-
if (phrase.every((token, offset) => tokens[at + offset] === token)) {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return false;
|
|
18
|
-
};
|
|
19
|
-
const match = (text, rules) => {
|
|
20
|
-
if (typeof text !== 'string' || text.trim() === '') {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
// Folded once per message, never per rule.
|
|
24
|
-
let tokens;
|
|
25
|
-
let spaced;
|
|
26
|
-
const matchesKeyword = (mode, keyword) => {
|
|
27
|
-
if (mode === BotMatchMode.Contains) {
|
|
28
|
-
const needle = spacedOf(keyword);
|
|
29
|
-
spaced ??= spacedOf(text);
|
|
30
|
-
return needle !== '' && spaced.includes(needle);
|
|
31
|
-
}
|
|
32
|
-
const phrase = tokensOf(keyword);
|
|
33
|
-
tokens ??= tokensOf(text);
|
|
34
|
-
return phrase.length > 0 && hasPhrase(tokens, phrase);
|
|
35
|
-
};
|
|
36
|
-
return rules
|
|
37
|
-
.filter(rule => rule.enabled)
|
|
38
|
-
.map((rule) => ({
|
|
39
|
-
rule,
|
|
40
|
-
keywords: [...new Set(rule.keywords.filter(keyword => typeof keyword === 'string' && matchesKeyword(rule.mode ?? BotMatchMode.Word, keyword)))],
|
|
41
|
-
}))
|
|
42
|
-
.filter(found => found.keywords.length > 0)
|
|
43
|
-
// Array#sort is stable: rules of equal order keep the order they were given in.
|
|
44
|
-
.sort((left, right) => (left.rule.order ?? 0) - (right.rule.order ?? 0));
|
|
45
|
-
};
|
|
46
|
-
const normalizeKeywords = (list) => {
|
|
47
|
-
const seen = new Set();
|
|
48
|
-
const result = [];
|
|
49
|
-
for (const raw of list ?? []) {
|
|
50
|
-
if (typeof raw !== 'string') {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const keyword = Array.from(raw.replace(BOT_SPACE_PATTERN, ' ').trim())
|
|
54
|
-
.slice(0, BOT_KEYWORD_MAX).join('').trim();
|
|
55
|
-
const key = spacedOf(keyword);
|
|
56
|
-
if (keyword === '' || seen.has(key)) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
seen.add(key);
|
|
60
|
-
result.push(keyword);
|
|
61
|
-
if (result.length >= BOT_KEYWORDS_MAX) {
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return result;
|
|
66
|
-
};
|
|
67
|
-
const appliesTo = (rule, connection) => rule.connection == null || rule.connection === connection;
|
|
68
|
-
return { match, normalizeKeywords, appliesTo };
|
|
69
|
-
};
|
|
70
|
-
export const botRuleHelper = createBotRuleHelper();
|
|
71
|
-
//# sourceMappingURL=rule.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rule.js","sourceRoot":"","sources":["../../src/helpers/rule.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { BotMatch, BotRule } from '../../types.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* How a message is matched against keyword groups — the ONE implementation the server's dispatch,
|
|
5
|
-
* the panels and the tests share, so a rule means the same thing everywhere.
|
|
6
|
-
*/
|
|
7
|
-
export interface BotRuleHelper {
|
|
8
|
-
/**
|
|
9
|
-
* The enabled rules whose keywords the text matches, ordered by `order` (ties keep the given
|
|
10
|
-
* order), each with the keywords (as stored) that matched.
|
|
11
|
-
*
|
|
12
|
-
* Both sides are folded the same way: NFKC, then case-folded (`ß` ≡ `ss`, `Ё` ≡ `ё`), accents
|
|
13
|
-
* kept. `word` compares whole tokens — runs of letters, combining marks, digits and `_` — and a
|
|
14
|
-
* multi-word keyword is a PHRASE that must appear as consecutive tokens; `contains` is a substring
|
|
15
|
-
* of the folded text with whitespace runs collapsed. Disabled rules never match.
|
|
16
|
-
*/
|
|
17
|
-
match: (text: string, rules: readonly BotRule[]) => BotMatch[]
|
|
18
|
-
/**
|
|
19
|
-
* A keyword list as it is stored: each trimmed with inner whitespace collapsed, cut to
|
|
20
|
-
* `BOT_KEYWORD_MAX` code points, empties dropped, duplicates (by their folded form) dropped keeping
|
|
21
|
-
* the first spelling, at most `BOT_KEYWORDS_MAX` kept.
|
|
22
|
-
*/
|
|
23
|
-
normalizeKeywords: (list: readonly string[]) => string[]
|
|
24
|
-
/** Whether a rule applies to a connection: its own, or a scope-wide one (`connection` null/absent). */
|
|
25
|
-
appliesTo: (rule: Pick<BotRule, 'connection'>, connection: string) => boolean
|
|
26
|
-
}
|
package/src/helpers/rule.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { BOT_KEYWORD_MAX, BOT_KEYWORDS_MAX, BotMatchMode } from '../consts.js'
|
|
2
|
-
import { BOT_SPACE_PATTERN, BOT_TOKEN_PATTERN } from '../consts.local.js'
|
|
3
|
-
import type { BotMatch, BotRule } from '../types.js'
|
|
4
|
-
import type { BotRuleHelper } from './rule/types.js'
|
|
5
|
-
|
|
6
|
-
export const createBotRuleHelper = (): BotRuleHelper => {
|
|
7
|
-
/**
|
|
8
|
-
* NFKC, then a case fold: upper-casing first maps what lower-casing alone keeps apart (`ß` → `SS`
|
|
9
|
-
* → `ss`, final `ς` → `Σ` → `σ`), and NFKC again recomposes what the case mapping decomposed.
|
|
10
|
-
*/
|
|
11
|
-
const fold = (text: string): string =>
|
|
12
|
-
text.normalize('NFKC').toUpperCase().toLowerCase().normalize('NFKC')
|
|
13
|
-
|
|
14
|
-
const tokensOf = (text: string): string[] => fold(text).match(BOT_TOKEN_PATTERN) ?? []
|
|
15
|
-
|
|
16
|
-
const spacedOf = (text: string): string => fold(text).replace(BOT_SPACE_PATTERN, ' ').trim()
|
|
17
|
-
|
|
18
|
-
const hasPhrase = (tokens: readonly string[], phrase: readonly string[]): boolean => {
|
|
19
|
-
for (let at = 0; at + phrase.length <= tokens.length; at++) {
|
|
20
|
-
if (phrase.every((token, offset) => tokens[at + offset] === token)) {
|
|
21
|
-
return true
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return false
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const match = (text: string, rules: readonly BotRule[]): BotMatch[] => {
|
|
28
|
-
if (typeof text !== 'string' || text.trim() === '') {
|
|
29
|
-
return []
|
|
30
|
-
}
|
|
31
|
-
// Folded once per message, never per rule.
|
|
32
|
-
let tokens: string[] | undefined
|
|
33
|
-
let spaced: string | undefined
|
|
34
|
-
|
|
35
|
-
const matchesKeyword = (mode: BotMatchMode, keyword: string): boolean => {
|
|
36
|
-
if (mode === BotMatchMode.Contains) {
|
|
37
|
-
const needle = spacedOf(keyword)
|
|
38
|
-
spaced ??= spacedOf(text)
|
|
39
|
-
return needle !== '' && spaced.includes(needle)
|
|
40
|
-
}
|
|
41
|
-
const phrase = tokensOf(keyword)
|
|
42
|
-
tokens ??= tokensOf(text)
|
|
43
|
-
return phrase.length > 0 && hasPhrase(tokens, phrase)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return rules
|
|
47
|
-
.filter(rule => rule.enabled)
|
|
48
|
-
.map((rule): BotMatch => ({
|
|
49
|
-
rule,
|
|
50
|
-
keywords: [...new Set(rule.keywords.filter(
|
|
51
|
-
keyword => typeof keyword === 'string' && matchesKeyword(rule.mode ?? BotMatchMode.Word, keyword)
|
|
52
|
-
))],
|
|
53
|
-
}))
|
|
54
|
-
.filter(found => found.keywords.length > 0)
|
|
55
|
-
// Array#sort is stable: rules of equal order keep the order they were given in.
|
|
56
|
-
.sort((left, right) => (left.rule.order ?? 0) - (right.rule.order ?? 0))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const normalizeKeywords = (list: readonly string[]): string[] => {
|
|
60
|
-
const seen = new Set<string>()
|
|
61
|
-
const result: string[] = []
|
|
62
|
-
|
|
63
|
-
for (const raw of list ?? []) {
|
|
64
|
-
if (typeof raw !== 'string') {
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
const keyword = Array.from(raw.replace(BOT_SPACE_PATTERN, ' ').trim())
|
|
68
|
-
.slice(0, BOT_KEYWORD_MAX).join('').trim()
|
|
69
|
-
const key = spacedOf(keyword)
|
|
70
|
-
if (keyword === '' || seen.has(key)) {
|
|
71
|
-
continue
|
|
72
|
-
}
|
|
73
|
-
seen.add(key)
|
|
74
|
-
result.push(keyword)
|
|
75
|
-
if (result.length >= BOT_KEYWORDS_MAX) {
|
|
76
|
-
break
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return result
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const appliesTo = (rule: Pick<BotRule, 'connection'>, connection: string): boolean =>
|
|
84
|
-
rule.connection == null || rule.connection === connection
|
|
85
|
-
|
|
86
|
-
return { match, normalizeKeywords, appliesTo }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const botRuleHelper = createBotRuleHelper()
|
package/tests/rule.spec.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { BOT_KEYWORD_MAX, BOT_KEYWORDS_MAX, BotMatchMode, BotRuleAction, botRuleHelper, type BotRule } from '../src/index.js'
|
|
3
|
-
|
|
4
|
-
const at = '2026-10-26T10:00:00.000Z'
|
|
5
|
-
|
|
6
|
-
const rule = (id: string, keywords: string[], extra: Partial<BotRule> = {}): BotRule => ({
|
|
7
|
-
id,
|
|
8
|
-
scope: 'org-1',
|
|
9
|
-
connection: null,
|
|
10
|
-
name: id,
|
|
11
|
-
keywords,
|
|
12
|
-
mode: BotMatchMode.Word,
|
|
13
|
-
action: BotRuleAction.Delete,
|
|
14
|
-
enabled: true,
|
|
15
|
-
order: 0,
|
|
16
|
-
createdAt: at,
|
|
17
|
-
updatedAt: at,
|
|
18
|
-
...extra,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const ids = (text: string, rules: BotRule[]): string[] => botRuleHelper.match(text, rules).map(found => found.rule.id)
|
|
22
|
-
|
|
23
|
-
describe('botRuleHelper.match — word mode', () => {
|
|
24
|
-
test('matches whole words only, case-insensitively and across punctuation', () => {
|
|
25
|
-
const spam = rule('spam', ['casino'])
|
|
26
|
-
|
|
27
|
-
expect(ids('Visit the CASINO!', [spam])).toEqual(['spam'])
|
|
28
|
-
expect(ids('casino, now', [spam])).toEqual(['spam'])
|
|
29
|
-
expect(ids('casinos are fun', [spam])).toEqual([])
|
|
30
|
-
expect(ids('megacasino', [spam])).toEqual([])
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('a multi-word keyword is a phrase of consecutive words', () => {
|
|
34
|
-
const offer = rule('offer', ['free money'])
|
|
35
|
-
|
|
36
|
-
expect(ids('Get FREE money now', [offer])).toEqual(['offer'])
|
|
37
|
-
expect(ids('free—money', [offer])).toEqual(['offer'])
|
|
38
|
-
expect(ids('money for free', [offer])).toEqual([])
|
|
39
|
-
expect(ids('free real money', [offer])).toEqual([])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('is Unicode-aware: Cyrillic, accents, compatibility forms and the German sharp s', () => {
|
|
43
|
-
expect(ids('Купите СПАМ сейчас', [rule('ru', ['спам'])])).toEqual(['ru'])
|
|
44
|
-
expect(ids('ёлка', [rule('yo', ['ЁЛКА'])])).toEqual(['yo'])
|
|
45
|
-
expect(ids('Un café, por favor', [rule('es', ['CAFÉ'])])).toEqual(['es'])
|
|
46
|
-
// Accents are kept: "cafe" is not "café".
|
|
47
|
-
expect(ids('Un café', [rule('plain', ['cafe'])])).toEqual([])
|
|
48
|
-
// NFKC: the full-width form and the ligature fold onto plain letters; NFD text onto NFC.
|
|
49
|
-
expect(ids('CASINO night', [rule('wide', ['casino'])])).toEqual(['wide'])
|
|
50
|
-
expect(ids('finance tips', [rule('lig', ['finance'])])).toEqual(['lig'])
|
|
51
|
-
expect(ids('café au lait', [rule('nfd', ['café'])])).toEqual(['nfd'])
|
|
52
|
-
expect(ids('STRASSE gesperrt', [rule('de', ['straße'])])).toEqual(['de'])
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('a keyword with no letters or digits never matches a word', () => {
|
|
56
|
-
expect(ids('!!! now', [rule('bang', ['!!!'])])).toEqual([])
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe('botRuleHelper.match — contains mode', () => {
|
|
61
|
-
test('matches a folded substring, inside words too, with whitespace runs collapsed', () => {
|
|
62
|
-
const inside = rule('inside', ['casino'], { mode: BotMatchMode.Contains })
|
|
63
|
-
const punct = rule('punct', ['!!!'], { mode: BotMatchMode.Contains })
|
|
64
|
-
const spaced = rule('spaced', ['free money'], { mode: BotMatchMode.Contains })
|
|
65
|
-
|
|
66
|
-
expect(ids('MEGACASINO.com', [inside])).toEqual(['inside'])
|
|
67
|
-
expect(ids('wow!!! ok', [punct])).toEqual(['punct'])
|
|
68
|
-
expect(ids('get free\n money', [spaced])).toEqual(['spaced'])
|
|
69
|
-
expect(ids('no match here', [inside, punct, spaced])).toEqual([])
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('botRuleHelper.match — groups', () => {
|
|
74
|
-
test('skips disabled rules and answers [] for empty text', () => {
|
|
75
|
-
const off = rule('off', ['spam'], { enabled: false })
|
|
76
|
-
|
|
77
|
-
expect(ids('spam', [off])).toEqual([])
|
|
78
|
-
expect(ids(' ', [rule('on', ['spam'])])).toEqual([])
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('orders matches by rule order, ties keeping the given order', () => {
|
|
82
|
-
const rules = [
|
|
83
|
-
rule('third', ['spam'], { order: 3 }),
|
|
84
|
-
rule('first', ['spam'], { order: 1 }),
|
|
85
|
-
rule('tie-a', ['spam'], { order: 2 }),
|
|
86
|
-
rule('tie-b', ['spam'], { order: 2 }),
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
expect(ids('spam', rules)).toEqual(['first', 'tie-a', 'tie-b', 'third'])
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('reports every group a message matches, each with its own matched keywords as stored', () => {
|
|
93
|
-
const abuse = rule('abuse', ['Idiot', 'stupid', 'moron'], { order: 1, action: BotRuleAction.Warn })
|
|
94
|
-
const hours = rule('hours', ['opening hours', 'open'], { order: 2, action: BotRuleAction.Reply })
|
|
95
|
-
const quiet = rule('quiet', ['delivery'], { order: 0 })
|
|
96
|
-
const found = botRuleHelper.match('You idiot, what are the OPENING hours? stupid!', [hours, quiet, abuse])
|
|
97
|
-
|
|
98
|
-
expect(found.map(match => [match.rule.id, match.keywords])).toEqual([
|
|
99
|
-
['abuse', ['Idiot', 'stupid']],
|
|
100
|
-
['hours', ['opening hours']],
|
|
101
|
-
])
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
describe('botRuleHelper.normalizeKeywords', () => {
|
|
106
|
-
test('trims, collapses inner whitespace, drops empties and folded duplicates keeping the first spelling', () => {
|
|
107
|
-
expect(botRuleHelper.normalizeKeywords([' Free money ', '', ' ', 'free MONEY', 'Спам', 'СПАМ', 'café']))
|
|
108
|
-
.toEqual(['Free money', 'Спам', 'café'])
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('caps the keyword length (in code points) and the list size', () => {
|
|
112
|
-
const long = '😀'.repeat(BOT_KEYWORD_MAX + 10)
|
|
113
|
-
const [capped] = botRuleHelper.normalizeKeywords([long])
|
|
114
|
-
const many = botRuleHelper.normalizeKeywords(Array.from({ length: BOT_KEYWORDS_MAX + 5 }, (_, n) => `kw${n}`))
|
|
115
|
-
|
|
116
|
-
expect(Array.from(capped)).toHaveLength(BOT_KEYWORD_MAX)
|
|
117
|
-
expect(many).toHaveLength(BOT_KEYWORDS_MAX)
|
|
118
|
-
expect(many[0]).toBe('kw0')
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
describe('botRuleHelper.appliesTo', () => {
|
|
123
|
-
test('a scope-wide rule applies to every connection, an own one to its connection only', () => {
|
|
124
|
-
expect(botRuleHelper.appliesTo({ connection: null }, 'c1')).toBe(true)
|
|
125
|
-
expect(botRuleHelper.appliesTo({}, 'c1')).toBe(true)
|
|
126
|
-
expect(botRuleHelper.appliesTo({ connection: 'c1' }, 'c1')).toBe(true)
|
|
127
|
-
expect(botRuleHelper.appliesTo({ connection: 'c2' }, 'c1')).toBe(false)
|
|
128
|
-
})
|
|
129
|
-
})
|
|
File without changes
|