zugzbot 1.0.36 → 1.0.37
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.
|
@@ -11,6 +11,27 @@ const getRoot = (context: any) => {
|
|
|
11
11
|
return process.cwd();
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
// Helper to read the targetDir from sdd_state.json or .sdd_bootstrap.json
|
|
15
|
+
const getTargetDir = (root: string): string => {
|
|
16
|
+
try {
|
|
17
|
+
const bootstrapPath = path.resolve(root, ".openspec/.sdd_bootstrap.json")
|
|
18
|
+
if (fs.existsSync(bootstrapPath)) {
|
|
19
|
+
const data = JSON.parse(fs.readFileSync(bootstrapPath, "utf8"))
|
|
20
|
+
if (data && data.targetDir) return data.targetDir
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const statePath = path.resolve(root, ".openspec/sdd_state.json")
|
|
26
|
+
if (fs.existsSync(statePath)) {
|
|
27
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
28
|
+
if (state && state.targetDir) return state.targetDir
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
|
|
32
|
+
return "."
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
// Tool: sdd_clean_docker_environment
|
|
15
36
|
export const clean_docker_environment = tool({
|
|
16
37
|
description: "Asegura que Docker esté abierto y realiza una limpieza total y agresiva: detiene y elimina TODOS los contenedores (activos o inactivos), remueve TODAS las imágenes, volúmenes y redes para garantizar un lienzo en blanco absoluto antes de desplegar.",
|
|
@@ -100,6 +121,8 @@ export const generate_dockerfile = tool({
|
|
|
100
121
|
},
|
|
101
122
|
async execute(args, context) {
|
|
102
123
|
const root = getRoot(context)
|
|
124
|
+
const targetDir = getTargetDir(root)
|
|
125
|
+
const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
|
|
103
126
|
|
|
104
127
|
if (args.stack === "agnostic") {
|
|
105
128
|
const dockerfileAg = `FROM node:20-alpine
|
|
@@ -111,7 +134,7 @@ CMD ["node", "src/index.js"]
|
|
|
111
134
|
const filesWritten: string[] = []
|
|
112
135
|
let content = dockerfileAg
|
|
113
136
|
let targetName = "src/index.js"
|
|
114
|
-
if (fs.existsSync(path.resolve(
|
|
137
|
+
if (fs.existsSync(path.resolve(targetPath, "src/main.py"))) {
|
|
115
138
|
content = `FROM python:3.11-slim
|
|
116
139
|
WORKDIR /app
|
|
117
140
|
COPY . .
|
|
@@ -121,9 +144,9 @@ CMD ["python", "src/main.py"]
|
|
|
121
144
|
targetName = "src/main.py"
|
|
122
145
|
}
|
|
123
146
|
|
|
124
|
-
const fullPath = path.resolve(
|
|
147
|
+
const fullPath = path.resolve(targetPath, "Dockerfile")
|
|
125
148
|
fs.writeFileSync(fullPath, content, "utf8")
|
|
126
|
-
filesWritten.push(
|
|
149
|
+
filesWritten.push(path.relative(root, fullPath))
|
|
127
150
|
|
|
128
151
|
return JSON.stringify({
|
|
129
152
|
status: "SUCCESS",
|
|
@@ -140,11 +163,11 @@ CMD ["python", "src/main.py"]
|
|
|
140
163
|
let pm = "npm"
|
|
141
164
|
let installCmd = "npm ci --frozen-lockfile"
|
|
142
165
|
let buildCmd = "npm run build"
|
|
143
|
-
if (fs.existsSync(path.resolve(
|
|
166
|
+
if (fs.existsSync(path.resolve(targetPath, "pnpm-lock.yaml"))) {
|
|
144
167
|
pm = "pnpm"
|
|
145
168
|
installCmd = "pnpm install --frozen-lockfile"
|
|
146
169
|
buildCmd = "pnpm build"
|
|
147
|
-
} else if (fs.existsSync(path.resolve(
|
|
170
|
+
} else if (fs.existsSync(path.resolve(targetPath, "yarn.lock"))) {
|
|
148
171
|
pm = "yarn"
|
|
149
172
|
installCmd = "yarn install --frozen-lockfile"
|
|
150
173
|
buildCmd = "yarn build"
|
|
@@ -231,7 +254,7 @@ test-results
|
|
|
231
254
|
[".dockerignore", dockerignore],
|
|
232
255
|
["docker-compose.yml", compose],
|
|
233
256
|
] as const) {
|
|
234
|
-
const fullPath = path.resolve(
|
|
257
|
+
const fullPath = path.resolve(targetPath, name)
|
|
235
258
|
fs.writeFileSync(fullPath, content, "utf8")
|
|
236
259
|
filesWritten.push(path.relative(root, fullPath))
|
|
237
260
|
}
|
|
@@ -288,7 +311,7 @@ tests/
|
|
|
288
311
|
[".dockerignore", dockerignorePy],
|
|
289
312
|
["docker-compose.yml", composePy],
|
|
290
313
|
] as const) {
|
|
291
|
-
const fullPath = path.resolve(
|
|
314
|
+
const fullPath = path.resolve(targetPath, name)
|
|
292
315
|
fs.writeFileSync(fullPath, content, "utf8")
|
|
293
316
|
filesWritten.push(path.relative(root, fullPath))
|
|
294
317
|
}
|
|
@@ -11,6 +11,27 @@ const getRoot = (context: any) => {
|
|
|
11
11
|
return process.cwd();
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
// Helper to read the targetDir from sdd_state.json or .sdd_bootstrap.json
|
|
15
|
+
const getTargetDir = (root: string): string => {
|
|
16
|
+
try {
|
|
17
|
+
const bootstrapPath = path.resolve(root, ".openspec/.sdd_bootstrap.json")
|
|
18
|
+
if (fs.existsSync(bootstrapPath)) {
|
|
19
|
+
const data = JSON.parse(fs.readFileSync(bootstrapPath, "utf8"))
|
|
20
|
+
if (data && data.targetDir) return data.targetDir
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const statePath = path.resolve(root, ".openspec/sdd_state.json")
|
|
26
|
+
if (fs.existsSync(statePath)) {
|
|
27
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
28
|
+
if (state && state.targetDir) return state.targetDir
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
|
|
32
|
+
return "."
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
// Helper to get PID file path
|
|
15
36
|
const getPidFilePath = (root: string) => {
|
|
16
37
|
return path.resolve(root, ".openspec/dev_server.pid")
|
|
@@ -56,7 +77,8 @@ export const start_server = tool({
|
|
|
56
77
|
},
|
|
57
78
|
async execute(args, context) {
|
|
58
79
|
const root = getRoot(context)
|
|
59
|
-
const
|
|
80
|
+
const targetDir = getTargetDir(root)
|
|
81
|
+
const targetCwd = args.cwd ? path.resolve(root, args.cwd) : (targetDir === "." ? root : path.resolve(root, targetDir))
|
|
60
82
|
const pidFile = getPidFilePath(root)
|
|
61
83
|
|
|
62
84
|
try {
|
|
@@ -11,6 +11,27 @@ const getRoot = (context: any) => {
|
|
|
11
11
|
return process.cwd();
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
// Helper to read the targetDir from sdd_state.json or .sdd_bootstrap.json
|
|
15
|
+
const getTargetDir = (root: string): string => {
|
|
16
|
+
try {
|
|
17
|
+
const bootstrapPath = path.resolve(root, ".openspec/.sdd_bootstrap.json")
|
|
18
|
+
if (fs.existsSync(bootstrapPath)) {
|
|
19
|
+
const data = JSON.parse(fs.readFileSync(bootstrapPath, "utf8"))
|
|
20
|
+
if (data && data.targetDir) return data.targetDir
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const statePath = path.resolve(root, ".openspec/sdd_state.json")
|
|
26
|
+
if (fs.existsSync(statePath)) {
|
|
27
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
28
|
+
if (state && state.targetDir) return state.targetDir
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
|
|
32
|
+
return "."
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
// Helper to parse semantic errors from compiler and linter outputs (reducing raw trace log bloat for the LLM)
|
|
15
36
|
const parseSemanticErrors = (rawOutput: string, type: "eslint" | "tsc"): any[] => {
|
|
16
37
|
const parsed: any[] = []
|
|
@@ -54,8 +75,10 @@ export const quick_lint = tool({
|
|
|
54
75
|
args: {},
|
|
55
76
|
async execute(args, context) {
|
|
56
77
|
const root = getRoot(context)
|
|
78
|
+
const targetDir = getTargetDir(root)
|
|
79
|
+
const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
|
|
57
80
|
|
|
58
|
-
const pkgPath = path.resolve(
|
|
81
|
+
const pkgPath = path.resolve(targetPath, "package.json")
|
|
59
82
|
if (!fs.existsSync(pkgPath)) {
|
|
60
83
|
return JSON.stringify({
|
|
61
84
|
status: "ERROR",
|
|
@@ -63,6 +86,13 @@ export const quick_lint = tool({
|
|
|
63
86
|
}, null, 2)
|
|
64
87
|
}
|
|
65
88
|
|
|
89
|
+
const getRelPath = (fullRel: string) => {
|
|
90
|
+
if (targetDir !== "." && fullRel.startsWith(targetDir + "/")) {
|
|
91
|
+
return fullRel.slice(targetDir.length + 1)
|
|
92
|
+
}
|
|
93
|
+
return fullRel
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
let eslintFiles = "src/"
|
|
67
97
|
try {
|
|
68
98
|
const stateFile = path.resolve(root, ".openspec/sdd_state.json")
|
|
@@ -73,7 +103,9 @@ export const quick_lint = tool({
|
|
|
73
103
|
if (fs.existsSync(contractPath)) {
|
|
74
104
|
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
|
|
75
105
|
if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
|
|
76
|
-
const existingFiles = contract.files_affected
|
|
106
|
+
const existingFiles = contract.files_affected
|
|
107
|
+
.filter((f: string) => fs.existsSync(path.resolve(root, f)))
|
|
108
|
+
.map((f: string) => getRelPath(f))
|
|
77
109
|
if (existingFiles.length > 0) {
|
|
78
110
|
eslintFiles = existingFiles.join(" ")
|
|
79
111
|
}
|
|
@@ -87,7 +119,7 @@ export const quick_lint = tool({
|
|
|
87
119
|
|
|
88
120
|
try {
|
|
89
121
|
const out = execSync(`npx eslint ${eslintFiles} --quiet --max-warnings 0 2>&1 || true`, {
|
|
90
|
-
cwd:
|
|
122
|
+
cwd: targetPath,
|
|
91
123
|
encoding: "utf8",
|
|
92
124
|
timeout: 120_000,
|
|
93
125
|
})
|
|
@@ -112,9 +144,11 @@ export const shift_left_verify = tool({
|
|
|
112
144
|
args: {},
|
|
113
145
|
async execute(args, context) {
|
|
114
146
|
const root = getRoot(context)
|
|
147
|
+
const targetDir = getTargetDir(root)
|
|
148
|
+
const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
|
|
115
149
|
|
|
116
150
|
// Check if it is a Python project
|
|
117
|
-
let isPython = fs.existsSync(path.resolve(
|
|
151
|
+
let isPython = fs.existsSync(path.resolve(targetPath, "requirements.txt")) || fs.existsSync(path.resolve(targetPath, "pyproject.toml"));
|
|
118
152
|
try {
|
|
119
153
|
const stateFile = path.resolve(root, ".openspec/sdd_state.json")
|
|
120
154
|
if (fs.existsSync(stateFile)) {
|
|
@@ -133,6 +167,13 @@ export const shift_left_verify = tool({
|
|
|
133
167
|
// ignore
|
|
134
168
|
}
|
|
135
169
|
|
|
170
|
+
const getRelPath = (fullRel: string) => {
|
|
171
|
+
if (targetDir !== "." && fullRel.startsWith(targetDir + "/")) {
|
|
172
|
+
return fullRel.slice(targetDir.length + 1)
|
|
173
|
+
}
|
|
174
|
+
return fullRel
|
|
175
|
+
}
|
|
176
|
+
|
|
136
177
|
if (isPython) {
|
|
137
178
|
const resultPython: { ruff: { status: string, errors?: any[] } } = {
|
|
138
179
|
ruff: { status: "SUCCESS" }
|
|
@@ -147,7 +188,9 @@ export const shift_left_verify = tool({
|
|
|
147
188
|
if (fs.existsSync(contractPath)) {
|
|
148
189
|
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
|
|
149
190
|
if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
|
|
150
|
-
const existingFiles = contract.files_affected
|
|
191
|
+
const existingFiles = contract.files_affected
|
|
192
|
+
.filter((f: string) => fs.existsSync(path.resolve(root, f)) && f.endsWith(".py"))
|
|
193
|
+
.map((f: string) => getRelPath(f))
|
|
151
194
|
if (existingFiles.length > 0) {
|
|
152
195
|
pythonFiles = existingFiles.join(" ")
|
|
153
196
|
}
|
|
@@ -157,7 +200,7 @@ export const shift_left_verify = tool({
|
|
|
157
200
|
}
|
|
158
201
|
|
|
159
202
|
try {
|
|
160
|
-
execSync(`ruff check ${pythonFiles} --quiet`, { cwd:
|
|
203
|
+
execSync(`ruff check ${pythonFiles} --quiet`, { cwd: targetPath, stdio: "pipe", timeout: 20000 })
|
|
161
204
|
} catch (ruffErr: any) {
|
|
162
205
|
const output = ruffErr.stdout?.toString() || ruffErr.stderr?.toString() || ""
|
|
163
206
|
if (output.includes("command not found") || ruffErr.message?.includes("ENOENT") || ruffErr.status === 127) {
|
|
@@ -212,7 +255,7 @@ export const shift_left_verify = tool({
|
|
|
212
255
|
|
|
213
256
|
// 1. Run tsc --noEmit
|
|
214
257
|
try {
|
|
215
|
-
execSync("npx tsc --noEmit", { cwd:
|
|
258
|
+
execSync("npx tsc --noEmit", { cwd: targetPath, stdio: "pipe", timeout: 60000 })
|
|
216
259
|
} catch (e: any) {
|
|
217
260
|
const rawOutput = e.stdout?.toString() || e.stderr?.toString() || ""
|
|
218
261
|
const errors = parseSemanticErrors(rawOutput, "tsc")
|
|
@@ -233,7 +276,9 @@ export const shift_left_verify = tool({
|
|
|
233
276
|
if (fs.existsSync(contractPath)) {
|
|
234
277
|
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
|
|
235
278
|
if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
|
|
236
|
-
const existingFiles = contract.files_affected
|
|
279
|
+
const existingFiles = contract.files_affected
|
|
280
|
+
.filter((f: string) => fs.existsSync(path.resolve(root, f)))
|
|
281
|
+
.map((f: string) => getRelPath(f))
|
|
237
282
|
if (existingFiles.length > 0) {
|
|
238
283
|
eslintFiles = existingFiles.join(" ")
|
|
239
284
|
}
|
|
@@ -247,7 +292,7 @@ export const shift_left_verify = tool({
|
|
|
247
292
|
|
|
248
293
|
try {
|
|
249
294
|
const out = execSync(`npx eslint ${eslintFiles} --quiet 2>&1 || true`, {
|
|
250
|
-
cwd:
|
|
295
|
+
cwd: targetPath,
|
|
251
296
|
encoding: "utf8",
|
|
252
297
|
timeout: 60000,
|
|
253
298
|
})
|
|
@@ -278,10 +323,13 @@ export const shift_left_verify = tool({
|
|
|
278
323
|
|
|
279
324
|
// Tool: sdd_generate_tests
|
|
280
325
|
export const generate_tests = tool({
|
|
281
|
-
description: "Autogenera plantillas de pruebas unitarias/integración en
|
|
326
|
+
description: "Autogenera plantillas de pruebas unitarias/integración en el directorio corporativo de pruebas a partir de los escenarios de prueba descritos en el contrato activo de sdd_state.json. No pisa archivos de pruebas existentes.",
|
|
282
327
|
args: {},
|
|
283
328
|
async execute(args, context) {
|
|
284
329
|
const root = getRoot(context)
|
|
330
|
+
const targetDir = getTargetDir(root)
|
|
331
|
+
const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
|
|
332
|
+
|
|
285
333
|
const stateFile = path.resolve(root, ".openspec/sdd_state.json")
|
|
286
334
|
if (!fs.existsSync(stateFile)) {
|
|
287
335
|
return JSON.stringify({ success: false, error: "sdd_state.json no existe. Inicia una sesión SDD primero." }, null, 2)
|
|
@@ -311,48 +359,76 @@ export const generate_tests = tool({
|
|
|
311
359
|
const created: string[] = []
|
|
312
360
|
const skipped: string[] = []
|
|
313
361
|
|
|
362
|
+
const isPythonProject = fs.existsSync(path.resolve(targetPath, "requirements.txt")) || fs.existsSync(path.resolve(targetPath, "pyproject.toml"))
|
|
363
|
+
const test_dir = isPythonProject
|
|
364
|
+
? path.resolve(targetPath, "tests/unit")
|
|
365
|
+
: path.resolve(targetPath, "src/__tests__")
|
|
366
|
+
|
|
367
|
+
if (!fs.existsSync(test_dir)) {
|
|
368
|
+
fs.mkdirSync(test_dir, { recursive: true })
|
|
369
|
+
}
|
|
370
|
+
|
|
314
371
|
for (const [feature, scenarios] of Object.entries(grouped_tests)) {
|
|
315
372
|
const clean_feature = feature.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "")
|
|
316
373
|
const hasReact = scenarios.some(s => s.type === "unit" || s.type === "visual")
|
|
317
374
|
const ext = hasReact ? "tsx" : "ts"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const test_file = path.join(test_dir,
|
|
375
|
+
|
|
376
|
+
const filename = isPythonProject
|
|
377
|
+
? `test_${clean_feature}.py`
|
|
378
|
+
: `${feature}.test.${ext}`
|
|
379
|
+
const test_file = path.join(test_dir, filename)
|
|
380
|
+
const relative_test_path = path.relative(root, test_file)
|
|
323
381
|
|
|
324
382
|
if (fs.existsSync(test_file)) {
|
|
325
|
-
skipped.push(
|
|
383
|
+
skipped.push(relative_test_path)
|
|
326
384
|
continue
|
|
327
385
|
}
|
|
328
386
|
|
|
329
387
|
const lines: string[] = []
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
lines.push(
|
|
333
|
-
lines.push('import userEvent from "@testing-library/user-event";')
|
|
334
|
-
lines.push(`// import ${feature} from "@/components/blocks/${clean_feature}";`)
|
|
335
|
-
}
|
|
336
|
-
lines.push("")
|
|
337
|
-
lines.push(`describe("${feature} Tests (Contract Scenarios)", () => {`)
|
|
338
|
-
|
|
339
|
-
for (const s of scenarios) {
|
|
340
|
-
const tid = s.id || "TS-XX"
|
|
341
|
-
const name = s.name || "Test case"
|
|
342
|
-
lines.push(` // ${tid}: ${name}`)
|
|
343
|
-
lines.push(` // Given: ${s.given || ""}`)
|
|
344
|
-
lines.push(` // When: ${s.when || ""}`)
|
|
345
|
-
lines.push(` // Then: ${s.then || ""}`)
|
|
346
|
-
lines.push(` it("${tid}: ${name}", async () => {`)
|
|
347
|
-
lines.push(' // TODO: Implement actual contract assertions')
|
|
348
|
-
lines.push(' expect(true).toBe(true);')
|
|
349
|
-
lines.push(' });')
|
|
388
|
+
if (isPythonProject) {
|
|
389
|
+
lines.push("import pytest")
|
|
390
|
+
lines.push(`from src.main import app # o tu router correspondiente`)
|
|
350
391
|
lines.push("")
|
|
392
|
+
lines.push(`# tests para ${feature}`)
|
|
393
|
+
for (const s of scenarios) {
|
|
394
|
+
const tid = s.id || "TS-XX"
|
|
395
|
+
const name = s.name || "Test case"
|
|
396
|
+
const test_func_name = `test_${tid.toLowerCase().replace(/-/g, "_")}_${clean_feature.replace(/-/g, "_")}`
|
|
397
|
+
lines.push(`def ${test_func_name}():`)
|
|
398
|
+
lines.push(` # Given: ${s.given || ""}`)
|
|
399
|
+
lines.push(` # When: ${s.when || ""}`)
|
|
400
|
+
lines.push(` # Then: ${s.then || ""}`)
|
|
401
|
+
lines.push(" assert True")
|
|
402
|
+
lines.push("")
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
lines.push('import { describe, it, expect } from "vitest";')
|
|
406
|
+
if (hasReact) {
|
|
407
|
+
lines.push('import { render, screen } from "@testing-library/react";')
|
|
408
|
+
lines.push('import userEvent from "@testing-library/user-event";')
|
|
409
|
+
lines.push(`// import { ${feature} } from "@/components/blocks/${feature}";`)
|
|
410
|
+
}
|
|
411
|
+
lines.push("")
|
|
412
|
+
lines.push(`describe("${feature} Tests (Contract Scenarios)", () => {`)
|
|
413
|
+
|
|
414
|
+
for (const s of scenarios) {
|
|
415
|
+
const tid = s.id || "TS-XX"
|
|
416
|
+
const name = s.name || "Test case"
|
|
417
|
+
lines.push(` // ${tid}: ${name}`)
|
|
418
|
+
lines.push(` // Given: ${s.given || ""}`)
|
|
419
|
+
lines.push(` // When: ${s.when || ""}`)
|
|
420
|
+
lines.push(` // Then: ${s.then || ""}`)
|
|
421
|
+
lines.push(` it("${tid}: ${name}", async () => {`)
|
|
422
|
+
lines.push(' // TODO: Implement actual contract assertions')
|
|
423
|
+
lines.push(' expect(true).toBe(true);')
|
|
424
|
+
lines.push(' });')
|
|
425
|
+
lines.push("")
|
|
426
|
+
}
|
|
427
|
+
lines.push("});")
|
|
351
428
|
}
|
|
352
|
-
lines.push("});")
|
|
353
429
|
|
|
354
430
|
fs.writeFileSync(test_file, lines.join("\n").trim() + "\n", "utf8")
|
|
355
|
-
created.push(
|
|
431
|
+
created.push(relative_test_path)
|
|
356
432
|
}
|
|
357
433
|
|
|
358
434
|
return JSON.stringify({
|