@calltelemetry/openclaw-linear 0.9.1 → 0.9.3
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/README.md +390 -257
- package/package.json +1 -1
- package/src/__test__/smoke-linear-api.test.ts +147 -0
- package/src/infra/doctor.test.ts +762 -2
- package/src/pipeline/dag-dispatch.test.ts +444 -0
- package/src/pipeline/e2e-dispatch.test.ts +135 -0
- package/src/pipeline/pipeline.test.ts +1326 -1
- package/src/pipeline/planner.test.ts +457 -3
- package/src/pipeline/planning-state.test.ts +164 -3
- package/src/pipeline/webhook.test.ts +2438 -19
- package/src/tools/planner-tools.test.ts +722 -0
package/package.json
CHANGED
|
@@ -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;
|