@bbearai/core 0.2.0 → 0.2.2

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.js CHANGED
@@ -146,6 +146,10 @@ var BugBearClient = class {
146
146
  const { data, error } = await this.supabase.from("test_assignments").select(`
147
147
  id,
148
148
  status,
149
+ started_at,
150
+ skip_reason,
151
+ is_verification,
152
+ original_report_id,
149
153
  test_case:test_cases(
150
154
  id,
151
155
  title,
@@ -163,6 +167,12 @@ var BugBearClient = class {
163
167
  test_template,
164
168
  rubric_mode,
165
169
  description
170
+ ),
171
+ group:test_groups(
172
+ id,
173
+ name,
174
+ description,
175
+ sort_order
166
176
  )
167
177
  )
168
178
  `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
@@ -173,6 +183,10 @@ var BugBearClient = class {
173
183
  return (data || []).map((item) => ({
174
184
  id: item.id,
175
185
  status: item.status,
186
+ startedAt: item.started_at,
187
+ skipReason: item.skip_reason,
188
+ isVerification: item.is_verification || false,
189
+ originalReportId: item.original_report_id,
176
190
  testCase: {
177
191
  id: item.test_case.id,
178
192
  title: item.test_case.title,
@@ -190,6 +204,12 @@ var BugBearClient = class {
190
204
  testTemplate: item.test_case.track.test_template,
191
205
  rubricMode: item.test_case.track.rubric_mode || "pass_fail",
192
206
  description: item.test_case.track.description
207
+ } : void 0,
208
+ group: item.test_case.group ? {
209
+ id: item.test_case.group.id,
210
+ name: item.test_case.group.name,
211
+ description: item.test_case.group.description,
212
+ sortOrder: item.test_case.group.sort_order
193
213
  } : void 0
194
214
  }
195
215
  }));
@@ -198,6 +218,280 @@ var BugBearClient = class {
198
218
  return [];
199
219
  }
200
220
  }
221
+ /**
222
+ * Get assignment by ID with time tracking fields
223
+ */
224
+ async getAssignment(assignmentId) {
225
+ try {
226
+ const { data, error } = await this.supabase.from("test_assignments").select(`
227
+ id,
228
+ status,
229
+ started_at,
230
+ completed_at,
231
+ duration_seconds,
232
+ test_case:test_cases(
233
+ id,
234
+ title,
235
+ test_key,
236
+ description,
237
+ steps,
238
+ expected_result,
239
+ priority,
240
+ target_route,
241
+ track:qa_tracks(
242
+ id,
243
+ name,
244
+ icon,
245
+ color,
246
+ test_template,
247
+ rubric_mode,
248
+ description
249
+ )
250
+ )
251
+ `).eq("id", assignmentId).single();
252
+ if (error || !data) return null;
253
+ const testCase = data.test_case;
254
+ if (!testCase) {
255
+ console.error("BugBear: Assignment returned without test_case");
256
+ return null;
257
+ }
258
+ const track = testCase.track;
259
+ return {
260
+ id: data.id,
261
+ status: data.status,
262
+ startedAt: data.started_at,
263
+ durationSeconds: data.duration_seconds,
264
+ testCase: {
265
+ id: testCase.id,
266
+ title: testCase.title,
267
+ testKey: testCase.test_key,
268
+ description: testCase.description,
269
+ steps: testCase.steps,
270
+ expectedResult: testCase.expected_result,
271
+ priority: testCase.priority,
272
+ targetRoute: testCase.target_route,
273
+ track: track ? {
274
+ id: track.id,
275
+ name: track.name,
276
+ icon: track.icon,
277
+ color: track.color,
278
+ testTemplate: track.test_template,
279
+ rubricMode: track.rubric_mode || "pass_fail",
280
+ description: track.description
281
+ } : void 0
282
+ }
283
+ };
284
+ } catch (err) {
285
+ console.error("BugBear: Error fetching assignment", err);
286
+ return null;
287
+ }
288
+ }
289
+ /**
290
+ * Update assignment status with automatic time tracking
291
+ * - Sets started_at when status changes to 'in_progress'
292
+ * - Calculates duration_seconds when status changes to 'passed'/'failed'/'blocked'
293
+ * - Optionally include feedback when completing a test
294
+ */
295
+ async updateAssignmentStatus(assignmentId, status, options) {
296
+ try {
297
+ const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
298
+ if (fetchError || !currentAssignment) {
299
+ return { success: false, error: "Assignment not found" };
300
+ }
301
+ const updateData = { status };
302
+ let durationSeconds;
303
+ if (status === "in_progress" && currentAssignment.status !== "in_progress") {
304
+ updateData.started_at = (/* @__PURE__ */ new Date()).toISOString();
305
+ }
306
+ if (["passed", "failed", "blocked"].includes(status)) {
307
+ updateData.completed_at = (/* @__PURE__ */ new Date()).toISOString();
308
+ if (currentAssignment.started_at) {
309
+ const startedAt = new Date(currentAssignment.started_at);
310
+ const completedAt = /* @__PURE__ */ new Date();
311
+ durationSeconds = Math.round((completedAt.getTime() - startedAt.getTime()) / 1e3);
312
+ updateData.duration_seconds = durationSeconds;
313
+ }
314
+ }
315
+ if (options?.notes) {
316
+ updateData.notes = options.notes;
317
+ }
318
+ if (options?.testResult) {
319
+ updateData.test_result = options.testResult;
320
+ }
321
+ const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
322
+ if (error) {
323
+ console.error("BugBear: Failed to update assignment status", error);
324
+ return { success: false, error: error.message };
325
+ }
326
+ if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
327
+ const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
328
+ if (fetchError2) {
329
+ console.error("BugBear: Failed to fetch assignment for feedback", fetchError2);
330
+ } else if (assignmentData?.test_case_id) {
331
+ const feedbackResult = await this.submitTestFeedback({
332
+ testCaseId: assignmentData.test_case_id,
333
+ assignmentId,
334
+ feedback: options.feedback,
335
+ timeToCompleteSeconds: durationSeconds
336
+ });
337
+ if (!feedbackResult.success) {
338
+ console.error("BugBear: Failed to submit feedback", feedbackResult.error);
339
+ }
340
+ }
341
+ }
342
+ return { success: true, durationSeconds };
343
+ } catch (err) {
344
+ const message = err instanceof Error ? err.message : "Unknown error";
345
+ console.error("BugBear: Error updating assignment status", err);
346
+ return { success: false, error: message };
347
+ }
348
+ }
349
+ /**
350
+ * Skip a test assignment with a required reason
351
+ * Marks the assignment as 'skipped' and records why it was skipped
352
+ */
353
+ async skipAssignment(assignmentId, reason, notes) {
354
+ try {
355
+ const updateData = {
356
+ status: "skipped",
357
+ skip_reason: reason,
358
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
359
+ };
360
+ if (notes) {
361
+ updateData.notes = notes;
362
+ }
363
+ const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
364
+ if (error) {
365
+ console.error("BugBear: Failed to skip assignment", error);
366
+ return { success: false, error: error.message };
367
+ }
368
+ return { success: true };
369
+ } catch (err) {
370
+ const message = err instanceof Error ? err.message : "Unknown error";
371
+ console.error("BugBear: Error skipping assignment", err);
372
+ return { success: false, error: message };
373
+ }
374
+ }
375
+ /**
376
+ * Submit feedback on a test case to help improve test quality
377
+ * This empowers testers to shape better tests over time
378
+ */
379
+ async submitTestFeedback(options) {
380
+ try {
381
+ const testerInfo = await this.getTesterInfo();
382
+ if (!testerInfo) {
383
+ return { success: false, error: "Not authenticated as tester" };
384
+ }
385
+ const { testCaseId, assignmentId, feedback, timeToCompleteSeconds } = options;
386
+ if (feedback.rating < 1 || feedback.rating > 5) {
387
+ return { success: false, error: "Rating must be between 1 and 5" };
388
+ }
389
+ const { error: feedbackError } = await this.supabase.from("test_feedback").insert({
390
+ project_id: this.config.projectId,
391
+ test_case_id: testCaseId,
392
+ assignment_id: assignmentId || null,
393
+ tester_id: testerInfo.id,
394
+ rating: feedback.rating,
395
+ clarity_rating: feedback.clarityRating || null,
396
+ steps_rating: feedback.stepsRating || null,
397
+ relevance_rating: feedback.relevanceRating || null,
398
+ feedback_note: feedback.feedbackNote?.trim() || null,
399
+ suggested_improvement: feedback.suggestedImprovement?.trim() || null,
400
+ is_outdated: feedback.isOutdated || false,
401
+ needs_more_detail: feedback.needsMoreDetail || false,
402
+ steps_unclear: feedback.stepsUnclear || false,
403
+ expected_result_unclear: feedback.expectedResultUnclear || false,
404
+ platform: this.getDeviceInfo().platform,
405
+ time_to_complete_seconds: timeToCompleteSeconds || null
406
+ });
407
+ if (feedbackError) {
408
+ console.error("BugBear: Failed to submit feedback", feedbackError);
409
+ return { success: false, error: feedbackError.message };
410
+ }
411
+ if (assignmentId) {
412
+ const { error: assignmentError } = await this.supabase.from("test_assignments").update({
413
+ feedback_rating: feedback.rating,
414
+ feedback_note: feedback.feedbackNote?.trim() || null,
415
+ feedback_submitted_at: (/* @__PURE__ */ new Date()).toISOString()
416
+ }).eq("id", assignmentId);
417
+ if (assignmentError) {
418
+ console.error("BugBear: Failed to update assignment feedback fields", assignmentError);
419
+ }
420
+ }
421
+ return { success: true };
422
+ } catch (err) {
423
+ const message = err instanceof Error ? err.message : "Unknown error";
424
+ console.error("BugBear: Error submitting feedback", err);
425
+ return { success: false, error: message };
426
+ }
427
+ }
428
+ /**
429
+ * Get the currently active (in_progress) assignment for the tester
430
+ */
431
+ async getActiveAssignment() {
432
+ try {
433
+ const testerInfo = await this.getTesterInfo();
434
+ if (!testerInfo) return null;
435
+ const { data, error } = await this.supabase.from("test_assignments").select(`
436
+ id,
437
+ status,
438
+ started_at,
439
+ test_case:test_cases(
440
+ id,
441
+ title,
442
+ test_key,
443
+ description,
444
+ steps,
445
+ expected_result,
446
+ priority,
447
+ target_route,
448
+ track:qa_tracks(
449
+ id,
450
+ name,
451
+ icon,
452
+ color,
453
+ test_template,
454
+ rubric_mode,
455
+ description
456
+ )
457
+ )
458
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "in_progress").order("started_at", { ascending: false }).limit(1).maybeSingle();
459
+ if (error || !data) return null;
460
+ const testCase = data.test_case;
461
+ if (!testCase) {
462
+ console.error("BugBear: Active assignment returned without test_case");
463
+ return null;
464
+ }
465
+ const track = testCase.track;
466
+ return {
467
+ id: data.id,
468
+ status: data.status,
469
+ startedAt: data.started_at,
470
+ testCase: {
471
+ id: testCase.id,
472
+ title: testCase.title,
473
+ testKey: testCase.test_key,
474
+ description: testCase.description,
475
+ steps: testCase.steps,
476
+ expectedResult: testCase.expected_result,
477
+ priority: testCase.priority,
478
+ targetRoute: testCase.target_route,
479
+ track: track ? {
480
+ id: track.id,
481
+ name: track.name,
482
+ icon: track.icon,
483
+ color: track.color,
484
+ testTemplate: track.test_template,
485
+ rubricMode: track.rubric_mode || "pass_fail",
486
+ description: track.description
487
+ } : void 0
488
+ }
489
+ };
490
+ } catch (err) {
491
+ console.error("BugBear: Error fetching active assignment", err);
492
+ return null;
493
+ }
494
+ }
201
495
  /**
202
496
  * Get current tester info
203
497
  * Looks up tester by email from the host app's authenticated user
@@ -748,6 +1042,248 @@ var BugBearClient = class {
748
1042
  return { success: false, error: message };
749
1043
  }
750
1044
  }
1045
+ // ============================================
1046
+ // QA Sessions (Sprint 1)
1047
+ // ============================================
1048
+ /**
1049
+ * Start a new QA session for exploratory testing
1050
+ */
1051
+ async startSession(options = {}) {
1052
+ try {
1053
+ const testerInfo = await this.getTesterInfo();
1054
+ if (!testerInfo) {
1055
+ return { success: false, error: "Not authenticated as a tester" };
1056
+ }
1057
+ const activeSession = await this.getActiveSession();
1058
+ if (activeSession) {
1059
+ return { success: false, error: "You already have an active session. End it before starting a new one." };
1060
+ }
1061
+ const { data, error } = await this.supabase.rpc("start_qa_session", {
1062
+ p_project_id: this.config.projectId,
1063
+ p_tester_id: testerInfo.id,
1064
+ p_focus_area: options.focusArea || null,
1065
+ p_track: options.track || null,
1066
+ p_platform: options.platform || null
1067
+ });
1068
+ if (error) {
1069
+ console.error("BugBear: Failed to start session", error);
1070
+ return { success: false, error: error.message };
1071
+ }
1072
+ const session = await this.getSession(data);
1073
+ if (!session) {
1074
+ return { success: false, error: "Session created but could not be fetched" };
1075
+ }
1076
+ return { success: true, session };
1077
+ } catch (err) {
1078
+ const message = err instanceof Error ? err.message : "Unknown error";
1079
+ console.error("BugBear: Error starting session", err);
1080
+ return { success: false, error: message };
1081
+ }
1082
+ }
1083
+ /**
1084
+ * End the current QA session
1085
+ */
1086
+ async endSession(sessionId, options = {}) {
1087
+ try {
1088
+ const { data, error } = await this.supabase.rpc("end_qa_session", {
1089
+ p_session_id: sessionId,
1090
+ p_notes: options.notes || null,
1091
+ p_routes_covered: options.routesCovered || null
1092
+ });
1093
+ if (error) {
1094
+ console.error("BugBear: Failed to end session", error);
1095
+ return { success: false, error: error.message };
1096
+ }
1097
+ const session = this.transformSession(data);
1098
+ return { success: true, session };
1099
+ } catch (err) {
1100
+ const message = err instanceof Error ? err.message : "Unknown error";
1101
+ console.error("BugBear: Error ending session", err);
1102
+ return { success: false, error: message };
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Get the current active session for the tester
1107
+ */
1108
+ async getActiveSession() {
1109
+ try {
1110
+ const testerInfo = await this.getTesterInfo();
1111
+ if (!testerInfo) return null;
1112
+ 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();
1113
+ if (error || !data) return null;
1114
+ return this.transformSession(data);
1115
+ } catch (err) {
1116
+ console.error("BugBear: Error fetching active session", err);
1117
+ return null;
1118
+ }
1119
+ }
1120
+ /**
1121
+ * Get a session by ID
1122
+ */
1123
+ async getSession(sessionId) {
1124
+ try {
1125
+ const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
1126
+ if (error || !data) return null;
1127
+ return this.transformSession(data);
1128
+ } catch (err) {
1129
+ console.error("BugBear: Error fetching session", err);
1130
+ return null;
1131
+ }
1132
+ }
1133
+ /**
1134
+ * Get session history for the tester
1135
+ */
1136
+ async getSessionHistory(limit = 10) {
1137
+ try {
1138
+ const testerInfo = await this.getTesterInfo();
1139
+ if (!testerInfo) return [];
1140
+ 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);
1141
+ if (error) {
1142
+ console.error("BugBear: Failed to fetch session history", error);
1143
+ return [];
1144
+ }
1145
+ return (data || []).map((s) => this.transformSession(s));
1146
+ } catch (err) {
1147
+ console.error("BugBear: Error fetching session history", err);
1148
+ return [];
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Add a finding during a session
1153
+ */
1154
+ async addFinding(sessionId, options) {
1155
+ try {
1156
+ const { data, error } = await this.supabase.rpc("add_session_finding", {
1157
+ p_session_id: sessionId,
1158
+ p_type: options.type,
1159
+ p_title: options.title,
1160
+ p_description: options.description || null,
1161
+ p_severity: options.severity || "observation",
1162
+ p_route: options.route || null,
1163
+ p_screenshot_url: options.screenshotUrl || null,
1164
+ p_console_logs: options.consoleLogs || null,
1165
+ p_network_snapshot: options.networkSnapshot || null,
1166
+ p_device_info: options.deviceInfo || null,
1167
+ p_app_context: options.appContext || null
1168
+ });
1169
+ if (error) {
1170
+ console.error("BugBear: Failed to add finding", error);
1171
+ return { success: false, error: error.message };
1172
+ }
1173
+ const finding = this.transformFinding(data);
1174
+ return { success: true, finding };
1175
+ } catch (err) {
1176
+ const message = err instanceof Error ? err.message : "Unknown error";
1177
+ console.error("BugBear: Error adding finding", err);
1178
+ return { success: false, error: message };
1179
+ }
1180
+ }
1181
+ /**
1182
+ * Get findings for a session
1183
+ */
1184
+ async getSessionFindings(sessionId) {
1185
+ try {
1186
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
1187
+ if (error) {
1188
+ console.error("BugBear: Failed to fetch findings", error);
1189
+ return [];
1190
+ }
1191
+ return (data || []).map((f) => this.transformFinding(f));
1192
+ } catch (err) {
1193
+ console.error("BugBear: Error fetching findings", err);
1194
+ return [];
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Convert a finding to a bug report
1199
+ */
1200
+ async convertFindingToBug(findingId) {
1201
+ try {
1202
+ const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
1203
+ p_finding_id: findingId
1204
+ });
1205
+ if (error) {
1206
+ console.error("BugBear: Failed to convert finding", error);
1207
+ return { success: false, error: error.message };
1208
+ }
1209
+ return { success: true, bugId: data };
1210
+ } catch (err) {
1211
+ const message = err instanceof Error ? err.message : "Unknown error";
1212
+ console.error("BugBear: Error converting finding", err);
1213
+ return { success: false, error: message };
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Dismiss a finding
1218
+ */
1219
+ async dismissFinding(findingId, reason) {
1220
+ try {
1221
+ const { error } = await this.supabase.from("qa_findings").update({
1222
+ dismissed: true,
1223
+ dismissed_reason: reason || null,
1224
+ dismissed_at: (/* @__PURE__ */ new Date()).toISOString()
1225
+ }).eq("id", findingId);
1226
+ if (error) {
1227
+ console.error("BugBear: Failed to dismiss finding", error);
1228
+ return { success: false, error: error.message };
1229
+ }
1230
+ return { success: true };
1231
+ } catch (err) {
1232
+ const message = err instanceof Error ? err.message : "Unknown error";
1233
+ console.error("BugBear: Error dismissing finding", err);
1234
+ return { success: false, error: message };
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Transform database session to QASession type
1239
+ */
1240
+ transformSession(data) {
1241
+ return {
1242
+ id: data.id,
1243
+ projectId: data.project_id,
1244
+ testerId: data.tester_id,
1245
+ focusArea: data.focus_area,
1246
+ track: data.track,
1247
+ platform: data.platform,
1248
+ startedAt: data.started_at,
1249
+ endedAt: data.ended_at,
1250
+ notes: data.notes,
1251
+ routesCovered: data.routes_covered || [],
1252
+ status: data.status,
1253
+ durationMinutes: data.duration_minutes,
1254
+ findingsCount: data.findings_count || 0,
1255
+ bugsFiled: data.bugs_filed || 0,
1256
+ createdAt: data.created_at,
1257
+ updatedAt: data.updated_at
1258
+ };
1259
+ }
1260
+ /**
1261
+ * Transform database finding to QAFinding type
1262
+ */
1263
+ transformFinding(data) {
1264
+ return {
1265
+ id: data.id,
1266
+ sessionId: data.session_id,
1267
+ projectId: data.project_id,
1268
+ type: data.type,
1269
+ severity: data.severity,
1270
+ title: data.title,
1271
+ description: data.description,
1272
+ route: data.route,
1273
+ screenshotUrl: data.screenshot_url,
1274
+ consoleLogs: data.console_logs,
1275
+ networkSnapshot: data.network_snapshot,
1276
+ deviceInfo: data.device_info,
1277
+ appContext: data.app_context,
1278
+ convertedToBugId: data.converted_to_bug_id,
1279
+ convertedToTestId: data.converted_to_test_id,
1280
+ dismissed: data.dismissed || false,
1281
+ dismissedReason: data.dismissed_reason,
1282
+ dismissedAt: data.dismissed_at,
1283
+ createdAt: data.created_at,
1284
+ updatedAt: data.updated_at
1285
+ };
1286
+ }
751
1287
  };
752
1288
  function createBugBear(config) {
753
1289
  return new BugBearClient(config);