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