@bbearai/core 0.1.6 → 0.2.1
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/dist/index.d.mts +422 -1
- package/dist/index.d.ts +422 -1
- package/dist/index.js +652 -11
- package/dist/index.mjs +652 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -86,7 +86,16 @@ var BugBearClient = class {
|
|
|
86
86
|
*/
|
|
87
87
|
async submitReport(report) {
|
|
88
88
|
try {
|
|
89
|
+
const validationError = this.validateReport(report);
|
|
90
|
+
if (validationError) {
|
|
91
|
+
return { success: false, error: validationError };
|
|
92
|
+
}
|
|
89
93
|
const userInfo = await this.getCurrentUserInfo();
|
|
94
|
+
const rateLimitId = userInfo?.email || this.config.projectId;
|
|
95
|
+
const rateLimit = await this.checkRateLimit(rateLimitId, "report_submit");
|
|
96
|
+
if (!rateLimit.allowed) {
|
|
97
|
+
return { success: false, error: rateLimit.error };
|
|
98
|
+
}
|
|
90
99
|
if (!userInfo) {
|
|
91
100
|
console.error("BugBear: No user info available, cannot submit report");
|
|
92
101
|
return { success: false, error: "User not authenticated" };
|
|
@@ -137,6 +146,7 @@ var BugBearClient = class {
|
|
|
137
146
|
const { data, error } = await this.supabase.from("test_assignments").select(`
|
|
138
147
|
id,
|
|
139
148
|
status,
|
|
149
|
+
started_at,
|
|
140
150
|
test_case:test_cases(
|
|
141
151
|
id,
|
|
142
152
|
title,
|
|
@@ -164,6 +174,7 @@ var BugBearClient = class {
|
|
|
164
174
|
return (data || []).map((item) => ({
|
|
165
175
|
id: item.id,
|
|
166
176
|
status: item.status,
|
|
177
|
+
startedAt: item.started_at,
|
|
167
178
|
testCase: {
|
|
168
179
|
id: item.test_case.id,
|
|
169
180
|
title: item.test_case.title,
|
|
@@ -189,47 +200,430 @@ var BugBearClient = class {
|
|
|
189
200
|
return [];
|
|
190
201
|
}
|
|
191
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Get assignment by ID with time tracking fields
|
|
205
|
+
*/
|
|
206
|
+
async getAssignment(assignmentId) {
|
|
207
|
+
try {
|
|
208
|
+
const { data, error } = await this.supabase.from("test_assignments").select(`
|
|
209
|
+
id,
|
|
210
|
+
status,
|
|
211
|
+
started_at,
|
|
212
|
+
completed_at,
|
|
213
|
+
duration_seconds,
|
|
214
|
+
test_case:test_cases(
|
|
215
|
+
id,
|
|
216
|
+
title,
|
|
217
|
+
test_key,
|
|
218
|
+
description,
|
|
219
|
+
steps,
|
|
220
|
+
expected_result,
|
|
221
|
+
priority,
|
|
222
|
+
target_route,
|
|
223
|
+
track:qa_tracks(
|
|
224
|
+
id,
|
|
225
|
+
name,
|
|
226
|
+
icon,
|
|
227
|
+
color,
|
|
228
|
+
test_template,
|
|
229
|
+
rubric_mode,
|
|
230
|
+
description
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
`).eq("id", assignmentId).single();
|
|
234
|
+
if (error || !data) return null;
|
|
235
|
+
const testCase = data.test_case;
|
|
236
|
+
if (!testCase) {
|
|
237
|
+
console.error("BugBear: Assignment returned without test_case");
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const track = testCase.track;
|
|
241
|
+
return {
|
|
242
|
+
id: data.id,
|
|
243
|
+
status: data.status,
|
|
244
|
+
startedAt: data.started_at,
|
|
245
|
+
durationSeconds: data.duration_seconds,
|
|
246
|
+
testCase: {
|
|
247
|
+
id: testCase.id,
|
|
248
|
+
title: testCase.title,
|
|
249
|
+
testKey: testCase.test_key,
|
|
250
|
+
description: testCase.description,
|
|
251
|
+
steps: testCase.steps,
|
|
252
|
+
expectedResult: testCase.expected_result,
|
|
253
|
+
priority: testCase.priority,
|
|
254
|
+
targetRoute: testCase.target_route,
|
|
255
|
+
track: track ? {
|
|
256
|
+
id: track.id,
|
|
257
|
+
name: track.name,
|
|
258
|
+
icon: track.icon,
|
|
259
|
+
color: track.color,
|
|
260
|
+
testTemplate: track.test_template,
|
|
261
|
+
rubricMode: track.rubric_mode || "pass_fail",
|
|
262
|
+
description: track.description
|
|
263
|
+
} : void 0
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error("BugBear: Error fetching assignment", err);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Update assignment status with automatic time tracking
|
|
273
|
+
* - Sets started_at when status changes to 'in_progress'
|
|
274
|
+
* - Calculates duration_seconds when status changes to 'passed'/'failed'/'blocked'
|
|
275
|
+
* - Optionally include feedback when completing a test
|
|
276
|
+
*/
|
|
277
|
+
async updateAssignmentStatus(assignmentId, status, options) {
|
|
278
|
+
try {
|
|
279
|
+
const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
|
|
280
|
+
if (fetchError || !currentAssignment) {
|
|
281
|
+
return { success: false, error: "Assignment not found" };
|
|
282
|
+
}
|
|
283
|
+
const updateData = { status };
|
|
284
|
+
let durationSeconds;
|
|
285
|
+
if (status === "in_progress" && currentAssignment.status !== "in_progress") {
|
|
286
|
+
updateData.started_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
287
|
+
}
|
|
288
|
+
if (["passed", "failed", "blocked"].includes(status)) {
|
|
289
|
+
updateData.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
290
|
+
if (currentAssignment.started_at) {
|
|
291
|
+
const startedAt = new Date(currentAssignment.started_at);
|
|
292
|
+
const completedAt = /* @__PURE__ */ new Date();
|
|
293
|
+
durationSeconds = Math.round((completedAt.getTime() - startedAt.getTime()) / 1e3);
|
|
294
|
+
updateData.duration_seconds = durationSeconds;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (options?.notes) {
|
|
298
|
+
updateData.notes = options.notes;
|
|
299
|
+
}
|
|
300
|
+
if (options?.testResult) {
|
|
301
|
+
updateData.test_result = options.testResult;
|
|
302
|
+
}
|
|
303
|
+
const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
|
|
304
|
+
if (error) {
|
|
305
|
+
console.error("BugBear: Failed to update assignment status", error);
|
|
306
|
+
return { success: false, error: error.message };
|
|
307
|
+
}
|
|
308
|
+
if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
|
|
309
|
+
const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
|
|
310
|
+
if (fetchError2) {
|
|
311
|
+
console.error("BugBear: Failed to fetch assignment for feedback", fetchError2);
|
|
312
|
+
} else if (assignmentData?.test_case_id) {
|
|
313
|
+
const feedbackResult = await this.submitTestFeedback({
|
|
314
|
+
testCaseId: assignmentData.test_case_id,
|
|
315
|
+
assignmentId,
|
|
316
|
+
feedback: options.feedback,
|
|
317
|
+
timeToCompleteSeconds: durationSeconds
|
|
318
|
+
});
|
|
319
|
+
if (!feedbackResult.success) {
|
|
320
|
+
console.error("BugBear: Failed to submit feedback", feedbackResult.error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { success: true, durationSeconds };
|
|
325
|
+
} catch (err) {
|
|
326
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
327
|
+
console.error("BugBear: Error updating assignment status", err);
|
|
328
|
+
return { success: false, error: message };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Submit feedback on a test case to help improve test quality
|
|
333
|
+
* This empowers testers to shape better tests over time
|
|
334
|
+
*/
|
|
335
|
+
async submitTestFeedback(options) {
|
|
336
|
+
try {
|
|
337
|
+
const testerInfo = await this.getTesterInfo();
|
|
338
|
+
if (!testerInfo) {
|
|
339
|
+
return { success: false, error: "Not authenticated as tester" };
|
|
340
|
+
}
|
|
341
|
+
const { testCaseId, assignmentId, feedback, timeToCompleteSeconds } = options;
|
|
342
|
+
if (feedback.rating < 1 || feedback.rating > 5) {
|
|
343
|
+
return { success: false, error: "Rating must be between 1 and 5" };
|
|
344
|
+
}
|
|
345
|
+
const { error: feedbackError } = await this.supabase.from("test_feedback").insert({
|
|
346
|
+
project_id: this.config.projectId,
|
|
347
|
+
test_case_id: testCaseId,
|
|
348
|
+
assignment_id: assignmentId || null,
|
|
349
|
+
tester_id: testerInfo.id,
|
|
350
|
+
rating: feedback.rating,
|
|
351
|
+
clarity_rating: feedback.clarityRating || null,
|
|
352
|
+
steps_rating: feedback.stepsRating || null,
|
|
353
|
+
relevance_rating: feedback.relevanceRating || null,
|
|
354
|
+
feedback_note: feedback.feedbackNote?.trim() || null,
|
|
355
|
+
suggested_improvement: feedback.suggestedImprovement?.trim() || null,
|
|
356
|
+
is_outdated: feedback.isOutdated || false,
|
|
357
|
+
needs_more_detail: feedback.needsMoreDetail || false,
|
|
358
|
+
steps_unclear: feedback.stepsUnclear || false,
|
|
359
|
+
expected_result_unclear: feedback.expectedResultUnclear || false,
|
|
360
|
+
platform: this.getDeviceInfo().platform,
|
|
361
|
+
time_to_complete_seconds: timeToCompleteSeconds || null
|
|
362
|
+
});
|
|
363
|
+
if (feedbackError) {
|
|
364
|
+
console.error("BugBear: Failed to submit feedback", feedbackError);
|
|
365
|
+
return { success: false, error: feedbackError.message };
|
|
366
|
+
}
|
|
367
|
+
if (assignmentId) {
|
|
368
|
+
const { error: assignmentError } = await this.supabase.from("test_assignments").update({
|
|
369
|
+
feedback_rating: feedback.rating,
|
|
370
|
+
feedback_note: feedback.feedbackNote?.trim() || null,
|
|
371
|
+
feedback_submitted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
372
|
+
}).eq("id", assignmentId);
|
|
373
|
+
if (assignmentError) {
|
|
374
|
+
console.error("BugBear: Failed to update assignment feedback fields", assignmentError);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { success: true };
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
380
|
+
console.error("BugBear: Error submitting feedback", err);
|
|
381
|
+
return { success: false, error: message };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get the currently active (in_progress) assignment for the tester
|
|
386
|
+
*/
|
|
387
|
+
async getActiveAssignment() {
|
|
388
|
+
try {
|
|
389
|
+
const testerInfo = await this.getTesterInfo();
|
|
390
|
+
if (!testerInfo) return null;
|
|
391
|
+
const { data, error } = await this.supabase.from("test_assignments").select(`
|
|
392
|
+
id,
|
|
393
|
+
status,
|
|
394
|
+
started_at,
|
|
395
|
+
test_case:test_cases(
|
|
396
|
+
id,
|
|
397
|
+
title,
|
|
398
|
+
test_key,
|
|
399
|
+
description,
|
|
400
|
+
steps,
|
|
401
|
+
expected_result,
|
|
402
|
+
priority,
|
|
403
|
+
target_route,
|
|
404
|
+
track:qa_tracks(
|
|
405
|
+
id,
|
|
406
|
+
name,
|
|
407
|
+
icon,
|
|
408
|
+
color,
|
|
409
|
+
test_template,
|
|
410
|
+
rubric_mode,
|
|
411
|
+
description
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
`).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "in_progress").order("started_at", { ascending: false }).limit(1).maybeSingle();
|
|
415
|
+
if (error || !data) return null;
|
|
416
|
+
const testCase = data.test_case;
|
|
417
|
+
if (!testCase) {
|
|
418
|
+
console.error("BugBear: Active assignment returned without test_case");
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const track = testCase.track;
|
|
422
|
+
return {
|
|
423
|
+
id: data.id,
|
|
424
|
+
status: data.status,
|
|
425
|
+
startedAt: data.started_at,
|
|
426
|
+
testCase: {
|
|
427
|
+
id: testCase.id,
|
|
428
|
+
title: testCase.title,
|
|
429
|
+
testKey: testCase.test_key,
|
|
430
|
+
description: testCase.description,
|
|
431
|
+
steps: testCase.steps,
|
|
432
|
+
expectedResult: testCase.expected_result,
|
|
433
|
+
priority: testCase.priority,
|
|
434
|
+
targetRoute: testCase.target_route,
|
|
435
|
+
track: track ? {
|
|
436
|
+
id: track.id,
|
|
437
|
+
name: track.name,
|
|
438
|
+
icon: track.icon,
|
|
439
|
+
color: track.color,
|
|
440
|
+
testTemplate: track.test_template,
|
|
441
|
+
rubricMode: track.rubric_mode || "pass_fail",
|
|
442
|
+
description: track.description
|
|
443
|
+
} : void 0
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.error("BugBear: Error fetching active assignment", err);
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
192
451
|
/**
|
|
193
452
|
* Get current tester info
|
|
194
453
|
* Looks up tester by email from the host app's authenticated user
|
|
454
|
+
* Checks both primary email AND additional_emails array
|
|
455
|
+
* Uses parameterized RPC function to prevent SQL injection
|
|
195
456
|
*/
|
|
196
457
|
async getTesterInfo() {
|
|
197
458
|
try {
|
|
198
459
|
const userInfo = await this.getCurrentUserInfo();
|
|
199
|
-
if (!userInfo) return null;
|
|
200
|
-
|
|
460
|
+
if (!userInfo?.email) return null;
|
|
461
|
+
if (!this.isValidEmail(userInfo.email)) {
|
|
462
|
+
console.warn("BugBear: Invalid email format");
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const { data, error } = await this.supabase.rpc("lookup_tester_by_email", {
|
|
466
|
+
p_project_id: this.config.projectId,
|
|
467
|
+
p_email: userInfo.email
|
|
468
|
+
}).maybeSingle();
|
|
201
469
|
if (error || !data) return null;
|
|
470
|
+
const tester = data;
|
|
202
471
|
return {
|
|
203
|
-
id:
|
|
204
|
-
name:
|
|
205
|
-
email:
|
|
206
|
-
additionalEmails:
|
|
207
|
-
avatarUrl:
|
|
208
|
-
platforms:
|
|
209
|
-
assignedTests:
|
|
210
|
-
completedTests:
|
|
472
|
+
id: tester.id,
|
|
473
|
+
name: tester.name,
|
|
474
|
+
email: tester.email,
|
|
475
|
+
additionalEmails: tester.additional_emails || [],
|
|
476
|
+
avatarUrl: tester.avatar_url || void 0,
|
|
477
|
+
platforms: tester.platforms || [],
|
|
478
|
+
assignedTests: tester.assigned_count || 0,
|
|
479
|
+
completedTests: tester.completed_count || 0
|
|
211
480
|
};
|
|
212
481
|
} catch (err) {
|
|
213
482
|
console.error("BugBear: getTesterInfo error", err);
|
|
214
483
|
return null;
|
|
215
484
|
}
|
|
216
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Basic email format validation (defense in depth)
|
|
488
|
+
*/
|
|
489
|
+
isValidEmail(email) {
|
|
490
|
+
if (!email || email.length > 254) return false;
|
|
491
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
492
|
+
return emailRegex.test(email);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Validate report input before submission
|
|
496
|
+
* Returns error message if invalid, null if valid
|
|
497
|
+
*/
|
|
498
|
+
validateReport(report) {
|
|
499
|
+
const validTypes = ["bug", "feedback", "suggestion", "test_pass", "test_fail"];
|
|
500
|
+
if (report.type && !validTypes.includes(report.type)) {
|
|
501
|
+
return `Invalid report type: ${report.type}. Must be one of: ${validTypes.join(", ")}`;
|
|
502
|
+
}
|
|
503
|
+
const validSeverities = ["critical", "high", "medium", "low"];
|
|
504
|
+
if (report.severity && !validSeverities.includes(report.severity)) {
|
|
505
|
+
return `Invalid severity: ${report.severity}. Must be one of: ${validSeverities.join(", ")}`;
|
|
506
|
+
}
|
|
507
|
+
if (report.title && report.title.length > 500) {
|
|
508
|
+
return "Title must be 500 characters or less";
|
|
509
|
+
}
|
|
510
|
+
if (report.description && report.description.length > 1e4) {
|
|
511
|
+
return "Description must be 10,000 characters or less";
|
|
512
|
+
}
|
|
513
|
+
if (report.screenshots && Array.isArray(report.screenshots)) {
|
|
514
|
+
if (report.screenshots.length > 10) {
|
|
515
|
+
return "Maximum 10 screenshots allowed";
|
|
516
|
+
}
|
|
517
|
+
for (const url of report.screenshots) {
|
|
518
|
+
if (typeof url !== "string" || url.length > 2e3) {
|
|
519
|
+
return "Invalid screenshot URL";
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Validate profile update input
|
|
527
|
+
* Returns error message if invalid, null if valid
|
|
528
|
+
*/
|
|
529
|
+
validateProfileUpdate(updates) {
|
|
530
|
+
if (updates.name !== void 0) {
|
|
531
|
+
if (typeof updates.name !== "string" || updates.name.length > 100) {
|
|
532
|
+
return "Name must be 100 characters or less";
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (updates.additionalEmails !== void 0) {
|
|
536
|
+
if (!Array.isArray(updates.additionalEmails)) {
|
|
537
|
+
return "Additional emails must be an array";
|
|
538
|
+
}
|
|
539
|
+
if (updates.additionalEmails.length > 5) {
|
|
540
|
+
return "Maximum 5 additional emails allowed";
|
|
541
|
+
}
|
|
542
|
+
for (const email of updates.additionalEmails) {
|
|
543
|
+
if (!this.isValidEmail(email)) {
|
|
544
|
+
return `Invalid email format: ${email}`;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (updates.avatarUrl !== void 0 && updates.avatarUrl !== null) {
|
|
549
|
+
if (typeof updates.avatarUrl !== "string" || updates.avatarUrl.length > 2e3) {
|
|
550
|
+
return "Invalid avatar URL";
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (updates.platforms !== void 0) {
|
|
554
|
+
if (!Array.isArray(updates.platforms)) {
|
|
555
|
+
return "Platforms must be an array";
|
|
556
|
+
}
|
|
557
|
+
const validPlatforms = ["ios", "android", "web", "desktop", "other"];
|
|
558
|
+
for (const platform of updates.platforms) {
|
|
559
|
+
if (!validPlatforms.includes(platform)) {
|
|
560
|
+
return `Invalid platform: ${platform}. Must be one of: ${validPlatforms.join(", ")}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Check rate limit for an action
|
|
568
|
+
* Returns { allowed: boolean, error?: string, remaining?: number }
|
|
569
|
+
*/
|
|
570
|
+
async checkRateLimit(identifier, action) {
|
|
571
|
+
try {
|
|
572
|
+
const { data, error } = await this.supabase.rpc("check_rate_limit", {
|
|
573
|
+
p_identifier: identifier,
|
|
574
|
+
p_action: action,
|
|
575
|
+
p_project_id: this.config.projectId
|
|
576
|
+
});
|
|
577
|
+
if (error) {
|
|
578
|
+
console.warn("BugBear: Rate limit check failed, allowing request", error.message);
|
|
579
|
+
return { allowed: true };
|
|
580
|
+
}
|
|
581
|
+
if (!data.allowed) {
|
|
582
|
+
return {
|
|
583
|
+
allowed: false,
|
|
584
|
+
error: `Rate limit exceeded. Try again in ${Math.ceil((new Date(data.reset_at).getTime() - Date.now()) / 1e3)} seconds.`,
|
|
585
|
+
remaining: 0,
|
|
586
|
+
resetAt: data.reset_at
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
allowed: true,
|
|
591
|
+
remaining: data.remaining,
|
|
592
|
+
resetAt: data.reset_at
|
|
593
|
+
};
|
|
594
|
+
} catch (err) {
|
|
595
|
+
console.warn("BugBear: Rate limit check error", err);
|
|
596
|
+
return { allowed: true };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
217
599
|
/**
|
|
218
600
|
* Update tester profile
|
|
219
601
|
* Allows testers to update their name, additional emails, avatar, and platforms
|
|
220
602
|
*/
|
|
221
603
|
async updateTesterProfile(updates) {
|
|
222
604
|
try {
|
|
605
|
+
const validationError = this.validateProfileUpdate(updates);
|
|
606
|
+
if (validationError) {
|
|
607
|
+
return { success: false, error: validationError };
|
|
608
|
+
}
|
|
223
609
|
const userInfo = await this.getCurrentUserInfo();
|
|
224
610
|
if (!userInfo) {
|
|
225
611
|
return { success: false, error: "Not authenticated" };
|
|
226
612
|
}
|
|
613
|
+
const rateLimit = await this.checkRateLimit(userInfo.email, "profile_update");
|
|
614
|
+
if (!rateLimit.allowed) {
|
|
615
|
+
return { success: false, error: rateLimit.error };
|
|
616
|
+
}
|
|
617
|
+
const testerInfo = await this.getTesterInfo();
|
|
618
|
+
if (!testerInfo) {
|
|
619
|
+
return { success: false, error: "Not a registered tester" };
|
|
620
|
+
}
|
|
227
621
|
const updateData = {};
|
|
228
622
|
if (updates.name !== void 0) updateData.name = updates.name;
|
|
229
623
|
if (updates.additionalEmails !== void 0) updateData.additional_emails = updates.additionalEmails;
|
|
230
624
|
if (updates.avatarUrl !== void 0) updateData.avatar_url = updates.avatarUrl;
|
|
231
625
|
if (updates.platforms !== void 0) updateData.platforms = updates.platforms;
|
|
232
|
-
const { error } = await this.supabase.from("testers").update(updateData).eq("
|
|
626
|
+
const { error } = await this.supabase.from("testers").update(updateData).eq("id", testerInfo.id);
|
|
233
627
|
if (error) {
|
|
234
628
|
console.error("BugBear: updateTesterProfile error", error);
|
|
235
629
|
return { success: false, error: error.message };
|
|
@@ -505,6 +899,11 @@ var BugBearClient = class {
|
|
|
505
899
|
console.error("BugBear: No tester info, cannot send message");
|
|
506
900
|
return false;
|
|
507
901
|
}
|
|
902
|
+
const rateLimit = await this.checkRateLimit(testerInfo.email, "message_send");
|
|
903
|
+
if (!rateLimit.allowed) {
|
|
904
|
+
console.error("BugBear: Rate limit exceeded for messages");
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
508
907
|
const { error } = await this.supabase.from("discussion_messages").insert({
|
|
509
908
|
thread_id: threadId,
|
|
510
909
|
sender_type: "tester",
|
|
@@ -599,6 +998,248 @@ var BugBearClient = class {
|
|
|
599
998
|
return { success: false, error: message };
|
|
600
999
|
}
|
|
601
1000
|
}
|
|
1001
|
+
// ============================================
|
|
1002
|
+
// QA Sessions (Sprint 1)
|
|
1003
|
+
// ============================================
|
|
1004
|
+
/**
|
|
1005
|
+
* Start a new QA session for exploratory testing
|
|
1006
|
+
*/
|
|
1007
|
+
async startSession(options = {}) {
|
|
1008
|
+
try {
|
|
1009
|
+
const testerInfo = await this.getTesterInfo();
|
|
1010
|
+
if (!testerInfo) {
|
|
1011
|
+
return { success: false, error: "Not authenticated as a tester" };
|
|
1012
|
+
}
|
|
1013
|
+
const activeSession = await this.getActiveSession();
|
|
1014
|
+
if (activeSession) {
|
|
1015
|
+
return { success: false, error: "You already have an active session. End it before starting a new one." };
|
|
1016
|
+
}
|
|
1017
|
+
const { data, error } = await this.supabase.rpc("start_qa_session", {
|
|
1018
|
+
p_project_id: this.config.projectId,
|
|
1019
|
+
p_tester_id: testerInfo.id,
|
|
1020
|
+
p_focus_area: options.focusArea || null,
|
|
1021
|
+
p_track: options.track || null,
|
|
1022
|
+
p_platform: options.platform || null
|
|
1023
|
+
});
|
|
1024
|
+
if (error) {
|
|
1025
|
+
console.error("BugBear: Failed to start session", error);
|
|
1026
|
+
return { success: false, error: error.message };
|
|
1027
|
+
}
|
|
1028
|
+
const session = await this.getSession(data);
|
|
1029
|
+
if (!session) {
|
|
1030
|
+
return { success: false, error: "Session created but could not be fetched" };
|
|
1031
|
+
}
|
|
1032
|
+
return { success: true, session };
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1035
|
+
console.error("BugBear: Error starting session", err);
|
|
1036
|
+
return { success: false, error: message };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* End the current QA session
|
|
1041
|
+
*/
|
|
1042
|
+
async endSession(sessionId, options = {}) {
|
|
1043
|
+
try {
|
|
1044
|
+
const { data, error } = await this.supabase.rpc("end_qa_session", {
|
|
1045
|
+
p_session_id: sessionId,
|
|
1046
|
+
p_notes: options.notes || null,
|
|
1047
|
+
p_routes_covered: options.routesCovered || null
|
|
1048
|
+
});
|
|
1049
|
+
if (error) {
|
|
1050
|
+
console.error("BugBear: Failed to end session", error);
|
|
1051
|
+
return { success: false, error: error.message };
|
|
1052
|
+
}
|
|
1053
|
+
const session = this.transformSession(data);
|
|
1054
|
+
return { success: true, session };
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1057
|
+
console.error("BugBear: Error ending session", err);
|
|
1058
|
+
return { success: false, error: message };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Get the current active session for the tester
|
|
1063
|
+
*/
|
|
1064
|
+
async getActiveSession() {
|
|
1065
|
+
try {
|
|
1066
|
+
const testerInfo = await this.getTesterInfo();
|
|
1067
|
+
if (!testerInfo) return null;
|
|
1068
|
+
const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
|
|
1069
|
+
if (error || !data) return null;
|
|
1070
|
+
return this.transformSession(data);
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
console.error("BugBear: Error fetching active session", err);
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Get a session by ID
|
|
1078
|
+
*/
|
|
1079
|
+
async getSession(sessionId) {
|
|
1080
|
+
try {
|
|
1081
|
+
const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
|
|
1082
|
+
if (error || !data) return null;
|
|
1083
|
+
return this.transformSession(data);
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
console.error("BugBear: Error fetching session", err);
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Get session history for the tester
|
|
1091
|
+
*/
|
|
1092
|
+
async getSessionHistory(limit = 10) {
|
|
1093
|
+
try {
|
|
1094
|
+
const testerInfo = await this.getTesterInfo();
|
|
1095
|
+
if (!testerInfo) return [];
|
|
1096
|
+
const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).order("started_at", { ascending: false }).limit(limit);
|
|
1097
|
+
if (error) {
|
|
1098
|
+
console.error("BugBear: Failed to fetch session history", error);
|
|
1099
|
+
return [];
|
|
1100
|
+
}
|
|
1101
|
+
return (data || []).map((s) => this.transformSession(s));
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.error("BugBear: Error fetching session history", err);
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Add a finding during a session
|
|
1109
|
+
*/
|
|
1110
|
+
async addFinding(sessionId, options) {
|
|
1111
|
+
try {
|
|
1112
|
+
const { data, error } = await this.supabase.rpc("add_session_finding", {
|
|
1113
|
+
p_session_id: sessionId,
|
|
1114
|
+
p_type: options.type,
|
|
1115
|
+
p_title: options.title,
|
|
1116
|
+
p_description: options.description || null,
|
|
1117
|
+
p_severity: options.severity || "observation",
|
|
1118
|
+
p_route: options.route || null,
|
|
1119
|
+
p_screenshot_url: options.screenshotUrl || null,
|
|
1120
|
+
p_console_logs: options.consoleLogs || null,
|
|
1121
|
+
p_network_snapshot: options.networkSnapshot || null,
|
|
1122
|
+
p_device_info: options.deviceInfo || null,
|
|
1123
|
+
p_app_context: options.appContext || null
|
|
1124
|
+
});
|
|
1125
|
+
if (error) {
|
|
1126
|
+
console.error("BugBear: Failed to add finding", error);
|
|
1127
|
+
return { success: false, error: error.message };
|
|
1128
|
+
}
|
|
1129
|
+
const finding = this.transformFinding(data);
|
|
1130
|
+
return { success: true, finding };
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1133
|
+
console.error("BugBear: Error adding finding", err);
|
|
1134
|
+
return { success: false, error: message };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Get findings for a session
|
|
1139
|
+
*/
|
|
1140
|
+
async getSessionFindings(sessionId) {
|
|
1141
|
+
try {
|
|
1142
|
+
const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
|
|
1143
|
+
if (error) {
|
|
1144
|
+
console.error("BugBear: Failed to fetch findings", error);
|
|
1145
|
+
return [];
|
|
1146
|
+
}
|
|
1147
|
+
return (data || []).map((f) => this.transformFinding(f));
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
console.error("BugBear: Error fetching findings", err);
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Convert a finding to a bug report
|
|
1155
|
+
*/
|
|
1156
|
+
async convertFindingToBug(findingId) {
|
|
1157
|
+
try {
|
|
1158
|
+
const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
|
|
1159
|
+
p_finding_id: findingId
|
|
1160
|
+
});
|
|
1161
|
+
if (error) {
|
|
1162
|
+
console.error("BugBear: Failed to convert finding", error);
|
|
1163
|
+
return { success: false, error: error.message };
|
|
1164
|
+
}
|
|
1165
|
+
return { success: true, bugId: data };
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1168
|
+
console.error("BugBear: Error converting finding", err);
|
|
1169
|
+
return { success: false, error: message };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Dismiss a finding
|
|
1174
|
+
*/
|
|
1175
|
+
async dismissFinding(findingId, reason) {
|
|
1176
|
+
try {
|
|
1177
|
+
const { error } = await this.supabase.from("qa_findings").update({
|
|
1178
|
+
dismissed: true,
|
|
1179
|
+
dismissed_reason: reason || null,
|
|
1180
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1181
|
+
}).eq("id", findingId);
|
|
1182
|
+
if (error) {
|
|
1183
|
+
console.error("BugBear: Failed to dismiss finding", error);
|
|
1184
|
+
return { success: false, error: error.message };
|
|
1185
|
+
}
|
|
1186
|
+
return { success: true };
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1189
|
+
console.error("BugBear: Error dismissing finding", err);
|
|
1190
|
+
return { success: false, error: message };
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Transform database session to QASession type
|
|
1195
|
+
*/
|
|
1196
|
+
transformSession(data) {
|
|
1197
|
+
return {
|
|
1198
|
+
id: data.id,
|
|
1199
|
+
projectId: data.project_id,
|
|
1200
|
+
testerId: data.tester_id,
|
|
1201
|
+
focusArea: data.focus_area,
|
|
1202
|
+
track: data.track,
|
|
1203
|
+
platform: data.platform,
|
|
1204
|
+
startedAt: data.started_at,
|
|
1205
|
+
endedAt: data.ended_at,
|
|
1206
|
+
notes: data.notes,
|
|
1207
|
+
routesCovered: data.routes_covered || [],
|
|
1208
|
+
status: data.status,
|
|
1209
|
+
durationMinutes: data.duration_minutes,
|
|
1210
|
+
findingsCount: data.findings_count || 0,
|
|
1211
|
+
bugsFiled: data.bugs_filed || 0,
|
|
1212
|
+
createdAt: data.created_at,
|
|
1213
|
+
updatedAt: data.updated_at
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Transform database finding to QAFinding type
|
|
1218
|
+
*/
|
|
1219
|
+
transformFinding(data) {
|
|
1220
|
+
return {
|
|
1221
|
+
id: data.id,
|
|
1222
|
+
sessionId: data.session_id,
|
|
1223
|
+
projectId: data.project_id,
|
|
1224
|
+
type: data.type,
|
|
1225
|
+
severity: data.severity,
|
|
1226
|
+
title: data.title,
|
|
1227
|
+
description: data.description,
|
|
1228
|
+
route: data.route,
|
|
1229
|
+
screenshotUrl: data.screenshot_url,
|
|
1230
|
+
consoleLogs: data.console_logs,
|
|
1231
|
+
networkSnapshot: data.network_snapshot,
|
|
1232
|
+
deviceInfo: data.device_info,
|
|
1233
|
+
appContext: data.app_context,
|
|
1234
|
+
convertedToBugId: data.converted_to_bug_id,
|
|
1235
|
+
convertedToTestId: data.converted_to_test_id,
|
|
1236
|
+
dismissed: data.dismissed || false,
|
|
1237
|
+
dismissedReason: data.dismissed_reason,
|
|
1238
|
+
dismissedAt: data.dismissed_at,
|
|
1239
|
+
createdAt: data.created_at,
|
|
1240
|
+
updatedAt: data.updated_at
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
602
1243
|
};
|
|
603
1244
|
function createBugBear(config) {
|
|
604
1245
|
return new BugBearClient(config);
|