@calltelemetry/openclaw-linear 0.9.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,13 +32,31 @@ function loadApiKey(): string {
32
32
  return token;
33
33
  }
34
34
 
35
+ function loadOAuthToken(): string | null {
36
+ try {
37
+ const raw = readFileSync(AUTH_PROFILES_PATH, "utf8");
38
+ const store = JSON.parse(raw);
39
+ const profile = store?.profiles?.["linear:default"];
40
+ return profile?.accessToken ?? null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
35
46
  let api: LinearAgentApi;
47
+ let oauthApi: LinearAgentApi | null = null;
36
48
  let smokeIssueId: string | null = null;
37
49
  const createdCommentIds: string[] = [];
38
50
 
39
51
  beforeAll(() => {
40
52
  const token = loadApiKey();
41
53
  api = new LinearAgentApi(token);
54
+ const oauthToken = loadOAuthToken();
55
+ if (oauthToken) {
56
+ // Strip "Bearer " prefix if present — LinearAgentApi adds it
57
+ const bare = oauthToken.replace(/^Bearer\s+/i, "");
58
+ oauthApi = new LinearAgentApi(bare);
59
+ }
42
60
  });
43
61
 
44
62
  afterAll(async () => {
@@ -331,6 +349,135 @@ describe("Linear API smoke tests", () => {
331
349
  });
332
350
  });
333
351
 
352
+ describe("AgentSessionEvent webhook flow", () => {
353
+ // Tests two paths that trigger AgentSessionEvent.created:
354
+ // Path A: @mention in a comment — user posts "@ctclaw do X" on an issue.
355
+ // Linear sees the agent mention, creates an AgentSession, and
356
+ // fires the webhook. This is the normal user flow.
357
+ // Path B: createSessionOnIssue API call — programmatic session creation
358
+ // (requires OAuth token, not API key).
359
+ //
360
+ // Both should result in the gateway receiving the webhook and spawning
361
+ // an agent run. We verify by polling for agent comments on the issue.
362
+ //
363
+ // Requires:
364
+ // - OAuth app webhook → https://linear.calltelemetry.com/linear/webhook
365
+ // - Gateway running (systemctl --user status openclaw-gateway)
366
+
367
+ let sessionIssueId: string | null = null;
368
+ let sessionIssueIdentifier: string | null = null;
369
+
370
+ it("creates a test issue for agent session", async () => {
371
+ const states = await api.getTeamStates(TEAM_ID);
372
+ const backlogState = states.find((s) => s.type === "backlog");
373
+ expect(backlogState).toBeTruthy();
374
+
375
+ const result = await api.createIssue({
376
+ teamId: TEAM_ID,
377
+ title: "[SMOKE TEST] AgentSessionEvent webhook test",
378
+ description:
379
+ "Auto-generated to test AgentSessionEvent webhook flow.\n" +
380
+ "Tests both @mention and createSessionOnIssue paths.\n\n" +
381
+ `Created: ${new Date().toISOString()}`,
382
+ stateId: backlogState!.id,
383
+ priority: 4,
384
+ });
385
+
386
+ expect(result.id).toBeTruthy();
387
+ sessionIssueId = result.id;
388
+ sessionIssueIdentifier = result.identifier;
389
+ console.log(`Created test issue: ${result.identifier} (${result.id})`);
390
+ });
391
+
392
+ it("Path A: @mention triggers AgentSessionEvent", async () => {
393
+ expect(sessionIssueId).toBeTruthy();
394
+
395
+ // Post a comment mentioning @ctclaw — this is what real users do.
396
+ // Linear should detect the agent @mention, create an AgentSession,
397
+ // and fire AgentSessionEvent.created to our webhook.
398
+ const commentId = await api.createComment(
399
+ sessionIssueId!,
400
+ "@ctclaw What is the status of this issue? [SMOKE TEST — ignore this]",
401
+ );
402
+ expect(commentId).toBeTruthy();
403
+ createdCommentIds.push(commentId);
404
+ console.log(`Posted @ctclaw mention comment: ${commentId}`);
405
+
406
+ // Give Linear time to process the @mention → create session → fire webhook
407
+ // → gateway receives → agent spawns → agent posts response
408
+ console.log("Waiting 12s for webhook round-trip...");
409
+ await new Promise((r) => setTimeout(r, 12000));
410
+
411
+ // Check for agent activity on the issue
412
+ const issue = await api.getIssueDetails(sessionIssueId!);
413
+ const comments = issue.comments?.nodes ?? [];
414
+
415
+ // Look for comments that aren't ours (agent responses)
416
+ const agentComments = comments.filter(
417
+ (c: any) =>
418
+ !c.body?.includes("[SMOKE TEST]") &&
419
+ c.id !== commentId,
420
+ );
421
+
422
+ if (agentComments.length > 0) {
423
+ console.log(
424
+ `@mention flow: ${agentComments.length} agent response(s) found — webhook flow confirmed!`,
425
+ );
426
+ // Preview first response
427
+ const preview = agentComments[0].body?.slice(0, 120) ?? "(empty)";
428
+ console.log(` First response: ${preview}...`);
429
+ } else {
430
+ console.log(
431
+ "@mention flow: No agent response yet. Possible causes:\n" +
432
+ " - @ctclaw not recognized as an agent mention by Linear\n" +
433
+ " - OAuth app webhook not configured or not pointing to gateway\n" +
434
+ " - Agent still processing (may take >12s for full response)\n" +
435
+ " Check: journalctl --user -u openclaw-gateway --since '1 min ago'",
436
+ );
437
+ }
438
+
439
+ expect(issue.id).toBe(sessionIssueId);
440
+ });
441
+
442
+ it("Path B: createSessionOnIssue via OAuth (programmatic)", async () => {
443
+ expect(sessionIssueId).toBeTruthy();
444
+
445
+ // agentSessionCreateOnIssue requires OAuth — personal API keys get 403
446
+ const sessionApi = oauthApi ?? api;
447
+ const result = await sessionApi.createSessionOnIssue(sessionIssueId!);
448
+
449
+ if (result.error) {
450
+ if (result.error.includes("apiKey") || result.error.includes("FORBIDDEN")) {
451
+ console.warn(
452
+ "Path B skipped: createSessionOnIssue requires OAuth token. " +
453
+ "Personal API keys get 403. Ensure linear:default has a valid OAuth token.",
454
+ );
455
+ } else {
456
+ console.warn(`createSessionOnIssue error: ${result.error}`);
457
+ }
458
+ }
459
+ if (result.sessionId) {
460
+ expect(typeof result.sessionId).toBe("string");
461
+ console.log(`Path B: Agent session created: ${result.sessionId}`);
462
+ }
463
+ });
464
+
465
+ it("cleans up agent session test issue", async () => {
466
+ if (!sessionIssueId) return;
467
+ try {
468
+ const states = await api.getTeamStates(TEAM_ID);
469
+ const canceledState = states.find(
470
+ (s) => s.type === "canceled" || s.name.toLowerCase().includes("cancel"),
471
+ );
472
+ if (canceledState) {
473
+ await api.updateIssue(sessionIssueId, { stateId: canceledState.id });
474
+ }
475
+ } catch {
476
+ // Best effort
477
+ }
478
+ });
479
+ });
480
+
334
481
  describe("cleanup", () => {
335
482
  it("cancels the smoke test issue", async () => {
336
483
  if (!smokeIssueId) return;