@calltelemetry/openclaw-linear 0.9.3 → 0.9.4

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.
@@ -24,6 +24,9 @@ const {
24
24
  resetGuidanceCacheMock,
25
25
  setActiveSessionMock,
26
26
  clearActiveSessionMock,
27
+ getIssueAffinityMock,
28
+ configureAffinityTtlMock,
29
+ resetAffinityForTestingMock,
27
30
  readDispatchStateMock,
28
31
  getActiveDispatchMock,
29
32
  registerDispatchMock,
@@ -97,6 +100,9 @@ const {
97
100
  resetGuidanceCacheMock: vi.fn(),
98
101
  setActiveSessionMock: vi.fn(),
99
102
  clearActiveSessionMock: vi.fn(),
103
+ getIssueAffinityMock: vi.fn().mockReturnValue(null),
104
+ configureAffinityTtlMock: vi.fn(),
105
+ resetAffinityForTestingMock: vi.fn(),
100
106
  readDispatchStateMock: vi.fn().mockResolvedValue({ activeDispatches: {} }),
101
107
  getActiveDispatchMock: vi.fn().mockReturnValue(null),
102
108
  registerDispatchMock: vi.fn().mockResolvedValue(undefined),
@@ -107,7 +113,7 @@ const {
107
113
  createWorktreeMock: vi.fn().mockReturnValue({ path: "/tmp/worktree", branch: "codex/ENG-123", resumed: false }),
108
114
  createMultiWorktreeMock: vi.fn().mockReturnValue({ parentPath: "/tmp/multi", worktrees: [] }),
109
115
  prepareWorkspaceMock: vi.fn().mockReturnValue({ pulled: true, submodulesInitialized: false, errors: [] }),
110
- resolveReposMock: vi.fn().mockReturnValue({ repos: [{ name: "main", path: "/home/claw/ai-workspace" }], source: "config_default" }),
116
+ resolveReposMock: vi.fn().mockReturnValue({ repos: [{ name: "main", path: "/tmp/test/workspace" }], source: "config_default" }),
111
117
  isMultiRepoMock: vi.fn().mockReturnValue(false),
112
118
  ensureClawDirMock: vi.fn(),
113
119
  writeManifestMock: vi.fn(),
@@ -167,6 +173,9 @@ vi.mock("./guidance.js", () => ({
167
173
  vi.mock("./active-session.js", () => ({
168
174
  setActiveSession: setActiveSessionMock,
169
175
  clearActiveSession: clearActiveSessionMock,
176
+ getIssueAffinity: getIssueAffinityMock,
177
+ _configureAffinityTtl: configureAffinityTtlMock,
178
+ _resetAffinityForTesting: resetAffinityForTestingMock,
170
179
  }));
171
180
 
172
181
  vi.mock("./dispatch-state.js", () => ({
@@ -355,6 +364,9 @@ afterEach(() => {
355
364
  isGuidanceEnabledMock.mockReset().mockReturnValue(false);
356
365
  setActiveSessionMock.mockReset();
357
366
  clearActiveSessionMock.mockReset();
367
+ getIssueAffinityMock.mockReset().mockReturnValue(null);
368
+ configureAffinityTtlMock.mockReset();
369
+ resetAffinityMock.mockReset();
358
370
  readDispatchStateMock.mockReset().mockResolvedValue({ activeDispatches: {} });
359
371
  getActiveDispatchMock.mockReset().mockReturnValue(null);
360
372
  registerDispatchMock.mockReset().mockResolvedValue(undefined);
@@ -363,7 +375,7 @@ afterEach(() => {
363
375
  assessTierMock.mockReset().mockResolvedValue({ tier: "medium", model: "anthropic/claude-sonnet-4-6", reasoning: "moderate complexity" });
364
376
  createWorktreeMock.mockReset().mockReturnValue({ path: "/tmp/worktree", branch: "codex/ENG-123", resumed: false });
365
377
  prepareWorkspaceMock.mockReset().mockReturnValue({ pulled: true, submodulesInitialized: false, errors: [] });
366
- resolveReposMock.mockReset().mockReturnValue({ repos: [{ name: "main", path: "/home/claw/ai-workspace" }], source: "config_default" });
378
+ resolveReposMock.mockReset().mockReturnValue({ repos: [{ name: "main", path: "/tmp/test/workspace" }], source: "config_default" });
367
379
  isMultiRepoMock.mockReset().mockReturnValue(false);
368
380
  ensureClawDirMock.mockReset();
369
381
  writeManifestMock.mockReset();
@@ -2756,3 +2768,2077 @@ describe("Unhandled webhook types", () => {
2756
2768
  expect(body).toBe("Invalid payload");
2757
2769
  });
2758
2770
  });
2771
+
2772
+ // ---------------------------------------------------------------------------
2773
+ // Coverage push: .catch(() => {}) callbacks in AgentSession.created
2774
+ // ---------------------------------------------------------------------------
2775
+
2776
+ describe("AgentSession.created .catch callbacks", () => {
2777
+ it("covers initial thought emitActivity .catch when it rejects", async () => {
2778
+ // Make emitActivity reject for "thought" type so .catch(() => {}) runs
2779
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
2780
+ if (content.type === "thought") return Promise.reject(new Error("thought emit fail"));
2781
+ if (content.type === "response") return Promise.resolve(undefined);
2782
+ return Promise.resolve(undefined);
2783
+ });
2784
+
2785
+ const result = await postWebhook({
2786
+ type: "AgentSessionEvent",
2787
+ action: "created",
2788
+ agentSession: {
2789
+ id: "sess-catch-thought",
2790
+ issue: { id: "issue-catch-thought", identifier: "ENG-CT" },
2791
+ },
2792
+ previousComments: [{ body: "Do this", user: { name: "Dev" } }],
2793
+ });
2794
+
2795
+ expect(result.status).toBe(200);
2796
+ await new Promise((r) => setTimeout(r, 150));
2797
+ // Agent should still run despite thought emission failure
2798
+ expect(runAgentMock).toHaveBeenCalled();
2799
+ });
2800
+
2801
+ it("covers error handler emitActivity .catch when it also rejects", async () => {
2802
+ // Make runAgent throw to enter catch block, then make error emitActivity also reject
2803
+ runAgentMock.mockRejectedValue(new Error("agent crashed hard"));
2804
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
2805
+ if (content.type === "thought") return Promise.resolve(undefined);
2806
+ if (content.type === "error") return Promise.reject(new Error("error emit also fails"));
2807
+ return Promise.resolve(undefined);
2808
+ });
2809
+
2810
+ const result = await postWebhook({
2811
+ type: "AgentSessionEvent",
2812
+ action: "created",
2813
+ agentSession: {
2814
+ id: "sess-catch-err",
2815
+ issue: { id: "issue-catch-err", identifier: "ENG-CE" },
2816
+ },
2817
+ previousComments: [],
2818
+ });
2819
+
2820
+ expect(result.status).toBe(200);
2821
+ await new Promise((r) => setTimeout(r, 150));
2822
+ // Error should be logged
2823
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
2824
+ expect(errorCalls.some((msg: string) => msg.includes("AgentSession handler error"))).toBe(true);
2825
+ // Active session should still be cleared in finally
2826
+ expect(clearActiveSessionMock).toHaveBeenCalledWith("issue-catch-err");
2827
+ });
2828
+
2829
+ it("covers isTriaged=true tool access lines for started state", async () => {
2830
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
2831
+ id: "issue-triaged",
2832
+ identifier: "ENG-TRIAGED",
2833
+ title: "Triaged Issue",
2834
+ description: "Already triaged",
2835
+ state: { name: "In Progress", type: "started" },
2836
+ assignee: { name: "Dev" },
2837
+ team: { id: "team-triaged" },
2838
+ });
2839
+
2840
+ const result = await postWebhook({
2841
+ type: "AgentSessionEvent",
2842
+ action: "created",
2843
+ agentSession: {
2844
+ id: "sess-triaged",
2845
+ issue: { id: "issue-triaged", identifier: "ENG-TRIAGED" },
2846
+ },
2847
+ previousComments: [{ body: "Fix this bug", user: { name: "PM" } }],
2848
+ });
2849
+
2850
+ expect(result.status).toBe(200);
2851
+ await new Promise((r) => setTimeout(r, 100));
2852
+ expect(runAgentMock).toHaveBeenCalled();
2853
+ const agentCall = runAgentMock.mock.calls[0][0];
2854
+ expect(agentCall.message).toContain("Full access");
2855
+ });
2856
+
2857
+ it("covers no avatarUrl fallback when emitActivity fails for response", async () => {
2858
+ // Set profiles with no avatarUrl
2859
+ loadAgentProfilesMock.mockReturnValue({
2860
+ mal: { label: "Mal", mission: "captain", mentionAliases: ["mal"], isDefault: true },
2861
+ });
2862
+ // emitActivity fails for response
2863
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
2864
+ if (content.type === "response") return Promise.reject(new Error("fail"));
2865
+ return Promise.resolve(undefined);
2866
+ });
2867
+
2868
+ const result = await postWebhook({
2869
+ type: "AgentSessionEvent",
2870
+ action: "created",
2871
+ agentSession: {
2872
+ id: "sess-no-avatar",
2873
+ issue: { id: "issue-no-avatar", identifier: "ENG-NA" },
2874
+ },
2875
+ previousComments: [],
2876
+ });
2877
+
2878
+ expect(result.status).toBe(200);
2879
+ await new Promise((r) => setTimeout(r, 150));
2880
+ // Should fall back to comment without agentOpts (no avatar)
2881
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
2882
+ });
2883
+
2884
+ it("covers getIssueDetails failure in created handler", async () => {
2885
+ mockLinearApiInstance.getIssueDetails.mockRejectedValue(new Error("fetch fail"));
2886
+
2887
+ const result = await postWebhook({
2888
+ type: "AgentSessionEvent",
2889
+ action: "created",
2890
+ agentSession: {
2891
+ id: "sess-fetch-fail",
2892
+ issue: { id: "issue-fetch-fail", identifier: "ENG-FF", title: "Original Title", description: "Original desc" },
2893
+ },
2894
+ previousComments: [{ body: "Check this", user: { name: "Dev" } }],
2895
+ });
2896
+
2897
+ expect(result.status).toBe(200);
2898
+ await new Promise((r) => setTimeout(r, 100));
2899
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
2900
+ expect(warnCalls.some((msg: string) => msg.includes("Could not fetch issue details"))).toBe(true);
2901
+ expect(runAgentMock).toHaveBeenCalled();
2902
+ });
2903
+ });
2904
+
2905
+ // ---------------------------------------------------------------------------
2906
+ // Coverage push: .catch callbacks in AgentSession.prompted
2907
+ // ---------------------------------------------------------------------------
2908
+
2909
+ describe("AgentSession.prompted .catch callbacks and branches", () => {
2910
+ it("covers thought emitActivity .catch when it rejects in prompted", async () => {
2911
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
2912
+ if (content.type === "thought") return Promise.reject(new Error("thought fail"));
2913
+ if (content.type === "response") return Promise.resolve(undefined);
2914
+ return Promise.resolve(undefined);
2915
+ });
2916
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
2917
+ id: "issue-p-catch",
2918
+ identifier: "ENG-PC",
2919
+ title: "Prompted Catch",
2920
+ description: "desc",
2921
+ state: { name: "In Progress", type: "started" },
2922
+ team: { id: "team-pc" },
2923
+ comments: { nodes: [{ user: { name: "User" }, body: "Some context" }] },
2924
+ });
2925
+
2926
+ const result = await postWebhook({
2927
+ type: "AgentSessionEvent",
2928
+ action: "prompted",
2929
+ agentSession: {
2930
+ id: "sess-p-catch",
2931
+ issue: { id: "issue-p-catch", identifier: "ENG-PC" },
2932
+ },
2933
+ agentActivity: { content: { body: "Follow up please" } },
2934
+ webhookId: "wh-p-catch",
2935
+ });
2936
+
2937
+ expect(result.status).toBe(200);
2938
+ await new Promise((r) => setTimeout(r, 150));
2939
+ expect(runAgentMock).toHaveBeenCalled();
2940
+ });
2941
+
2942
+ it("covers error handler emitActivity .catch in prompted when it rejects", async () => {
2943
+ runAgentMock.mockRejectedValue(new Error("prompted agent crash"));
2944
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
2945
+ if (content.type === "error") return Promise.reject(new Error("error emit fail too"));
2946
+ return Promise.resolve(undefined);
2947
+ });
2948
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
2949
+ id: "issue-p-err-catch",
2950
+ identifier: "ENG-PEC",
2951
+ title: "Prompted Error Catch",
2952
+ description: "desc",
2953
+ state: { name: "Backlog", type: "backlog" },
2954
+ team: { id: "team-pec" },
2955
+ comments: { nodes: [] },
2956
+ });
2957
+
2958
+ const result = await postWebhook({
2959
+ type: "AgentSessionEvent",
2960
+ action: "prompted",
2961
+ agentSession: {
2962
+ id: "sess-p-err-catch",
2963
+ issue: { id: "issue-p-err-catch", identifier: "ENG-PEC" },
2964
+ },
2965
+ agentActivity: { content: { body: "Try again" } },
2966
+ webhookId: "wh-pec",
2967
+ });
2968
+
2969
+ expect(result.status).toBe(200);
2970
+ await new Promise((r) => setTimeout(r, 150));
2971
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
2972
+ expect(errorCalls.some((msg: string) => msg.includes("prompted handler error"))).toBe(true);
2973
+ expect(clearActiveSessionMock).toHaveBeenCalledWith("issue-p-err-catch");
2974
+ });
2975
+
2976
+ it("covers response emitActivity .then/.catch path in prompted (reject path)", async () => {
2977
+ // emitActivity succeeds for thought, rejects for response
2978
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
2979
+ if (content.type === "response") return Promise.reject(new Error("response fail"));
2980
+ return Promise.resolve(undefined);
2981
+ });
2982
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
2983
+ id: "issue-p-resp-fail",
2984
+ identifier: "ENG-PRF",
2985
+ title: "Prompted Response Fail",
2986
+ description: "desc",
2987
+ state: { name: "In Progress", type: "started" },
2988
+ team: { id: "team-prf" },
2989
+ comments: { nodes: [] },
2990
+ });
2991
+
2992
+ const result = await postWebhook({
2993
+ type: "AgentSessionEvent",
2994
+ action: "prompted",
2995
+ agentSession: {
2996
+ id: "sess-p-resp-fail",
2997
+ issue: { id: "issue-p-resp-fail", identifier: "ENG-PRF" },
2998
+ },
2999
+ agentActivity: { content: { body: "What about this?" } },
3000
+ webhookId: "wh-prf",
3001
+ });
3002
+
3003
+ expect(result.status).toBe(200);
3004
+ await new Promise((r) => setTimeout(r, 150));
3005
+ // Should fall back to comment
3006
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
3007
+ });
3008
+
3009
+ it("covers prompted with agent returning success=false", async () => {
3010
+ runAgentMock.mockResolvedValue({ success: false, output: "Prompted failure" });
3011
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3012
+ id: "issue-p-fail",
3013
+ identifier: "ENG-PFL",
3014
+ title: "Prompted Fail Result",
3015
+ description: "desc",
3016
+ state: { name: "Backlog", type: "backlog" },
3017
+ team: { id: "team-pfl" },
3018
+ comments: { nodes: [] },
3019
+ });
3020
+
3021
+ const result = await postWebhook({
3022
+ type: "AgentSessionEvent",
3023
+ action: "prompted",
3024
+ agentSession: {
3025
+ id: "sess-p-fail",
3026
+ issue: { id: "issue-p-fail", identifier: "ENG-PFL" },
3027
+ },
3028
+ agentActivity: { content: { body: "Anything new?" } },
3029
+ webhookId: "wh-pfl",
3030
+ });
3031
+
3032
+ expect(result.status).toBe(200);
3033
+ await new Promise((r) => setTimeout(r, 150));
3034
+ const emitCalls = mockLinearApiInstance.emitActivity.mock.calls;
3035
+ const responseCall = emitCalls.find((c: any[]) => c[1]?.type === "response");
3036
+ if (responseCall) {
3037
+ expect(responseCall[1].body).toContain("Something went wrong");
3038
+ }
3039
+ });
3040
+
3041
+ it("covers getIssueDetails failure in prompted handler", async () => {
3042
+ mockLinearApiInstance.getIssueDetails.mockRejectedValue(new Error("fetch fail"));
3043
+
3044
+ const result = await postWebhook({
3045
+ type: "AgentSessionEvent",
3046
+ action: "prompted",
3047
+ agentSession: {
3048
+ id: "sess-p-fetch-fail",
3049
+ issue: { id: "issue-p-fetch-fail", identifier: "ENG-PFF", title: "Title", description: "Desc" },
3050
+ },
3051
+ agentActivity: { content: { body: "Follow up" } },
3052
+ webhookId: "wh-pff",
3053
+ });
3054
+
3055
+ expect(result.status).toBe(200);
3056
+ await new Promise((r) => setTimeout(r, 150));
3057
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
3058
+ expect(warnCalls.some((msg: string) => msg.includes("Could not fetch issue details"))).toBe(true);
3059
+ expect(runAgentMock).toHaveBeenCalled();
3060
+ });
3061
+
3062
+ it("covers prompted with no avatarUrl when response emitActivity fails", async () => {
3063
+ loadAgentProfilesMock.mockReturnValue({
3064
+ mal: { label: "Mal", mission: "captain", mentionAliases: ["mal"], isDefault: true },
3065
+ });
3066
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3067
+ if (content.type === "response") return Promise.reject(new Error("fail"));
3068
+ return Promise.resolve(undefined);
3069
+ });
3070
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3071
+ id: "issue-p-no-av",
3072
+ identifier: "ENG-PNA",
3073
+ title: "No Avatar Prompted",
3074
+ description: "desc",
3075
+ state: { name: "Backlog", type: "backlog" },
3076
+ team: { id: "team-pna" },
3077
+ comments: { nodes: [] },
3078
+ });
3079
+
3080
+ const result = await postWebhook({
3081
+ type: "AgentSessionEvent",
3082
+ action: "prompted",
3083
+ agentSession: {
3084
+ id: "sess-p-no-av",
3085
+ issue: { id: "issue-p-no-av", identifier: "ENG-PNA" },
3086
+ },
3087
+ agentActivity: { content: { body: "Hello" } },
3088
+ webhookId: "wh-pna",
3089
+ });
3090
+
3091
+ expect(result.status).toBe(200);
3092
+ await new Promise((r) => setTimeout(r, 150));
3093
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
3094
+ });
3095
+
3096
+ it("covers prompted guidance caching and formatting", async () => {
3097
+ extractGuidanceMock.mockReturnValue({ guidance: "Follow standards", source: "webhook" });
3098
+ isGuidanceEnabledMock.mockReturnValue(true);
3099
+ formatGuidanceAppendixMock.mockReturnValue("\n## Guidance\nFollow standards");
3100
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3101
+ id: "issue-p-guid",
3102
+ identifier: "ENG-PG",
3103
+ title: "Prompted Guidance",
3104
+ description: "desc",
3105
+ state: { name: "In Progress", type: "started" },
3106
+ team: { id: "team-pg" },
3107
+ comments: { nodes: [] },
3108
+ });
3109
+
3110
+ const result = await postWebhook({
3111
+ type: "AgentSessionEvent",
3112
+ action: "prompted",
3113
+ agentSession: {
3114
+ id: "sess-p-guid",
3115
+ issue: { id: "issue-p-guid", identifier: "ENG-PG" },
3116
+ },
3117
+ agentActivity: { content: { body: "Continue" } },
3118
+ webhookId: "wh-pg",
3119
+ guidance: "Follow standards",
3120
+ });
3121
+
3122
+ expect(result.status).toBe(200);
3123
+ await new Promise((r) => setTimeout(r, 150));
3124
+ expect(cacheGuidanceForTeamMock).toHaveBeenCalledWith("team-pg", "Follow standards");
3125
+ expect(runAgentMock).toHaveBeenCalled();
3126
+ });
3127
+ });
3128
+
3129
+ // ---------------------------------------------------------------------------
3130
+ // Coverage push: Comment.create .catch callbacks on intent dispatches
3131
+ // ---------------------------------------------------------------------------
3132
+
3133
+ describe("Comment.create .catch callbacks on fire-and-forget dispatches", () => {
3134
+ it("covers @mention fast path dispatchCommentToAgent .catch", async () => {
3135
+ resolveAgentFromAliasMock.mockReturnValue({ agentId: "kaylee", profile: { label: "Kaylee" } });
3136
+ // Make the whole dispatch fail — runAgent throws
3137
+ runAgentMock.mockRejectedValue(new Error("mention dispatch fail"));
3138
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3139
+ id: "issue-mention-catch",
3140
+ identifier: "ENG-MC",
3141
+ title: "Mention Catch",
3142
+ description: "desc",
3143
+ state: { name: "Backlog", type: "backlog" },
3144
+ team: { id: "team-mc" },
3145
+ comments: { nodes: [] },
3146
+ });
3147
+
3148
+ const result = await postWebhook({
3149
+ type: "Comment",
3150
+ action: "create",
3151
+ data: {
3152
+ id: "comment-mention-catch",
3153
+ body: "@kaylee do this now",
3154
+ user: { id: "human-mc", name: "Human" },
3155
+ issue: { id: "issue-mention-catch", identifier: "ENG-MC" },
3156
+ },
3157
+ });
3158
+
3159
+ expect(result.status).toBe(200);
3160
+ await new Promise((r) => setTimeout(r, 300));
3161
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3162
+ // The .catch on dispatchCommentToAgent should log
3163
+ expect(errorCalls.some((msg: string) =>
3164
+ msg.includes("dispatch error") || msg.includes("dispatchCommentToAgent error")
3165
+ )).toBe(true);
3166
+ });
3167
+
3168
+ it("covers plan_start initiatePlanningSession .catch when it rejects", async () => {
3169
+ classifyIntentMock.mockResolvedValue({
3170
+ intent: "plan_start",
3171
+ reasoning: "Start planning",
3172
+ fromFallback: false,
3173
+ });
3174
+ initiatePlanningSessionMock.mockRejectedValue(new Error("planning init fail"));
3175
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3176
+ id: "issue-ps-catch",
3177
+ identifier: "ENG-PSC",
3178
+ title: "Plan Start Catch",
3179
+ state: { name: "Backlog" },
3180
+ project: { id: "proj-ps-catch" },
3181
+ team: { id: "team-psc" },
3182
+ });
3183
+
3184
+ const result = await postWebhook({
3185
+ type: "Comment",
3186
+ action: "create",
3187
+ data: {
3188
+ id: "comment-ps-catch",
3189
+ body: "Start planning",
3190
+ user: { id: "human-psc", name: "Human" },
3191
+ issue: { id: "issue-ps-catch", identifier: "ENG-PSC", project: { id: "proj-ps-catch" } },
3192
+ },
3193
+ });
3194
+
3195
+ expect(result.status).toBe(200);
3196
+ await new Promise((r) => setTimeout(r, 100));
3197
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3198
+ expect(errorCalls.some((msg: string) => msg.includes("Planning initiation error"))).toBe(true);
3199
+ });
3200
+
3201
+ it("covers plan_start already-planning handlePlannerTurn .catch when it rejects", async () => {
3202
+ classifyIntentMock.mockResolvedValue({
3203
+ intent: "plan_start",
3204
+ reasoning: "Start planning",
3205
+ fromFallback: false,
3206
+ });
3207
+ isInPlanningModeMock.mockReturnValue(true);
3208
+ getPlanningSessionMock.mockReturnValue({
3209
+ projectId: "proj-hpt-catch",
3210
+ projectName: "HPT Catch",
3211
+ rootIssueId: "root-hpt",
3212
+ status: "interviewing",
3213
+ });
3214
+ handlePlannerTurnMock.mockRejectedValue(new Error("planner turn fail"));
3215
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3216
+ id: "issue-hpt-catch",
3217
+ identifier: "ENG-HPC",
3218
+ title: "HPT Catch",
3219
+ state: { name: "Backlog" },
3220
+ project: { id: "proj-hpt-catch" },
3221
+ });
3222
+
3223
+ const result = await postWebhook({
3224
+ type: "Comment",
3225
+ action: "create",
3226
+ data: {
3227
+ id: "comment-hpt-catch",
3228
+ body: "Start again",
3229
+ user: { id: "human-hpc", name: "Human" },
3230
+ issue: { id: "issue-hpt-catch", identifier: "ENG-HPC", project: { id: "proj-hpt-catch" } },
3231
+ },
3232
+ });
3233
+
3234
+ expect(result.status).toBe(200);
3235
+ await new Promise((r) => setTimeout(r, 100));
3236
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3237
+ expect(errorCalls.some((msg: string) => msg.includes("Planner turn error"))).toBe(true);
3238
+ });
3239
+
3240
+ it("covers plan_finalize audit runPlanAudit .catch when it rejects", async () => {
3241
+ classifyIntentMock.mockResolvedValue({
3242
+ intent: "plan_finalize",
3243
+ reasoning: "Finalize",
3244
+ fromFallback: false,
3245
+ });
3246
+ isInPlanningModeMock.mockReturnValue(true);
3247
+ getPlanningSessionMock.mockReturnValue({
3248
+ projectId: "proj-aud-catch",
3249
+ projectName: "Audit Catch",
3250
+ rootIssueId: "root-aud-catch",
3251
+ status: "interviewing",
3252
+ });
3253
+ runPlanAuditMock.mockRejectedValue(new Error("audit fail"));
3254
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3255
+ id: "issue-aud-catch",
3256
+ identifier: "ENG-AC",
3257
+ title: "Audit Catch",
3258
+ state: { name: "Backlog" },
3259
+ project: { id: "proj-aud-catch" },
3260
+ });
3261
+
3262
+ const result = await postWebhook({
3263
+ type: "Comment",
3264
+ action: "create",
3265
+ data: {
3266
+ id: "comment-aud-catch",
3267
+ body: "Finalize",
3268
+ user: { id: "human-ac", name: "Human" },
3269
+ issue: { id: "issue-aud-catch", identifier: "ENG-AC", project: { id: "proj-aud-catch" } },
3270
+ },
3271
+ });
3272
+
3273
+ expect(result.status).toBe(200);
3274
+ await new Promise((r) => setTimeout(r, 100));
3275
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3276
+ expect(errorCalls.some((msg: string) => msg.includes("Plan audit error"))).toBe(true);
3277
+ });
3278
+
3279
+ it("covers plan_continue handlePlannerTurn .catch when it rejects", async () => {
3280
+ classifyIntentMock.mockResolvedValue({
3281
+ intent: "plan_continue",
3282
+ reasoning: "Continue",
3283
+ fromFallback: false,
3284
+ });
3285
+ isInPlanningModeMock.mockReturnValue(true);
3286
+ getPlanningSessionMock.mockReturnValue({
3287
+ projectId: "proj-cont-catch",
3288
+ projectName: "Continue Catch",
3289
+ rootIssueId: "root-cont-catch",
3290
+ status: "interviewing",
3291
+ });
3292
+ handlePlannerTurnMock.mockRejectedValue(new Error("planner continue fail"));
3293
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3294
+ id: "issue-cont-catch",
3295
+ identifier: "ENG-CC",
3296
+ title: "Continue Catch",
3297
+ state: { name: "Backlog" },
3298
+ project: { id: "proj-cont-catch" },
3299
+ });
3300
+
3301
+ const result = await postWebhook({
3302
+ type: "Comment",
3303
+ action: "create",
3304
+ data: {
3305
+ id: "comment-cont-catch",
3306
+ body: "Add more",
3307
+ user: { id: "human-cc", name: "Human" },
3308
+ issue: { id: "issue-cont-catch", identifier: "ENG-CC", project: { id: "proj-cont-catch" } },
3309
+ },
3310
+ });
3311
+
3312
+ expect(result.status).toBe(200);
3313
+ await new Promise((r) => setTimeout(r, 100));
3314
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3315
+ expect(errorCalls.some((msg: string) => msg.includes("Planner turn error"))).toBe(true);
3316
+ });
3317
+
3318
+ it("covers plan_continue dispatchCommentToAgent .catch when not planning", async () => {
3319
+ classifyIntentMock.mockResolvedValue({
3320
+ intent: "plan_continue",
3321
+ reasoning: "Continue",
3322
+ fromFallback: false,
3323
+ });
3324
+ runAgentMock.mockRejectedValue(new Error("dispatch fail in plan_continue"));
3325
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3326
+ id: "issue-cont-noplan-catch",
3327
+ identifier: "ENG-CNC",
3328
+ title: "Continue No Plan Catch",
3329
+ state: { name: "Backlog", type: "backlog" },
3330
+ project: null,
3331
+ team: { id: "team-cnc" },
3332
+ comments: { nodes: [] },
3333
+ });
3334
+
3335
+ const result = await postWebhook({
3336
+ type: "Comment",
3337
+ action: "create",
3338
+ data: {
3339
+ id: "comment-cont-noplan-catch",
3340
+ body: "Continue",
3341
+ user: { id: "human-cnc", name: "Human" },
3342
+ issue: { id: "issue-cont-noplan-catch", identifier: "ENG-CNC" },
3343
+ },
3344
+ });
3345
+
3346
+ expect(result.status).toBe(200);
3347
+ await new Promise((r) => setTimeout(r, 300));
3348
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3349
+ expect(errorCalls.some((msg: string) =>
3350
+ msg.includes("Comment dispatch error") || msg.includes("dispatchCommentToAgent error")
3351
+ )).toBe(true);
3352
+ });
3353
+
3354
+ it("covers ask_agent dispatchCommentToAgent .catch", async () => {
3355
+ classifyIntentMock.mockResolvedValue({
3356
+ intent: "ask_agent",
3357
+ agentId: "kaylee",
3358
+ reasoning: "Ask",
3359
+ fromFallback: false,
3360
+ });
3361
+ runAgentMock.mockRejectedValue(new Error("ask agent fail"));
3362
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3363
+ id: "issue-aa-catch",
3364
+ identifier: "ENG-AAC",
3365
+ title: "Ask Agent Catch",
3366
+ description: "desc",
3367
+ state: { name: "Backlog", type: "backlog" },
3368
+ team: { id: "team-aac" },
3369
+ comments: { nodes: [] },
3370
+ });
3371
+
3372
+ const result = await postWebhook({
3373
+ type: "Comment",
3374
+ action: "create",
3375
+ data: {
3376
+ id: "comment-aa-catch",
3377
+ body: "Ask kaylee",
3378
+ user: { id: "human-aac", name: "Human" },
3379
+ issue: { id: "issue-aa-catch", identifier: "ENG-AAC" },
3380
+ },
3381
+ });
3382
+
3383
+ expect(result.status).toBe(200);
3384
+ await new Promise((r) => setTimeout(r, 300));
3385
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3386
+ expect(errorCalls.some((msg: string) =>
3387
+ msg.includes("Comment dispatch error") || msg.includes("dispatchCommentToAgent error")
3388
+ )).toBe(true);
3389
+ });
3390
+
3391
+ it("covers request_work/question dispatchCommentToAgent .catch", async () => {
3392
+ classifyIntentMock.mockResolvedValue({
3393
+ intent: "request_work",
3394
+ reasoning: "Work",
3395
+ fromFallback: false,
3396
+ });
3397
+ runAgentMock.mockRejectedValue(new Error("request work fail"));
3398
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3399
+ id: "issue-rw-catch",
3400
+ identifier: "ENG-RWC",
3401
+ title: "Request Work Catch",
3402
+ description: "desc",
3403
+ state: { name: "Backlog", type: "backlog" },
3404
+ team: { id: "team-rwc" },
3405
+ comments: { nodes: [] },
3406
+ });
3407
+
3408
+ const result = await postWebhook({
3409
+ type: "Comment",
3410
+ action: "create",
3411
+ data: {
3412
+ id: "comment-rw-catch",
3413
+ body: "Do work",
3414
+ user: { id: "human-rwc", name: "Human" },
3415
+ issue: { id: "issue-rw-catch", identifier: "ENG-RWC" },
3416
+ },
3417
+ });
3418
+
3419
+ expect(result.status).toBe(200);
3420
+ await new Promise((r) => setTimeout(r, 300));
3421
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3422
+ expect(errorCalls.some((msg: string) =>
3423
+ msg.includes("Comment dispatch error") || msg.includes("dispatchCommentToAgent error")
3424
+ )).toBe(true);
3425
+ });
3426
+
3427
+ it("covers close_issue handleCloseIssue .catch", async () => {
3428
+ classifyIntentMock.mockResolvedValue({
3429
+ intent: "close_issue",
3430
+ reasoning: "Close",
3431
+ fromFallback: false,
3432
+ });
3433
+ runAgentMock.mockRejectedValue(new Error("close issue fail"));
3434
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3435
+ id: "issue-ci-catch",
3436
+ identifier: "ENG-CIC",
3437
+ title: "Close Catch",
3438
+ description: "desc",
3439
+ state: { name: "In Progress", type: "started" },
3440
+ team: { id: "team-cic" },
3441
+ comments: { nodes: [] },
3442
+ });
3443
+
3444
+ const result = await postWebhook({
3445
+ type: "Comment",
3446
+ action: "create",
3447
+ data: {
3448
+ id: "comment-ci-catch",
3449
+ body: "Close it",
3450
+ user: { id: "human-cic", name: "Human" },
3451
+ issue: { id: "issue-ci-catch", identifier: "ENG-CIC" },
3452
+ },
3453
+ });
3454
+
3455
+ expect(result.status).toBe(200);
3456
+ await new Promise((r) => setTimeout(r, 300));
3457
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3458
+ expect(errorCalls.some((msg: string) =>
3459
+ msg.includes("Close issue error") || msg.includes("handleCloseIssue error")
3460
+ )).toBe(true);
3461
+ });
3462
+
3463
+ it("covers getViewerId .catch path when it fails", async () => {
3464
+ mockLinearApiInstance.getViewerId.mockRejectedValue(new Error("viewer fail"));
3465
+
3466
+ const result = await postWebhook({
3467
+ type: "Comment",
3468
+ action: "create",
3469
+ data: {
3470
+ id: "comment-viewer-fail",
3471
+ body: "Hello",
3472
+ user: { id: "human-vf", name: "Human" },
3473
+ issue: { id: "issue-viewer-fail", identifier: "ENG-VF" },
3474
+ },
3475
+ });
3476
+
3477
+ expect(result.status).toBe(200);
3478
+ // Should proceed to intent classification despite getViewerId failure
3479
+ expect(classifyIntentMock).toHaveBeenCalled();
3480
+ });
3481
+
3482
+ it("covers getIssueDetails failure in Comment.create intent classification", async () => {
3483
+ mockLinearApiInstance.getIssueDetails.mockRejectedValue(new Error("fetch details fail"));
3484
+ classifyIntentMock.mockResolvedValue({
3485
+ intent: "general",
3486
+ reasoning: "Just a general comment",
3487
+ fromFallback: false,
3488
+ });
3489
+
3490
+ const result = await postWebhook({
3491
+ type: "Comment",
3492
+ action: "create",
3493
+ data: {
3494
+ id: "comment-details-fail",
3495
+ body: "Just a note",
3496
+ user: { id: "human-df", name: "Human" },
3497
+ issue: { id: "issue-details-fail", identifier: "ENG-DFL" },
3498
+ },
3499
+ });
3500
+
3501
+ expect(result.status).toBe(200);
3502
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
3503
+ expect(warnCalls.some((msg: string) => msg.includes("Could not fetch issue details"))).toBe(true);
3504
+ });
3505
+
3506
+ it("covers readPlanningState failure in Comment.create", async () => {
3507
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3508
+ id: "issue-plan-fail",
3509
+ identifier: "ENG-PLF",
3510
+ title: "Planning Fail",
3511
+ state: { name: "Backlog" },
3512
+ project: { id: "proj-plan-fail" },
3513
+ team: { id: "team-plf" },
3514
+ });
3515
+ readPlanningStateMock.mockRejectedValue(new Error("planning state read fail"));
3516
+ classifyIntentMock.mockResolvedValue({
3517
+ intent: "general",
3518
+ reasoning: "Just general",
3519
+ fromFallback: false,
3520
+ });
3521
+
3522
+ const result = await postWebhook({
3523
+ type: "Comment",
3524
+ action: "create",
3525
+ data: {
3526
+ id: "comment-plan-fail",
3527
+ body: "Some comment",
3528
+ user: { id: "human-plf", name: "Human" },
3529
+ issue: { id: "issue-plan-fail", identifier: "ENG-PLF", project: { id: "proj-plan-fail" } },
3530
+ },
3531
+ });
3532
+
3533
+ expect(result.status).toBe(200);
3534
+ // Should proceed despite planning state read failure
3535
+ expect(classifyIntentMock).toHaveBeenCalled();
3536
+ });
3537
+ });
3538
+
3539
+ // ---------------------------------------------------------------------------
3540
+ // Coverage push: dispatchCommentToAgent internal .catch callbacks
3541
+ // ---------------------------------------------------------------------------
3542
+
3543
+ describe("dispatchCommentToAgent internal .catch callbacks", () => {
3544
+ it("covers thought emitActivity .catch in dispatchCommentToAgent", async () => {
3545
+ classifyIntentMock.mockResolvedValue({
3546
+ intent: "request_work",
3547
+ reasoning: "Work request",
3548
+ fromFallback: false,
3549
+ });
3550
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3551
+ if (content.type === "thought") return Promise.reject(new Error("thought fail"));
3552
+ if (content.type === "response") return Promise.resolve(undefined);
3553
+ return Promise.resolve(undefined);
3554
+ });
3555
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3556
+ id: "issue-dca-thought",
3557
+ identifier: "ENG-DCT",
3558
+ title: "DCA Thought",
3559
+ description: "desc",
3560
+ state: { name: "In Progress", type: "started" },
3561
+ team: { id: "team-dct" },
3562
+ comments: { nodes: [] },
3563
+ });
3564
+
3565
+ const result = await postWebhook({
3566
+ type: "Comment",
3567
+ action: "create",
3568
+ data: {
3569
+ id: "comment-dca-thought",
3570
+ body: "Do this work",
3571
+ user: { id: "human-dct", name: "Human" },
3572
+ issue: { id: "issue-dca-thought", identifier: "ENG-DCT" },
3573
+ },
3574
+ });
3575
+
3576
+ expect(result.status).toBe(200);
3577
+ await new Promise((r) => setTimeout(r, 300));
3578
+ expect(runAgentMock).toHaveBeenCalled();
3579
+ });
3580
+
3581
+ it("covers error emitActivity .catch in dispatchCommentToAgent", async () => {
3582
+ classifyIntentMock.mockResolvedValue({
3583
+ intent: "request_work",
3584
+ reasoning: "Work request",
3585
+ fromFallback: false,
3586
+ });
3587
+ runAgentMock.mockRejectedValue(new Error("agent exploded in dispatch"));
3588
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3589
+ if (content.type === "error") return Promise.reject(new Error("error emit also fails"));
3590
+ return Promise.resolve(undefined);
3591
+ });
3592
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3593
+ id: "issue-dca-err",
3594
+ identifier: "ENG-DCE",
3595
+ title: "DCA Error",
3596
+ description: "desc",
3597
+ state: { name: "Backlog", type: "backlog" },
3598
+ team: { id: "team-dce" },
3599
+ comments: { nodes: [] },
3600
+ });
3601
+
3602
+ const result = await postWebhook({
3603
+ type: "Comment",
3604
+ action: "create",
3605
+ data: {
3606
+ id: "comment-dca-err",
3607
+ body: "Do work",
3608
+ user: { id: "human-dce", name: "Human" },
3609
+ issue: { id: "issue-dca-err", identifier: "ENG-DCE" },
3610
+ },
3611
+ });
3612
+
3613
+ expect(result.status).toBe(200);
3614
+ await new Promise((r) => setTimeout(r, 300));
3615
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3616
+ expect(errorCalls.some((msg: string) => msg.includes("dispatchCommentToAgent error"))).toBe(true);
3617
+ });
3618
+ });
3619
+
3620
+ // ---------------------------------------------------------------------------
3621
+ // Coverage push: handleCloseIssue .catch callbacks
3622
+ // ---------------------------------------------------------------------------
3623
+
3624
+ describe("handleCloseIssue .catch callbacks", () => {
3625
+ it("covers thought emitActivity .catch in handleCloseIssue", async () => {
3626
+ classifyIntentMock.mockResolvedValue({
3627
+ intent: "close_issue",
3628
+ reasoning: "Close it",
3629
+ fromFallback: false,
3630
+ });
3631
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3632
+ if (content.type === "thought") return Promise.reject(new Error("thought fail close"));
3633
+ if (content.type === "response") return Promise.resolve(undefined);
3634
+ return Promise.resolve(undefined);
3635
+ });
3636
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3637
+ id: "issue-close-thought",
3638
+ identifier: "ENG-CLT",
3639
+ title: "Close Thought Catch",
3640
+ description: "desc",
3641
+ state: { name: "In Progress", type: "started" },
3642
+ team: { id: "team-clt" },
3643
+ comments: { nodes: [] },
3644
+ creator: { name: "Creator" },
3645
+ });
3646
+
3647
+ const result = await postWebhook({
3648
+ type: "Comment",
3649
+ action: "create",
3650
+ data: {
3651
+ id: "comment-close-thought",
3652
+ body: "Close this issue",
3653
+ user: { id: "human-clt", name: "Human" },
3654
+ issue: { id: "issue-close-thought", identifier: "ENG-CLT" },
3655
+ },
3656
+ });
3657
+
3658
+ expect(result.status).toBe(200);
3659
+ await new Promise((r) => setTimeout(r, 300));
3660
+ expect(runAgentMock).toHaveBeenCalled();
3661
+ });
3662
+
3663
+ it("covers response emitActivity .then/.catch in handleCloseIssue", async () => {
3664
+ classifyIntentMock.mockResolvedValue({
3665
+ intent: "close_issue",
3666
+ reasoning: "Close",
3667
+ fromFallback: false,
3668
+ });
3669
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3670
+ if (content.type === "response") return Promise.reject(new Error("response fail close"));
3671
+ return Promise.resolve(undefined);
3672
+ });
3673
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3674
+ id: "issue-close-resp",
3675
+ identifier: "ENG-CLR",
3676
+ title: "Close Resp Catch",
3677
+ description: "desc",
3678
+ state: { name: "In Progress", type: "started" },
3679
+ team: { id: "team-clr" },
3680
+ comments: { nodes: [] },
3681
+ });
3682
+
3683
+ const result = await postWebhook({
3684
+ type: "Comment",
3685
+ action: "create",
3686
+ data: {
3687
+ id: "comment-close-resp",
3688
+ body: "Close now",
3689
+ user: { id: "human-clr", name: "Human" },
3690
+ issue: { id: "issue-close-resp", identifier: "ENG-CLR" },
3691
+ },
3692
+ });
3693
+
3694
+ expect(result.status).toBe(200);
3695
+ await new Promise((r) => setTimeout(r, 300));
3696
+ // Should fall back to comment
3697
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
3698
+ });
3699
+
3700
+ it("covers error emitActivity .catch in handleCloseIssue", async () => {
3701
+ classifyIntentMock.mockResolvedValue({
3702
+ intent: "close_issue",
3703
+ reasoning: "Close",
3704
+ fromFallback: false,
3705
+ });
3706
+ runAgentMock.mockRejectedValue(new Error("close agent crash"));
3707
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3708
+ if (content.type === "error") return Promise.reject(new Error("error emit fail close"));
3709
+ return Promise.resolve(undefined);
3710
+ });
3711
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3712
+ id: "issue-close-err-catch",
3713
+ identifier: "ENG-CLEC",
3714
+ title: "Close Error Catch",
3715
+ description: "desc",
3716
+ state: { name: "In Progress", type: "started" },
3717
+ team: { id: "team-clec" },
3718
+ comments: { nodes: [] },
3719
+ });
3720
+
3721
+ const result = await postWebhook({
3722
+ type: "Comment",
3723
+ action: "create",
3724
+ data: {
3725
+ id: "comment-close-err-catch",
3726
+ body: "Close",
3727
+ user: { id: "human-clec", name: "Human" },
3728
+ issue: { id: "issue-close-err-catch", identifier: "ENG-CLEC" },
3729
+ },
3730
+ });
3731
+
3732
+ expect(result.status).toBe(200);
3733
+ await new Promise((r) => setTimeout(r, 300));
3734
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3735
+ expect(errorCalls.some((msg: string) => msg.includes("handleCloseIssue error"))).toBe(true);
3736
+ });
3737
+
3738
+ it("covers handleCloseIssue with no session available", async () => {
3739
+ classifyIntentMock.mockResolvedValue({
3740
+ intent: "close_issue",
3741
+ reasoning: "Close",
3742
+ fromFallback: false,
3743
+ });
3744
+ mockLinearApiInstance.createSessionOnIssue.mockResolvedValue({ sessionId: null });
3745
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3746
+ id: "issue-close-nosess",
3747
+ identifier: "ENG-CLNS",
3748
+ title: "Close No Sess",
3749
+ description: "desc",
3750
+ state: { name: "In Progress", type: "started" },
3751
+ team: { id: "team-clns" },
3752
+ comments: { nodes: [] },
3753
+ });
3754
+
3755
+ const result = await postWebhook({
3756
+ type: "Comment",
3757
+ action: "create",
3758
+ data: {
3759
+ id: "comment-close-nosess",
3760
+ body: "Close this",
3761
+ user: { id: "human-clns", name: "Human" },
3762
+ issue: { id: "issue-close-nosess", identifier: "ENG-CLNS" },
3763
+ },
3764
+ });
3765
+
3766
+ expect(result.status).toBe(200);
3767
+ await new Promise((r) => setTimeout(r, 300));
3768
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
3769
+ });
3770
+
3771
+ it("covers handleCloseIssue with no team (no completed state lookup)", async () => {
3772
+ classifyIntentMock.mockResolvedValue({
3773
+ intent: "close_issue",
3774
+ reasoning: "Close",
3775
+ fromFallback: false,
3776
+ });
3777
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3778
+ id: "issue-close-noteam",
3779
+ identifier: "ENG-CLNT",
3780
+ title: "Close No Team",
3781
+ description: "desc",
3782
+ state: { name: "In Progress", type: "started" },
3783
+ comments: { nodes: [] },
3784
+ });
3785
+
3786
+ const result = await postWebhook({
3787
+ type: "Comment",
3788
+ action: "create",
3789
+ data: {
3790
+ id: "comment-close-noteam",
3791
+ body: "Close this",
3792
+ user: { id: "human-clnt", name: "Human" },
3793
+ issue: { id: "issue-close-noteam", identifier: "ENG-CLNT" },
3794
+ },
3795
+ });
3796
+
3797
+ expect(result.status).toBe(200);
3798
+ await new Promise((r) => setTimeout(r, 300));
3799
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
3800
+ expect(warnCalls.some((msg: string) => msg.includes("No completed state found"))).toBe(true);
3801
+ });
3802
+
3803
+ it("covers handleCloseIssue getTeamStates failure", async () => {
3804
+ classifyIntentMock.mockResolvedValue({
3805
+ intent: "close_issue",
3806
+ reasoning: "Close",
3807
+ fromFallback: false,
3808
+ });
3809
+ mockLinearApiInstance.getTeamStates.mockRejectedValue(new Error("states fail"));
3810
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3811
+ id: "issue-close-states-fail",
3812
+ identifier: "ENG-CLSF",
3813
+ title: "Close States Fail",
3814
+ description: "desc",
3815
+ state: { name: "In Progress", type: "started" },
3816
+ team: { id: "team-clsf" },
3817
+ comments: { nodes: [] },
3818
+ });
3819
+
3820
+ const result = await postWebhook({
3821
+ type: "Comment",
3822
+ action: "create",
3823
+ data: {
3824
+ id: "comment-close-states-fail",
3825
+ body: "Close",
3826
+ user: { id: "human-clsf", name: "Human" },
3827
+ issue: { id: "issue-close-states-fail", identifier: "ENG-CLSF" },
3828
+ },
3829
+ });
3830
+
3831
+ expect(result.status).toBe(200);
3832
+ await new Promise((r) => setTimeout(r, 300));
3833
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
3834
+ expect(warnCalls.some((msg: string) => msg.includes("Could not fetch team states"))).toBe(true);
3835
+ });
3836
+
3837
+ it("covers handleCloseIssue updateIssue failure for state transition", async () => {
3838
+ classifyIntentMock.mockResolvedValue({
3839
+ intent: "close_issue",
3840
+ reasoning: "Close",
3841
+ fromFallback: false,
3842
+ });
3843
+ mockLinearApiInstance.updateIssue.mockRejectedValue(new Error("update fail"));
3844
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3845
+ id: "issue-close-update-fail",
3846
+ identifier: "ENG-CLUF",
3847
+ title: "Close Update Fail",
3848
+ description: "desc",
3849
+ state: { name: "In Progress", type: "started" },
3850
+ team: { id: "team-cluf" },
3851
+ comments: { nodes: [] },
3852
+ });
3853
+ mockLinearApiInstance.getTeamStates.mockResolvedValue([
3854
+ { id: "st-done", name: "Done", type: "completed" },
3855
+ ]);
3856
+
3857
+ const result = await postWebhook({
3858
+ type: "Comment",
3859
+ action: "create",
3860
+ data: {
3861
+ id: "comment-close-update-fail",
3862
+ body: "Close",
3863
+ user: { id: "human-cluf", name: "Human" },
3864
+ issue: { id: "issue-close-update-fail", identifier: "ENG-CLUF" },
3865
+ },
3866
+ });
3867
+
3868
+ expect(result.status).toBe(200);
3869
+ await new Promise((r) => setTimeout(r, 300));
3870
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
3871
+ expect(errorCalls.some((msg: string) => msg.includes("Failed to transition issue"))).toBe(true);
3872
+ });
3873
+
3874
+ it("covers handleCloseIssue with no avatarUrl and no session", async () => {
3875
+ classifyIntentMock.mockResolvedValue({
3876
+ intent: "close_issue",
3877
+ reasoning: "Close",
3878
+ fromFallback: false,
3879
+ });
3880
+ loadAgentProfilesMock.mockReturnValue({
3881
+ mal: { label: "Mal", mission: "captain", mentionAliases: ["mal"], isDefault: true },
3882
+ });
3883
+ mockLinearApiInstance.createSessionOnIssue.mockResolvedValue({ sessionId: null });
3884
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3885
+ id: "issue-close-no-av",
3886
+ identifier: "ENG-CLNAV",
3887
+ title: "Close No Avatar",
3888
+ description: "desc",
3889
+ state: { name: "In Progress", type: "started" },
3890
+ team: { id: "team-clnav" },
3891
+ comments: { nodes: [] },
3892
+ });
3893
+
3894
+ const result = await postWebhook({
3895
+ type: "Comment",
3896
+ action: "create",
3897
+ data: {
3898
+ id: "comment-close-no-av",
3899
+ body: "Close",
3900
+ user: { id: "human-clnav", name: "Human" },
3901
+ issue: { id: "issue-close-no-av", identifier: "ENG-CLNAV" },
3902
+ },
3903
+ });
3904
+
3905
+ expect(result.status).toBe(200);
3906
+ await new Promise((r) => setTimeout(r, 300));
3907
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
3908
+ });
3909
+
3910
+ it("covers handleCloseIssue getIssueDetails failure", async () => {
3911
+ classifyIntentMock.mockResolvedValue({
3912
+ intent: "close_issue",
3913
+ reasoning: "Close",
3914
+ fromFallback: false,
3915
+ });
3916
+ let callCount = 0;
3917
+ mockLinearApiInstance.getIssueDetails.mockImplementation(() => {
3918
+ callCount++;
3919
+ if (callCount === 1) {
3920
+ return Promise.resolve({
3921
+ id: "issue-close-details-fail",
3922
+ identifier: "ENG-CLDF",
3923
+ title: "Close Details Fail",
3924
+ state: { name: "In Progress", type: "started" },
3925
+ team: { id: "team-cldf" },
3926
+ comments: { nodes: [] },
3927
+ });
3928
+ }
3929
+ return Promise.reject(new Error("second fetch fail"));
3930
+ });
3931
+
3932
+ const result = await postWebhook({
3933
+ type: "Comment",
3934
+ action: "create",
3935
+ data: {
3936
+ id: "comment-close-details-fail",
3937
+ body: "Close this",
3938
+ user: { id: "human-cldf", name: "Human" },
3939
+ issue: { id: "issue-close-details-fail", identifier: "ENG-CLDF" },
3940
+ },
3941
+ });
3942
+
3943
+ expect(result.status).toBe(200);
3944
+ await new Promise((r) => setTimeout(r, 300));
3945
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
3946
+ expect(warnCalls.some((msg: string) => msg.includes("Could not fetch issue details"))).toBe(true);
3947
+ });
3948
+ });
3949
+
3950
+ // ---------------------------------------------------------------------------
3951
+ // Coverage push: Issue.create triage .catch callbacks
3952
+ // ---------------------------------------------------------------------------
3953
+
3954
+ describe("Issue.create triage .catch callbacks", () => {
3955
+ it("covers emitActivity thought .catch during triage", async () => {
3956
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3957
+ if (content.type === "thought") return Promise.reject(new Error("thought fail triage"));
3958
+ if (content.type === "response") return Promise.resolve(undefined);
3959
+ if (content.type === "action") return Promise.reject(new Error("action fail"));
3960
+ return Promise.resolve(undefined);
3961
+ });
3962
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3963
+ id: "issue-triage-thought",
3964
+ identifier: "ENG-TT",
3965
+ title: "Triage Thought",
3966
+ description: "desc",
3967
+ state: { name: "Backlog", type: "backlog" },
3968
+ team: { id: "team-1", issueEstimationType: "fibonacci" },
3969
+ labels: { nodes: [] },
3970
+ creatorId: "creator-1",
3971
+ creator: { name: "Dev" },
3972
+ });
3973
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
3974
+ runAgentMock.mockResolvedValue({
3975
+ success: true,
3976
+ output: '```json\n{"estimate": 3, "labelIds": [], "priority": 2, "assessment": "Medium"}\n```\nAssessment here.',
3977
+ });
3978
+
3979
+ const result = await postWebhook({
3980
+ type: "Issue",
3981
+ action: "create",
3982
+ data: { id: "issue-triage-thought", identifier: "ENG-TT", title: "Triage Thought", creatorId: "creator-1" },
3983
+ });
3984
+
3985
+ expect(result.status).toBe(200);
3986
+ await new Promise((r) => setTimeout(r, 200));
3987
+ expect(runAgentMock).toHaveBeenCalled();
3988
+ });
3989
+
3990
+ it("covers emitActivity response .then/.catch during triage", async () => {
3991
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
3992
+ if (content.type === "response") return Promise.reject(new Error("response fail triage"));
3993
+ return Promise.resolve(undefined);
3994
+ });
3995
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
3996
+ id: "issue-triage-resp",
3997
+ identifier: "ENG-TR2",
3998
+ title: "Triage Resp",
3999
+ description: "desc",
4000
+ state: { name: "Backlog", type: "backlog" },
4001
+ team: { id: "team-1" },
4002
+ labels: { nodes: [] },
4003
+ creatorId: "creator-1",
4004
+ });
4005
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4006
+ runAgentMock.mockResolvedValue({
4007
+ success: true,
4008
+ output: "Simple triage without JSON block",
4009
+ });
4010
+
4011
+ const result = await postWebhook({
4012
+ type: "Issue",
4013
+ action: "create",
4014
+ data: { id: "issue-triage-resp", identifier: "ENG-TR2", title: "Triage Resp", creatorId: "creator-1" },
4015
+ });
4016
+
4017
+ expect(result.status).toBe(200);
4018
+ await new Promise((r) => setTimeout(r, 200));
4019
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
4020
+ });
4021
+
4022
+ it("covers error emitActivity .catch during triage exception", async () => {
4023
+ runAgentMock.mockRejectedValue(new Error("triage agent crash"));
4024
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
4025
+ if (content.type === "error") return Promise.reject(new Error("error emit fail triage"));
4026
+ return Promise.resolve(undefined);
4027
+ });
4028
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4029
+ id: "issue-triage-err-catch",
4030
+ identifier: "ENG-TEC",
4031
+ title: "Triage Error Catch",
4032
+ description: "desc",
4033
+ state: { name: "Backlog", type: "backlog" },
4034
+ team: { id: "team-1" },
4035
+ labels: { nodes: [] },
4036
+ creatorId: "creator-1",
4037
+ });
4038
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4039
+
4040
+ const result = await postWebhook({
4041
+ type: "Issue",
4042
+ action: "create",
4043
+ data: { id: "issue-triage-err-catch", identifier: "ENG-TEC", title: "Triage Error Catch", creatorId: "creator-1" },
4044
+ });
4045
+
4046
+ expect(result.status).toBe(200);
4047
+ await new Promise((r) => setTimeout(r, 200));
4048
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
4049
+ expect(errorCalls.some((msg: string) => msg.includes("triage error"))).toBe(true);
4050
+ });
4051
+
4052
+ it("covers triage with JSON containing priority, labels, and estimate", async () => {
4053
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4054
+ id: "issue-triage-json",
4055
+ identifier: "ENG-TJ",
4056
+ title: "Triage JSON",
4057
+ description: "desc",
4058
+ state: { name: "Backlog", type: "backlog" },
4059
+ team: { id: "team-1", issueEstimationType: "fibonacci" },
4060
+ labels: { nodes: [{ id: "existing-label", name: "existing" }] },
4061
+ creatorId: "creator-1",
4062
+ creator: { name: "Dev", email: "dev@test.com" },
4063
+ });
4064
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4065
+ mockLinearApiInstance.getTeamLabels.mockResolvedValue([
4066
+ { id: "label-bug", name: "bug" },
4067
+ { id: "label-feature", name: "feature" },
4068
+ ]);
4069
+ runAgentMock.mockResolvedValue({
4070
+ success: true,
4071
+ output: '```json\n{"estimate": 5, "labelIds": ["label-bug"], "priority": 1, "assessment": "High priority bug"}\n```\n\nThis is a high priority bug.',
4072
+ });
4073
+
4074
+ const result = await postWebhook({
4075
+ type: "Issue",
4076
+ action: "create",
4077
+ data: { id: "issue-triage-json", identifier: "ENG-TJ", title: "Triage JSON", creatorId: "creator-1" },
4078
+ });
4079
+
4080
+ expect(result.status).toBe(200);
4081
+ await new Promise((r) => setTimeout(r, 200));
4082
+ expect(mockLinearApiInstance.updateIssue).toHaveBeenCalled();
4083
+ const updateCall = mockLinearApiInstance.updateIssue.mock.calls[0];
4084
+ expect(updateCall[1]).toEqual(expect.objectContaining({
4085
+ estimate: 5,
4086
+ priority: 1,
4087
+ labelIds: expect.arrayContaining(["existing-label", "label-bug"]),
4088
+ }));
4089
+ });
4090
+
4091
+ it("covers triage JSON parse failure", async () => {
4092
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4093
+ id: "issue-triage-bad-json",
4094
+ identifier: "ENG-TBJ",
4095
+ title: "Triage Bad JSON",
4096
+ description: "desc",
4097
+ state: { name: "Backlog", type: "backlog" },
4098
+ team: { id: "team-1" },
4099
+ labels: { nodes: [] },
4100
+ creatorId: "creator-1",
4101
+ });
4102
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4103
+ runAgentMock.mockResolvedValue({
4104
+ success: true,
4105
+ output: '```json\n{invalid json here}\n```\n\nAssessment text.',
4106
+ });
4107
+
4108
+ const result = await postWebhook({
4109
+ type: "Issue",
4110
+ action: "create",
4111
+ data: { id: "issue-triage-bad-json", identifier: "ENG-TBJ", title: "Triage Bad JSON", creatorId: "creator-1" },
4112
+ });
4113
+
4114
+ expect(result.status).toBe(200);
4115
+ await new Promise((r) => setTimeout(r, 200));
4116
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
4117
+ expect(warnCalls.some((msg: string) => msg.includes("Could not parse triage JSON"))).toBe(true);
4118
+ });
4119
+
4120
+ it("covers triage with no session and no avatar", async () => {
4121
+ loadAgentProfilesMock.mockReturnValue({
4122
+ mal: { label: "Mal", mission: "captain", mentionAliases: ["mal"], isDefault: true },
4123
+ });
4124
+ mockLinearApiInstance.createSessionOnIssue.mockResolvedValue({ sessionId: null });
4125
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4126
+ id: "issue-triage-no-av",
4127
+ identifier: "ENG-TNA",
4128
+ title: "Triage No Avatar",
4129
+ description: "desc",
4130
+ state: { name: "Backlog", type: "backlog" },
4131
+ team: { id: "team-1" },
4132
+ labels: { nodes: [] },
4133
+ creatorId: "creator-1",
4134
+ });
4135
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4136
+ runAgentMock.mockResolvedValue({
4137
+ success: true,
4138
+ output: "Simple triage text",
4139
+ });
4140
+
4141
+ const result = await postWebhook({
4142
+ type: "Issue",
4143
+ action: "create",
4144
+ data: { id: "issue-triage-no-av", identifier: "ENG-TNA", title: "Triage No Avatar", creatorId: "creator-1" },
4145
+ });
4146
+
4147
+ expect(result.status).toBe(200);
4148
+ await new Promise((r) => setTimeout(r, 200));
4149
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
4150
+ });
4151
+
4152
+ it("covers triage getIssueDetails failure", async () => {
4153
+ mockLinearApiInstance.getIssueDetails.mockRejectedValue(new Error("triage fetch fail"));
4154
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4155
+ runAgentMock.mockResolvedValue({
4156
+ success: true,
4157
+ output: "Triage without details",
4158
+ });
4159
+
4160
+ const result = await postWebhook({
4161
+ type: "Issue",
4162
+ action: "create",
4163
+ data: { id: "issue-triage-fetch-fail", identifier: "ENG-TFF", title: "Triage Fetch Fail", creatorId: "creator-1" },
4164
+ });
4165
+
4166
+ expect(result.status).toBe(200);
4167
+ await new Promise((r) => setTimeout(r, 200));
4168
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
4169
+ expect(warnCalls.some((msg: string) => msg.includes("Could not fetch issue details for triage"))).toBe(true);
4170
+ });
4171
+
4172
+ it("covers triage emitActivity action .catch for applied triage", async () => {
4173
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
4174
+ if (content.type === "action" && content.action === "Applied triage") {
4175
+ return Promise.reject(new Error("action emit fail"));
4176
+ }
4177
+ return Promise.resolve(undefined);
4178
+ });
4179
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4180
+ id: "issue-triage-action",
4181
+ identifier: "ENG-TA",
4182
+ title: "Triage Action",
4183
+ description: "desc",
4184
+ state: { name: "Backlog", type: "backlog" },
4185
+ team: { id: "team-1", issueEstimationType: "fibonacci" },
4186
+ labels: { nodes: [] },
4187
+ creatorId: "creator-1",
4188
+ });
4189
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4190
+ runAgentMock.mockResolvedValue({
4191
+ success: true,
4192
+ output: '```json\n{"estimate": 2, "labelIds": [], "priority": 3, "assessment": "Small"}\n```\nSmall task.',
4193
+ });
4194
+
4195
+ const result = await postWebhook({
4196
+ type: "Issue",
4197
+ action: "create",
4198
+ data: { id: "issue-triage-action", identifier: "ENG-TA", title: "Triage Action", creatorId: "creator-1" },
4199
+ });
4200
+
4201
+ expect(result.status).toBe(200);
4202
+ await new Promise((r) => setTimeout(r, 200));
4203
+ expect(mockLinearApiInstance.updateIssue).toHaveBeenCalled();
4204
+ });
4205
+
4206
+ it("covers triage planning state check failure", async () => {
4207
+ readPlanningStateMock.mockRejectedValue(new Error("planning check fail"));
4208
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4209
+ id: "issue-triage-plan-fail",
4210
+ identifier: "ENG-TPF",
4211
+ title: "Triage Plan Fail",
4212
+ description: "desc",
4213
+ state: { name: "Backlog", type: "backlog" },
4214
+ team: { id: "team-1" },
4215
+ labels: { nodes: [] },
4216
+ project: { id: "proj-triage-fail" },
4217
+ creatorId: "creator-1",
4218
+ });
4219
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4220
+ runAgentMock.mockResolvedValue({ success: true, output: "Triage result" });
4221
+
4222
+ const result = await postWebhook({
4223
+ type: "Issue",
4224
+ action: "create",
4225
+ data: { id: "issue-triage-plan-fail", identifier: "ENG-TPF", title: "Triage Plan Fail", creatorId: "creator-1" },
4226
+ });
4227
+
4228
+ expect(result.status).toBe(200);
4229
+ await new Promise((r) => setTimeout(r, 200));
4230
+ expect(runAgentMock).toHaveBeenCalled();
4231
+ });
4232
+
4233
+ it("covers triage guidance cache lookup", async () => {
4234
+ getCachedGuidanceForTeamMock.mockReturnValue("Cached guidance text");
4235
+ isGuidanceEnabledMock.mockReturnValue(true);
4236
+ formatGuidanceAppendixMock.mockReturnValue("\n## Guidance\nCached guidance text");
4237
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4238
+ id: "issue-triage-guid",
4239
+ identifier: "ENG-TG",
4240
+ title: "Triage Guidance",
4241
+ description: "desc",
4242
+ state: { name: "Backlog", type: "backlog" },
4243
+ team: { id: "team-tg" },
4244
+ labels: { nodes: [] },
4245
+ creatorId: "creator-1",
4246
+ });
4247
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4248
+ runAgentMock.mockResolvedValue({ success: true, output: "Triage with guidance" });
4249
+
4250
+ const result = await postWebhook({
4251
+ type: "Issue",
4252
+ action: "create",
4253
+ data: { id: "issue-triage-guid", identifier: "ENG-TG", title: "Triage Guidance", creatorId: "creator-1" },
4254
+ });
4255
+
4256
+ expect(result.status).toBe(200);
4257
+ await new Promise((r) => setTimeout(r, 200));
4258
+ expect(getCachedGuidanceForTeamMock).toHaveBeenCalledWith("team-tg");
4259
+ expect(runAgentMock).toHaveBeenCalled();
4260
+ });
4261
+ });
4262
+
4263
+ // ---------------------------------------------------------------------------
4264
+ // Coverage push: handleDispatch multi-repo, .catch, .finally
4265
+ // ---------------------------------------------------------------------------
4266
+
4267
+ describe("handleDispatch multi-repo and .catch/.finally", () => {
4268
+ it("covers multi-repo worktree creation path", async () => {
4269
+ isMultiRepoMock.mockReturnValue(true);
4270
+ createMultiWorktreeMock.mockReturnValue({
4271
+ parentPath: "/tmp/multi-wt",
4272
+ worktrees: [
4273
+ { repoName: "api", path: "/tmp/multi-wt/api", branch: "codex/ENG-MULTI", resumed: false },
4274
+ { repoName: "spa", path: "/tmp/multi-wt/spa", branch: "codex/ENG-MULTI", resumed: true },
4275
+ ],
4276
+ });
4277
+ resolveReposMock.mockReturnValue({
4278
+ repos: [
4279
+ { name: "api", path: "/tmp/test/api" },
4280
+ { name: "spa", path: "/tmp/test/spa" },
4281
+ ],
4282
+ source: "issue_markers",
4283
+ });
4284
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4285
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4286
+ id: "issue-multi",
4287
+ identifier: "ENG-MULTI",
4288
+ title: "Multi Repo",
4289
+ description: "Multi repo dispatch",
4290
+ state: { name: "In Progress", type: "started" },
4291
+ team: { id: "team-multi" },
4292
+ labels: { nodes: [] },
4293
+ comments: { nodes: [] },
4294
+ project: null,
4295
+ });
4296
+
4297
+ const result = await postWebhook({
4298
+ type: "Issue",
4299
+ action: "update",
4300
+ data: {
4301
+ id: "issue-multi",
4302
+ identifier: "ENG-MULTI",
4303
+ assigneeId: "viewer-1",
4304
+ },
4305
+ updatedFrom: { assigneeId: null },
4306
+ });
4307
+
4308
+ expect(result.status).toBe(200);
4309
+ await new Promise((r) => setTimeout(r, 500));
4310
+ expect(createMultiWorktreeMock).toHaveBeenCalled();
4311
+ expect(prepareWorkspaceMock).toHaveBeenCalledTimes(2);
4312
+ expect(registerDispatchMock).toHaveBeenCalled();
4313
+ });
4314
+
4315
+ it("covers spawnWorker .catch and .finally when worker fails", async () => {
4316
+ spawnWorkerMock.mockRejectedValue(new Error("pipeline v2 crash"));
4317
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4318
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4319
+ id: "issue-spawn-fail",
4320
+ identifier: "ENG-SF",
4321
+ title: "Spawn Fail",
4322
+ description: "desc",
4323
+ state: { name: "In Progress", type: "started" },
4324
+ team: { id: "team-sf" },
4325
+ labels: { nodes: [] },
4326
+ comments: { nodes: [] },
4327
+ project: null,
4328
+ });
4329
+
4330
+ const result = await postWebhook({
4331
+ type: "Issue",
4332
+ action: "update",
4333
+ data: {
4334
+ id: "issue-spawn-fail",
4335
+ identifier: "ENG-SF",
4336
+ assigneeId: "viewer-1",
4337
+ },
4338
+ updatedFrom: { assigneeId: null },
4339
+ });
4340
+
4341
+ expect(result.status).toBe(200);
4342
+ await new Promise((r) => setTimeout(r, 500));
4343
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
4344
+ expect(errorCalls.some((msg: string) => msg.includes("pipeline v2 failed"))).toBe(true);
4345
+ expect(updateDispatchStatusMock).toHaveBeenCalledWith("ENG-SF", "failed", undefined);
4346
+ expect(clearActiveSessionMock).toHaveBeenCalledWith("issue-spawn-fail");
4347
+ });
4348
+
4349
+ it("covers dispatch emitActivity thought .catch on session", async () => {
4350
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
4351
+ if (content.type === "thought") return Promise.reject(new Error("thought fail dispatch"));
4352
+ return Promise.resolve(undefined);
4353
+ });
4354
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4355
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4356
+ id: "issue-disp-thought",
4357
+ identifier: "ENG-DT",
4358
+ title: "Dispatch Thought",
4359
+ description: "desc",
4360
+ state: { name: "In Progress", type: "started" },
4361
+ team: { id: "team-dt" },
4362
+ labels: { nodes: [] },
4363
+ comments: { nodes: [] },
4364
+ project: null,
4365
+ });
4366
+
4367
+ const result = await postWebhook({
4368
+ type: "Issue",
4369
+ action: "update",
4370
+ data: {
4371
+ id: "issue-disp-thought",
4372
+ identifier: "ENG-DT",
4373
+ assigneeId: "viewer-1",
4374
+ },
4375
+ updatedFrom: { assigneeId: null },
4376
+ });
4377
+
4378
+ expect(result.status).toBe(200);
4379
+ await new Promise((r) => setTimeout(r, 500));
4380
+ expect(spawnWorkerMock).toHaveBeenCalled();
4381
+ });
4382
+
4383
+ it("covers dispatch tier label application with matching label", async () => {
4384
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4385
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4386
+ id: "issue-tier-label",
4387
+ identifier: "ENG-TL",
4388
+ title: "Tier Label",
4389
+ description: "desc",
4390
+ state: { name: "In Progress", type: "started" },
4391
+ team: { id: "team-tl" },
4392
+ labels: { nodes: [{ id: "existing-l1", name: "feature" }] },
4393
+ comments: { nodes: [] },
4394
+ project: null,
4395
+ });
4396
+ mockLinearApiInstance.getTeamLabels.mockResolvedValue([
4397
+ { id: "tier-label-id", name: "developer:medium" },
4398
+ ]);
4399
+
4400
+ const result = await postWebhook({
4401
+ type: "Issue",
4402
+ action: "update",
4403
+ data: {
4404
+ id: "issue-tier-label",
4405
+ identifier: "ENG-TL",
4406
+ assigneeId: "viewer-1",
4407
+ },
4408
+ updatedFrom: { assigneeId: null },
4409
+ });
4410
+
4411
+ expect(result.status).toBe(200);
4412
+ await new Promise((r) => setTimeout(r, 500));
4413
+ const updateCalls = mockLinearApiInstance.updateIssue.mock.calls;
4414
+ const tierLabelCall = updateCalls.find((c: any[]) =>
4415
+ c[1]?.labelIds?.includes("tier-label-id")
4416
+ );
4417
+ expect(tierLabelCall).toBeDefined();
4418
+ });
4419
+
4420
+ it("covers dispatch tier label failure (best effort)", async () => {
4421
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4422
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4423
+ id: "issue-tier-fail",
4424
+ identifier: "ENG-TLFAIL",
4425
+ title: "Tier Fail",
4426
+ description: "desc",
4427
+ state: { name: "In Progress", type: "started" },
4428
+ team: { id: "team-tlfail" },
4429
+ labels: { nodes: [] },
4430
+ comments: { nodes: [] },
4431
+ project: null,
4432
+ });
4433
+ mockLinearApiInstance.getTeamLabels.mockRejectedValue(new Error("team labels fail"));
4434
+
4435
+ const result = await postWebhook({
4436
+ type: "Issue",
4437
+ action: "update",
4438
+ data: {
4439
+ id: "issue-tier-fail",
4440
+ identifier: "ENG-TLFAIL",
4441
+ assigneeId: "viewer-1",
4442
+ },
4443
+ updatedFrom: { assigneeId: null },
4444
+ });
4445
+
4446
+ expect(result.status).toBe(200);
4447
+ await new Promise((r) => setTimeout(r, 500));
4448
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
4449
+ expect(warnCalls.some((msg: string) => msg.includes("could not apply tier label"))).toBe(true);
4450
+ });
4451
+
4452
+ it("covers handleDispatch planning mode check preventing dispatch", async () => {
4453
+ isInPlanningModeMock.mockReturnValue(true);
4454
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4455
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4456
+ id: "issue-plan-block",
4457
+ identifier: "ENG-PB",
4458
+ title: "Plan Block",
4459
+ description: "desc",
4460
+ state: { name: "In Progress", type: "started" },
4461
+ team: { id: "team-pb" },
4462
+ labels: { nodes: [] },
4463
+ comments: { nodes: [] },
4464
+ project: { id: "proj-plan-block" },
4465
+ });
4466
+
4467
+ const result = await postWebhook({
4468
+ type: "Issue",
4469
+ action: "update",
4470
+ data: {
4471
+ id: "issue-plan-block",
4472
+ identifier: "ENG-PB",
4473
+ assigneeId: "viewer-1",
4474
+ },
4475
+ updatedFrom: { assigneeId: null },
4476
+ });
4477
+
4478
+ expect(result.status).toBe(200);
4479
+ await new Promise((r) => setTimeout(r, 300));
4480
+ const infoCalls = (result.api.logger.info as any).mock.calls.map((c: any[]) => c[0]);
4481
+ expect(infoCalls.some((msg: string) => msg.includes("planning-mode project"))).toBe(true);
4482
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
4483
+ });
4484
+
4485
+ it("covers handleDispatch planning mode check failure", async () => {
4486
+ readPlanningStateMock.mockRejectedValue(new Error("plan state fail"));
4487
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4488
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4489
+ id: "issue-plan-check-fail",
4490
+ identifier: "ENG-PCF",
4491
+ title: "Plan Check Fail",
4492
+ description: "desc",
4493
+ state: { name: "In Progress", type: "started" },
4494
+ team: { id: "team-pcf" },
4495
+ labels: { nodes: [] },
4496
+ comments: { nodes: [] },
4497
+ project: { id: "proj-plan-check-fail" },
4498
+ });
4499
+
4500
+ const result = await postWebhook({
4501
+ type: "Issue",
4502
+ action: "update",
4503
+ data: {
4504
+ id: "issue-plan-check-fail",
4505
+ identifier: "ENG-PCF",
4506
+ assigneeId: "viewer-1",
4507
+ },
4508
+ updatedFrom: { assigneeId: null },
4509
+ });
4510
+
4511
+ expect(result.status).toBe(200);
4512
+ await new Promise((r) => setTimeout(r, 500));
4513
+ expect(assessTierMock).toHaveBeenCalled();
4514
+ });
4515
+
4516
+ it("covers handleDispatch .claw dir init failure", async () => {
4517
+ ensureClawDirMock.mockImplementation(() => { throw new Error("claw dir fail"); });
4518
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4519
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4520
+ id: "issue-claw-fail",
4521
+ identifier: "ENG-CDF",
4522
+ title: "Claw Dir Fail",
4523
+ description: "desc",
4524
+ state: { name: "In Progress", type: "started" },
4525
+ team: { id: "team-cdf" },
4526
+ labels: { nodes: [] },
4527
+ comments: { nodes: [] },
4528
+ project: null,
4529
+ });
4530
+
4531
+ const result = await postWebhook({
4532
+ type: "Issue",
4533
+ action: "update",
4534
+ data: {
4535
+ id: "issue-claw-fail",
4536
+ identifier: "ENG-CDF",
4537
+ assigneeId: "viewer-1",
4538
+ },
4539
+ updatedFrom: { assigneeId: null },
4540
+ });
4541
+
4542
+ expect(result.status).toBe(200);
4543
+ await new Promise((r) => setTimeout(r, 500));
4544
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
4545
+ expect(warnCalls.some((msg: string) => msg.includes(".claw/ init failed"))).toBe(true);
4546
+ expect(registerDispatchMock).toHaveBeenCalled();
4547
+ });
4548
+
4549
+ it("covers handleDispatch session creation failure", async () => {
4550
+ mockLinearApiInstance.createSessionOnIssue.mockRejectedValue(new Error("session create fail"));
4551
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4552
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4553
+ id: "issue-sess-fail",
4554
+ identifier: "ENG-SSF",
4555
+ title: "Sess Fail",
4556
+ description: "desc",
4557
+ state: { name: "In Progress", type: "started" },
4558
+ team: { id: "team-ssf" },
4559
+ labels: { nodes: [] },
4560
+ comments: { nodes: [] },
4561
+ project: null,
4562
+ });
4563
+
4564
+ const result = await postWebhook({
4565
+ type: "Issue",
4566
+ action: "update",
4567
+ data: {
4568
+ id: "issue-sess-fail",
4569
+ identifier: "ENG-SSF",
4570
+ assigneeId: "viewer-1",
4571
+ },
4572
+ updatedFrom: { assigneeId: null },
4573
+ });
4574
+
4575
+ expect(result.status).toBe(200);
4576
+ await new Promise((r) => setTimeout(r, 500));
4577
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
4578
+ expect(warnCalls.some((msg: string) => msg.includes("could not create agent session"))).toBe(true);
4579
+ expect(registerDispatchMock).toHaveBeenCalled();
4580
+ });
4581
+
4582
+ it("covers handleDispatch workspace prep errors", async () => {
4583
+ prepareWorkspaceMock.mockReturnValue({ pulled: false, submodulesInitialized: false, errors: ["git pull failed"] });
4584
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4585
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4586
+ id: "issue-prep-err",
4587
+ identifier: "ENG-PE",
4588
+ title: "Prep Error",
4589
+ description: "desc",
4590
+ state: { name: "In Progress", type: "started" },
4591
+ team: { id: "team-pe" },
4592
+ labels: { nodes: [] },
4593
+ comments: { nodes: [] },
4594
+ project: null,
4595
+ });
4596
+
4597
+ const result = await postWebhook({
4598
+ type: "Issue",
4599
+ action: "update",
4600
+ data: {
4601
+ id: "issue-prep-err",
4602
+ identifier: "ENG-PE",
4603
+ assigneeId: "viewer-1",
4604
+ },
4605
+ updatedFrom: { assigneeId: null },
4606
+ });
4607
+
4608
+ expect(result.status).toBe(200);
4609
+ await new Promise((r) => setTimeout(r, 500));
4610
+ const warnCalls = (result.api.logger.warn as any).mock.calls.map((c: any[]) => c[0]);
4611
+ expect(warnCalls.some((msg: string) => msg.includes("workspace prep had errors"))).toBe(true);
4612
+ });
4613
+
4614
+ it("covers spawnWorker .catch with writeDispatchMemory failure (best effort)", async () => {
4615
+ spawnWorkerMock.mockRejectedValue(new Error("pipeline crash"));
4616
+ resolveOrchestratorWorkspaceMock.mockImplementation(() => { throw new Error("workspace resolve fail"); });
4617
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4618
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4619
+ id: "issue-mem-fail",
4620
+ identifier: "ENG-MF2",
4621
+ title: "Memory Fail",
4622
+ description: "desc",
4623
+ state: { name: "In Progress", type: "started" },
4624
+ team: { id: "team-mf2" },
4625
+ labels: { nodes: [] },
4626
+ comments: { nodes: [] },
4627
+ project: null,
4628
+ });
4629
+
4630
+ const result = await postWebhook({
4631
+ type: "Issue",
4632
+ action: "update",
4633
+ data: {
4634
+ id: "issue-mem-fail",
4635
+ identifier: "ENG-MF2",
4636
+ assigneeId: "viewer-1",
4637
+ },
4638
+ updatedFrom: { assigneeId: null },
4639
+ });
4640
+
4641
+ expect(result.status).toBe(200);
4642
+ await new Promise((r) => setTimeout(r, 500));
4643
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
4644
+ expect(errorCalls.some((msg: string) => msg.includes("pipeline v2 failed"))).toBe(true);
4645
+ expect(clearActiveSessionMock).toHaveBeenCalledWith("issue-mem-fail");
4646
+ });
4647
+ });
4648
+
4649
+ // ---------------------------------------------------------------------------
4650
+ // Coverage push: plan_finalize approval error path
4651
+ // ---------------------------------------------------------------------------
4652
+
4653
+ describe("plan_finalize approval error paths", () => {
4654
+ it("covers plan_finalize approval error in async block", async () => {
4655
+ classifyIntentMock.mockResolvedValue({
4656
+ intent: "plan_finalize",
4657
+ reasoning: "Finalize",
4658
+ fromFallback: false,
4659
+ });
4660
+ isInPlanningModeMock.mockReturnValue(true);
4661
+ getPlanningSessionMock.mockReturnValue({
4662
+ projectId: "proj-fin-err",
4663
+ projectName: "Finalize Error",
4664
+ rootIssueId: "root-fin-err",
4665
+ status: "plan_review",
4666
+ });
4667
+ endPlanningSessionMock.mockRejectedValue(new Error("end session fail"));
4668
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4669
+ id: "issue-fin-err",
4670
+ identifier: "ENG-FE",
4671
+ title: "Finalize Error",
4672
+ state: { name: "Backlog" },
4673
+ project: { id: "proj-fin-err" },
4674
+ });
4675
+
4676
+ const result = await postWebhook({
4677
+ type: "Comment",
4678
+ action: "create",
4679
+ data: {
4680
+ id: "comment-fin-err",
4681
+ body: "Approve the plan",
4682
+ user: { id: "human-fe", name: "Human" },
4683
+ issue: { id: "issue-fin-err", identifier: "ENG-FE", project: { id: "proj-fin-err" } },
4684
+ },
4685
+ });
4686
+
4687
+ expect(result.status).toBe(200);
4688
+ await new Promise((r) => setTimeout(r, 150));
4689
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
4690
+ expect(errorCalls.some((msg: string) => msg.includes("Plan approval error"))).toBe(true);
4691
+ });
4692
+
4693
+ it("covers plan_abandon error in async block", async () => {
4694
+ classifyIntentMock.mockResolvedValue({
4695
+ intent: "plan_abandon",
4696
+ reasoning: "Abandon",
4697
+ fromFallback: false,
4698
+ });
4699
+ isInPlanningModeMock.mockReturnValue(true);
4700
+ getPlanningSessionMock.mockReturnValue({
4701
+ projectId: "proj-ab-err",
4702
+ projectName: "Abandon Error",
4703
+ rootIssueId: "root-ab-err",
4704
+ status: "interviewing",
4705
+ });
4706
+ endPlanningSessionMock.mockRejectedValue(new Error("abandon fail"));
4707
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4708
+ id: "issue-ab-err",
4709
+ identifier: "ENG-ABE",
4710
+ title: "Abandon Error",
4711
+ state: { name: "Backlog" },
4712
+ project: { id: "proj-ab-err" },
4713
+ });
4714
+
4715
+ const result = await postWebhook({
4716
+ type: "Comment",
4717
+ action: "create",
4718
+ data: {
4719
+ id: "comment-ab-err",
4720
+ body: "Abandon plan",
4721
+ user: { id: "human-abe", name: "Human" },
4722
+ issue: { id: "issue-ab-err", identifier: "ENG-ABE", project: { id: "proj-ab-err" } },
4723
+ },
4724
+ });
4725
+
4726
+ expect(result.status).toBe(200);
4727
+ await new Promise((r) => setTimeout(r, 150));
4728
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
4729
+ expect(errorCalls.some((msg: string) => msg.includes("Plan abandon error"))).toBe(true);
4730
+ });
4731
+ });
4732
+
4733
+ // ---------------------------------------------------------------------------
4734
+ // Coverage push: postAgentComment without opts (no agentOpts branch)
4735
+ // ---------------------------------------------------------------------------
4736
+
4737
+ describe("postAgentComment without agentOpts", () => {
4738
+ it("covers postAgentComment code path when no agentOpts is passed", async () => {
4739
+ loadAgentProfilesMock.mockReturnValue({
4740
+ mal: { label: "Mal", mission: "captain", mentionAliases: ["mal"], isDefault: true },
4741
+ });
4742
+ mockLinearApiInstance.emitActivity.mockImplementation((_sid: string, content: any) => {
4743
+ if (content.type === "response") return Promise.reject(new Error("fail"));
4744
+ return Promise.resolve(undefined);
4745
+ });
4746
+
4747
+ const result = await postWebhook({
4748
+ type: "AgentSessionEvent",
4749
+ action: "created",
4750
+ agentSession: {
4751
+ id: "sess-no-opts",
4752
+ issue: { id: "issue-no-opts", identifier: "ENG-NO" },
4753
+ },
4754
+ previousComments: [{ body: "Test", user: { name: "Dev" } }],
4755
+ });
4756
+
4757
+ expect(result.status).toBe(200);
4758
+ await new Promise((r) => setTimeout(r, 150));
4759
+ expect(mockLinearApiInstance.createComment).toHaveBeenCalled();
4760
+ const commentCall = mockLinearApiInstance.createComment.mock.calls[0];
4761
+ expect(commentCall[1]).toContain("**[Mal]**");
4762
+ });
4763
+ });
4764
+
4765
+ // ---------------------------------------------------------------------------
4766
+ // Coverage push: Comment.create guidance-enabled branch
4767
+ // ---------------------------------------------------------------------------
4768
+
4769
+ describe("Comment.create guidance paths", () => {
4770
+ it("covers guidance enabled path in dispatchCommentToAgent", async () => {
4771
+ isGuidanceEnabledMock.mockReturnValue(true);
4772
+ getCachedGuidanceForTeamMock.mockReturnValue("Cached comment guidance");
4773
+ formatGuidanceAppendixMock.mockReturnValue("\n## Guidance\nCached comment guidance");
4774
+ classifyIntentMock.mockResolvedValue({
4775
+ intent: "request_work",
4776
+ reasoning: "Work",
4777
+ fromFallback: false,
4778
+ });
4779
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4780
+ id: "issue-guid-comment",
4781
+ identifier: "ENG-GC",
4782
+ title: "Guidance Comment",
4783
+ description: "desc",
4784
+ state: { name: "In Progress", type: "started" },
4785
+ team: { id: "team-gc" },
4786
+ comments: { nodes: [{ user: { name: "Dev" }, body: "context" }] },
4787
+ creator: { name: "Creator", email: "c@test.com" },
4788
+ });
4789
+
4790
+ const result = await postWebhook({
4791
+ type: "Comment",
4792
+ action: "create",
4793
+ data: {
4794
+ id: "comment-guid",
4795
+ body: "Do the work",
4796
+ user: { id: "human-gc", name: "Human" },
4797
+ issue: { id: "issue-guid-comment", identifier: "ENG-GC" },
4798
+ },
4799
+ });
4800
+
4801
+ expect(result.status).toBe(200);
4802
+ await new Promise((r) => setTimeout(r, 300));
4803
+ expect(getCachedGuidanceForTeamMock).toHaveBeenCalledWith("team-gc");
4804
+ expect(runAgentMock).toHaveBeenCalled();
4805
+ });
4806
+ });
4807
+
4808
+ // ---------------------------------------------------------------------------
4809
+ // Coverage push: handleDispatch .catch wrapper (L999)
4810
+ // ---------------------------------------------------------------------------
4811
+
4812
+ describe("handleDispatch error via Issue.update .catch wrapper", () => {
4813
+ it("covers handleDispatch .catch wrapper when whole dispatch throws", async () => {
4814
+ mockLinearApiInstance.getViewerId.mockResolvedValue("viewer-1");
4815
+ assessTierMock.mockRejectedValue(new Error("tier assessment crash"));
4816
+ mockLinearApiInstance.getIssueDetails.mockResolvedValue({
4817
+ id: "issue-hdl-catch",
4818
+ identifier: "ENG-HC",
4819
+ title: "Handle Catch",
4820
+ description: "desc",
4821
+ state: { name: "In Progress", type: "started" },
4822
+ team: { id: "team-hc" },
4823
+ labels: { nodes: [] },
4824
+ comments: { nodes: [] },
4825
+ project: null,
4826
+ });
4827
+
4828
+ const result = await postWebhook({
4829
+ type: "Issue",
4830
+ action: "update",
4831
+ data: {
4832
+ id: "issue-hdl-catch",
4833
+ identifier: "ENG-HC",
4834
+ assigneeId: "viewer-1",
4835
+ },
4836
+ updatedFrom: { assigneeId: null },
4837
+ });
4838
+
4839
+ expect(result.status).toBe(200);
4840
+ await new Promise((r) => setTimeout(r, 300));
4841
+ const errorCalls = (result.api.logger.error as any).mock.calls.map((c: any[]) => c[0]);
4842
+ expect(errorCalls.some((msg: string) => msg.includes("Dispatch pipeline error"))).toBe(true);
4843
+ });
4844
+ });