@aigne/doc-smith 0.2.6 → 0.2.9
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 +26 -0
- package/agents/check-detail-result.mjs +2 -7
- package/agents/check-detail.mjs +4 -6
- package/agents/check-structure-plan.mjs +5 -10
- package/agents/find-item-by-path.mjs +13 -31
- package/agents/input-generator.mjs +15 -35
- package/agents/language-selector.mjs +6 -18
- package/agents/load-config.mjs +2 -2
- package/agents/load-sources.mjs +29 -117
- package/agents/publish-docs.mjs +15 -28
- package/agents/save-docs.mjs +8 -20
- package/agents/save-output.mjs +2 -9
- package/agents/save-single-doc.mjs +2 -2
- package/agents/schema/structure-plan.yaml +1 -1
- package/agents/transform-detail-datasources.mjs +2 -5
- package/biome.json +13 -3
- package/docs-mcp/get-docs-structure.mjs +1 -1
- package/docs-mcp/read-doc-content.mjs +1 -4
- package/package.json +10 -6
- package/tests/check-detail-result.test.mjs +8 -19
- package/tests/load-sources.test.mjs +65 -161
- package/tests/test-all-validation-cases.mjs +71 -37
- package/tests/test-save-docs.mjs +6 -17
- package/utils/constants.mjs +1 -2
- package/utils/file-utils.mjs +205 -0
- package/utils/markdown-checker.mjs +124 -57
- package/utils/mermaid-validator.mjs +5 -10
- package/utils/mermaid-worker-pool.mjs +7 -11
- package/utils/mermaid-worker.mjs +8 -17
- package/utils/utils.mjs +52 -104
package/agents/publish-docs.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { joinURL } from "ufo";
|
|
3
|
-
import open from "open";
|
|
4
|
-
import { publishDocs as publishDocsFn } from "@aigne/publish-docs";
|
|
5
|
-
import { createConnect } from "@aigne/cli/utils/load-aigne.js";
|
|
6
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
7
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
8
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import { createConnect } from "@aigne/aigne-hub";
|
|
6
|
+
import { publishDocs as publishDocsFn } from "@aigne/publish-docs";
|
|
7
|
+
import open from "open";
|
|
8
|
+
import { joinURL } from "ufo";
|
|
9
9
|
import { parse, stringify } from "yaml";
|
|
10
|
-
import { basename } from "node:path";
|
|
11
10
|
import { loadConfigFromFile, saveValueToConfig } from "../utils/utils.mjs";
|
|
12
11
|
|
|
13
12
|
const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
|
|
@@ -19,11 +18,7 @@ const DEFAULT_APP_URL = "https://docsmith.aigne.io";
|
|
|
19
18
|
* @returns {Promise<string>} - The access token
|
|
20
19
|
*/
|
|
21
20
|
async function getAccessToken(appUrl) {
|
|
22
|
-
const DOC_SMITH_ENV_FILE = join(
|
|
23
|
-
homedir(),
|
|
24
|
-
".aigne",
|
|
25
|
-
"doc-smith-connected.yaml"
|
|
26
|
-
);
|
|
21
|
+
const DOC_SMITH_ENV_FILE = join(homedir(), ".aigne", "doc-smith-connected.yaml");
|
|
27
22
|
const { hostname } = new URL(appUrl);
|
|
28
23
|
|
|
29
24
|
let accessToken = process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN;
|
|
@@ -35,7 +30,7 @@ async function getAccessToken(appUrl) {
|
|
|
35
30
|
const data = await readFile(DOC_SMITH_ENV_FILE, "utf8");
|
|
36
31
|
if (data.includes("DOC_DISCUSS_KIT_ACCESS_TOKEN")) {
|
|
37
32
|
const envs = parse(data);
|
|
38
|
-
if (envs[hostname]
|
|
33
|
+
if (envs[hostname]?.DOC_DISCUSS_KIT_ACCESS_TOKEN) {
|
|
39
34
|
accessToken = envs[hostname].DOC_DISCUSS_KIT_ACCESS_TOKEN;
|
|
40
35
|
}
|
|
41
36
|
}
|
|
@@ -48,10 +43,7 @@ async function getAccessToken(appUrl) {
|
|
|
48
43
|
// If still no access token, prompt user to authorize
|
|
49
44
|
if (!accessToken) {
|
|
50
45
|
const DISCUSS_KIT_URL = appUrl;
|
|
51
|
-
const connectUrl = joinURL(
|
|
52
|
-
new URL(DISCUSS_KIT_URL).origin,
|
|
53
|
-
WELLKNOWN_SERVICE_PATH_PREFIX
|
|
54
|
-
);
|
|
46
|
+
const connectUrl = joinURL(new URL(DISCUSS_KIT_URL).origin, WELLKNOWN_SERVICE_PATH_PREFIX);
|
|
55
47
|
|
|
56
48
|
try {
|
|
57
49
|
const result = await createConnect({
|
|
@@ -60,8 +52,7 @@ async function getAccessToken(appUrl) {
|
|
|
60
52
|
source: `AIGNE DocSmith connect to Discuss Kit`,
|
|
61
53
|
closeOnSuccess: true,
|
|
62
54
|
appName: "AIGNE DocSmith",
|
|
63
|
-
appLogo:
|
|
64
|
-
"https://www.aigne.io/image-bin/uploads/a7910a71364ee15a27e86f869ad59009.svg",
|
|
55
|
+
appLogo: "https://www.aigne.io/image-bin/uploads/a7910a71364ee15a27e86f869ad59009.svg",
|
|
65
56
|
openPage: (pageUrl) => open(pageUrl),
|
|
66
57
|
});
|
|
67
58
|
|
|
@@ -86,12 +77,12 @@ async function getAccessToken(appUrl) {
|
|
|
86
77
|
DOC_DISCUSS_KIT_ACCESS_TOKEN: accessToken,
|
|
87
78
|
DOC_DISCUSS_KIT_URL: DISCUSS_KIT_URL,
|
|
88
79
|
},
|
|
89
|
-
})
|
|
80
|
+
}),
|
|
90
81
|
);
|
|
91
82
|
} catch (error) {
|
|
92
83
|
console.error("Failed to get access token:", error);
|
|
93
84
|
throw new Error(
|
|
94
|
-
"Failed to obtain access token. Please check your network connection and try again later."
|
|
85
|
+
"Failed to obtain access token. Please check your network connection and try again later.",
|
|
95
86
|
);
|
|
96
87
|
}
|
|
97
88
|
}
|
|
@@ -101,7 +92,7 @@ async function getAccessToken(appUrl) {
|
|
|
101
92
|
|
|
102
93
|
export default async function publishDocs(
|
|
103
94
|
{ docsDir, appUrl, boardId, projectName, projectDesc, projectLogo },
|
|
104
|
-
options
|
|
95
|
+
options,
|
|
105
96
|
) {
|
|
106
97
|
// Check if DOC_DISCUSS_KIT_URL is set in environment variables
|
|
107
98
|
const envAppUrl = process.env.DOC_DISCUSS_KIT_URL;
|
|
@@ -115,7 +106,7 @@ export default async function publishDocs(
|
|
|
115
106
|
// Check if appUrl is default and not saved in config (only when not using env variable)
|
|
116
107
|
const config = await loadConfigFromFile();
|
|
117
108
|
const isDefaultAppUrl = appUrl === DEFAULT_APP_URL;
|
|
118
|
-
const hasAppUrlInConfig = config
|
|
109
|
+
const hasAppUrlInConfig = config?.appUrl;
|
|
119
110
|
|
|
120
111
|
if (!useEnvAppUrl && isDefaultAppUrl && !hasAppUrlInConfig) {
|
|
121
112
|
const choice = await options.prompts.select({
|
|
@@ -161,11 +152,7 @@ export default async function publishDocs(
|
|
|
161
152
|
};
|
|
162
153
|
|
|
163
154
|
try {
|
|
164
|
-
const {
|
|
165
|
-
success,
|
|
166
|
-
boardId: newBoardId,
|
|
167
|
-
docsUrl,
|
|
168
|
-
} = await publishDocsFn({
|
|
155
|
+
const { success, boardId: newBoardId } = await publishDocsFn({
|
|
169
156
|
sidebarPath,
|
|
170
157
|
accessToken,
|
|
171
158
|
appUrl,
|
|
@@ -189,7 +176,7 @@ export default async function publishDocs(
|
|
|
189
176
|
await saveValueToConfig(
|
|
190
177
|
"boardId",
|
|
191
178
|
newBoardId,
|
|
192
|
-
"⚠️ Warning: boardId is auto-generated by system, please do not edit manually"
|
|
179
|
+
"⚠️ Warning: boardId is auto-generated by system, please do not edit manually",
|
|
193
180
|
);
|
|
194
181
|
}
|
|
195
182
|
}
|
package/agents/save-docs.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getCurrentGitHead, saveGitHeadToConfig } from "../utils/utils.mjs";
|
|
4
3
|
import { shutdownMermaidWorkerPool } from "../utils/mermaid-worker-pool.mjs";
|
|
4
|
+
import { getCurrentGitHead, saveGitHeadToConfig } from "../utils/utils.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {Object} params
|
|
@@ -17,7 +17,7 @@ export default async function saveDocs({
|
|
|
17
17
|
locale,
|
|
18
18
|
projectInfoMessage,
|
|
19
19
|
}) {
|
|
20
|
-
const
|
|
20
|
+
const _results = [];
|
|
21
21
|
// Save current git HEAD to config.yaml for change detection
|
|
22
22
|
try {
|
|
23
23
|
const gitHead = getCurrentGitHead();
|
|
@@ -37,12 +37,7 @@ export default async function saveDocs({
|
|
|
37
37
|
|
|
38
38
|
// Clean up invalid .md files that are no longer in the structure plan
|
|
39
39
|
try {
|
|
40
|
-
await cleanupInvalidFiles(
|
|
41
|
-
structurePlan,
|
|
42
|
-
docsDir,
|
|
43
|
-
translateLanguages,
|
|
44
|
-
locale
|
|
45
|
-
);
|
|
40
|
+
await cleanupInvalidFiles(structurePlan, docsDir, translateLanguages, locale);
|
|
46
41
|
} catch (err) {
|
|
47
42
|
console.error("Failed to cleanup invalid .md files:", err.message);
|
|
48
43
|
}
|
|
@@ -112,12 +107,7 @@ function generateFileName(flatName, language) {
|
|
|
112
107
|
* @param {string} locale - Main language locale (e.g., 'en', 'zh', 'fr')
|
|
113
108
|
* @returns {Promise<Array<{ path: string, success: boolean, error?: string }>>}
|
|
114
109
|
*/
|
|
115
|
-
async function cleanupInvalidFiles(
|
|
116
|
-
structurePlan,
|
|
117
|
-
docsDir,
|
|
118
|
-
translateLanguages,
|
|
119
|
-
locale
|
|
120
|
-
) {
|
|
110
|
+
async function cleanupInvalidFiles(structurePlan, docsDir, translateLanguages, locale) {
|
|
121
111
|
const results = [];
|
|
122
112
|
|
|
123
113
|
try {
|
|
@@ -145,7 +135,7 @@ async function cleanupInvalidFiles(
|
|
|
145
135
|
|
|
146
136
|
// Find files to delete (files that are not in expectedFiles and not _sidebar.md)
|
|
147
137
|
const filesToDelete = mdFiles.filter(
|
|
148
|
-
(file) => !expectedFiles.has(file) && file !== "_sidebar.md"
|
|
138
|
+
(file) => !expectedFiles.has(file) && file !== "_sidebar.md",
|
|
149
139
|
);
|
|
150
140
|
|
|
151
141
|
// Delete invalid files
|
|
@@ -168,9 +158,7 @@ async function cleanupInvalidFiles(
|
|
|
168
158
|
}
|
|
169
159
|
|
|
170
160
|
if (filesToDelete.length > 0) {
|
|
171
|
-
console.log(
|
|
172
|
-
`Cleaned up ${filesToDelete.length} invalid .md files from ${docsDir}`
|
|
173
|
-
);
|
|
161
|
+
console.log(`Cleaned up ${filesToDelete.length} invalid .md files from ${docsDir}`);
|
|
174
162
|
}
|
|
175
163
|
} catch (err) {
|
|
176
164
|
// If docsDir doesn't exist or can't be read, that's okay
|
|
@@ -209,7 +197,7 @@ function generateSidebar(structurePlan) {
|
|
|
209
197
|
for (const key of Object.keys(node)) {
|
|
210
198
|
const item = node[key];
|
|
211
199
|
const fullSegments = [...parentSegments, key];
|
|
212
|
-
const flatFile = fullSegments.join("-")
|
|
200
|
+
const flatFile = `${fullSegments.join("-")}.md`;
|
|
213
201
|
if (item.__title) {
|
|
214
202
|
const realIndent = item.__parentId === null ? "" : indent;
|
|
215
203
|
out += `${realIndent}* [${item.__title}](/${flatFile})\n`;
|
package/agents/save-output.mjs
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
export default async function saveOutput({
|
|
5
|
-
savePath,
|
|
6
|
-
fileName,
|
|
7
|
-
saveKey,
|
|
8
|
-
...rest
|
|
9
|
-
}) {
|
|
4
|
+
export default async function saveOutput({ savePath, fileName, saveKey, ...rest }) {
|
|
10
5
|
if (!(saveKey in rest)) {
|
|
11
6
|
console.warn(`saveKey "${saveKey}" not found in input, skip saving.`);
|
|
12
7
|
return {
|
|
@@ -17,9 +12,7 @@ export default async function saveOutput({
|
|
|
17
12
|
|
|
18
13
|
const value = rest[saveKey];
|
|
19
14
|
const content =
|
|
20
|
-
typeof value === "object" && value !== null
|
|
21
|
-
? JSON.stringify(value, null, 2)
|
|
22
|
-
: String(value);
|
|
15
|
+
typeof value === "object" && value !== null ? JSON.stringify(value, null, 2) : String(value);
|
|
23
16
|
await fs.mkdir(savePath, { recursive: true });
|
|
24
17
|
const filePath = join(savePath, fileName);
|
|
25
18
|
await fs.writeFile(filePath, content, "utf8");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { saveDocWithTranslations } from "../utils/utils.mjs";
|
|
2
1
|
import { shutdownMermaidWorkerPool } from "../utils/mermaid-worker-pool.mjs";
|
|
2
|
+
import { saveDocWithTranslations } from "../utils/utils.mjs";
|
|
3
3
|
|
|
4
4
|
export default async function saveSingleDoc({
|
|
5
5
|
path,
|
|
@@ -11,7 +11,7 @@ export default async function saveSingleDoc({
|
|
|
11
11
|
isTranslate = false,
|
|
12
12
|
isShowMessage = false,
|
|
13
13
|
}) {
|
|
14
|
-
const
|
|
14
|
+
const _results = await saveDocWithTranslations({
|
|
15
15
|
path,
|
|
16
16
|
content,
|
|
17
17
|
docsDir,
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { normalizePath, toRelativePath } from "../utils/utils.mjs";
|
|
2
2
|
|
|
3
|
-
export default function transformDetailDatasources({
|
|
4
|
-
sourceIds,
|
|
5
|
-
datasourcesList,
|
|
6
|
-
}) {
|
|
3
|
+
export default function transformDetailDatasources({ sourceIds, datasourcesList }) {
|
|
7
4
|
// Build a map for fast lookup, with path normalization for compatibility
|
|
8
5
|
const dsMap = Object.fromEntries(
|
|
9
6
|
(datasourcesList || []).map((ds) => {
|
|
10
7
|
const normalizedSourceId = normalizePath(ds.sourceId);
|
|
11
8
|
return [normalizedSourceId, ds.content];
|
|
12
|
-
})
|
|
9
|
+
}),
|
|
13
10
|
);
|
|
14
11
|
|
|
15
12
|
// Collect formatted contents in order, with path normalization
|
package/biome.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.1.
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
|
3
3
|
"vcs": {
|
|
4
4
|
"enabled": true,
|
|
5
5
|
"clientKind": "git",
|
|
@@ -23,11 +23,21 @@
|
|
|
23
23
|
"noUnusedVariables": "error",
|
|
24
24
|
"noUnusedImports": "error",
|
|
25
25
|
"noUnusedFunctionParameters": "error",
|
|
26
|
-
"noUnusedPrivateClassMembers": "error"
|
|
26
|
+
"noUnusedPrivateClassMembers": "error",
|
|
27
|
+
"noUndeclaredVariables": "error",
|
|
28
|
+
"noUnreachable": "error",
|
|
29
|
+
"noSelfAssign": "error"
|
|
27
30
|
},
|
|
28
31
|
"suspicious": {
|
|
29
32
|
"noConfusingVoidType": "off",
|
|
30
|
-
"noExplicitAny": "off"
|
|
33
|
+
"noExplicitAny": "off",
|
|
34
|
+
"noDoubleEquals": "error",
|
|
35
|
+
"noGlobalAssign": "error",
|
|
36
|
+
"noAssignInExpressions": "warn"
|
|
37
|
+
},
|
|
38
|
+
"style": {
|
|
39
|
+
"useNodejsImportProtocol": "error",
|
|
40
|
+
"useTemplate": "error"
|
|
31
41
|
},
|
|
32
42
|
"complexity": {
|
|
33
43
|
"noForEach": "off",
|
|
@@ -3,10 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
const docsDir = path.join(process.cwd(), "./.aigne/doc-smith", "docs");
|
|
5
5
|
|
|
6
|
-
export default async function readDocContent({
|
|
7
|
-
relevantDocPaths,
|
|
8
|
-
docsDir: customDocsDir,
|
|
9
|
-
}) {
|
|
6
|
+
export default async function readDocContent({ relevantDocPaths, docsDir: customDocsDir }) {
|
|
10
7
|
const targetDocsDir = customDocsDir || docsDir;
|
|
11
8
|
const docContents = [];
|
|
12
9
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aigne/doc-smith",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
"author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@aigne/
|
|
16
|
-
"@aigne/
|
|
17
|
-
"@aigne/
|
|
18
|
-
"@aigne/
|
|
19
|
-
"@aigne/
|
|
15
|
+
"@aigne/aigne-hub": "^0.4.9",
|
|
16
|
+
"@aigne/anthropic": "^0.11.0",
|
|
17
|
+
"@aigne/cli": "^1.34.0",
|
|
18
|
+
"@aigne/core": "^1.49.0",
|
|
19
|
+
"@aigne/gemini": "^0.9.0",
|
|
20
|
+
"@aigne/openai": "^0.11.0",
|
|
20
21
|
"@aigne/publish-docs": "^0.5.4",
|
|
21
22
|
"chalk": "^5.5.0",
|
|
22
23
|
"dompurify": "^3.2.6",
|
|
@@ -34,6 +35,9 @@
|
|
|
34
35
|
"vfile": "^6.0.3",
|
|
35
36
|
"yaml": "^2.8.0"
|
|
36
37
|
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^2.1.4"
|
|
40
|
+
},
|
|
37
41
|
"scripts": {
|
|
38
42
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
39
43
|
"lint": "biome check && pnpm -r run lint",
|
|
@@ -23,28 +23,21 @@ async function runTests() {
|
|
|
23
23
|
const content = "This contains a [dead link](/dead-link).";
|
|
24
24
|
const result = await checkDetailResult({ structurePlan, content });
|
|
25
25
|
assert(result.isApproved === false, "Should not be approved");
|
|
26
|
-
assert(
|
|
27
|
-
result.detailFeedback.includes("Found a dead link"),
|
|
28
|
-
"Should report dead link"
|
|
29
|
-
);
|
|
26
|
+
assert(result.detailFeedback.includes("Found a dead link"), "Should report dead link");
|
|
30
27
|
console.log("✅ Test passed: should reject content with a dead link");
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
async function testRejectIncorrectTableSeparator() {
|
|
34
|
-
console.log(
|
|
35
|
-
"Testing: should reject content with incorrect table separator"
|
|
36
|
-
);
|
|
31
|
+
console.log("Testing: should reject content with incorrect table separator");
|
|
37
32
|
const structurePlan = [];
|
|
38
33
|
const content = "| Header | Header |\n| - | - |\n| Cell | Cell |";
|
|
39
34
|
const result = await checkDetailResult({ structurePlan, content });
|
|
40
35
|
assert(result.isApproved === false, "Should not be approved");
|
|
41
36
|
assert(
|
|
42
37
|
result.detailFeedback.includes("incorrect table separator"),
|
|
43
|
-
"Should report incorrect table separator"
|
|
44
|
-
);
|
|
45
|
-
console.log(
|
|
46
|
-
"✅ Test passed: should reject content with incorrect table separator"
|
|
38
|
+
"Should report incorrect table separator",
|
|
47
39
|
);
|
|
40
|
+
console.log("✅ Test passed: should reject content with incorrect table separator");
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
async function testApproveExternalLink() {
|
|
@@ -60,17 +53,13 @@ async function runTests() {
|
|
|
60
53
|
async function testRejectMultipleIssues() {
|
|
61
54
|
console.log("Testing: should reject content with multiple issues");
|
|
62
55
|
const structurePlan = [{ path: "/getting-started" }];
|
|
63
|
-
const content =
|
|
64
|
-
"This has a [dead link](/dead-link) and an incorrect table: | - |.";
|
|
56
|
+
const content = "This has a [dead link](/dead-link) and an incorrect table: | - |.";
|
|
65
57
|
const result = await checkDetailResult({ structurePlan, content });
|
|
66
58
|
assert(result.isApproved === false, "Should not be approved");
|
|
67
|
-
assert(
|
|
68
|
-
result.detailFeedback.includes("Found a dead link"),
|
|
69
|
-
"Should report dead link"
|
|
70
|
-
);
|
|
59
|
+
assert(result.detailFeedback.includes("Found a dead link"), "Should report dead link");
|
|
71
60
|
assert(
|
|
72
61
|
result.detailFeedback.includes("incorrect table separator"),
|
|
73
|
-
"Should report incorrect table separator"
|
|
62
|
+
"Should report incorrect table separator",
|
|
74
63
|
);
|
|
75
64
|
console.log("✅ Test passed: should reject content with multiple issues");
|
|
76
65
|
}
|
|
@@ -100,4 +89,4 @@ async function runTests() {
|
|
|
100
89
|
}
|
|
101
90
|
}
|
|
102
91
|
|
|
103
|
-
runTests();
|
|
92
|
+
runTests();
|