offwatch 0.5.8 → 0.5.10

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.
Files changed (94) hide show
  1. package/bin/offwatch.js +7 -6
  2. package/package.json +4 -3
  3. package/src/__tests__/agent-jwt-env.test.ts +79 -0
  4. package/src/__tests__/allowed-hostname.test.ts +80 -0
  5. package/src/__tests__/auth-command-registration.test.ts +16 -0
  6. package/src/__tests__/board-auth.test.ts +53 -0
  7. package/src/__tests__/common.test.ts +98 -0
  8. package/src/__tests__/company-delete.test.ts +95 -0
  9. package/src/__tests__/company-import-export-e2e.test.ts +502 -0
  10. package/src/__tests__/company-import-url.test.ts +74 -0
  11. package/src/__tests__/company-import-zip.test.ts +44 -0
  12. package/src/__tests__/company.test.ts +599 -0
  13. package/src/__tests__/context.test.ts +70 -0
  14. package/src/__tests__/data-dir.test.ts +79 -0
  15. package/src/__tests__/doctor.test.ts +102 -0
  16. package/src/__tests__/feedback.test.ts +177 -0
  17. package/src/__tests__/helpers/embedded-postgres.ts +6 -0
  18. package/src/__tests__/helpers/zip.ts +87 -0
  19. package/src/__tests__/home-paths.test.ts +44 -0
  20. package/src/__tests__/http.test.ts +106 -0
  21. package/src/__tests__/network-bind.test.ts +62 -0
  22. package/src/__tests__/onboard.test.ts +166 -0
  23. package/src/__tests__/routines.test.ts +249 -0
  24. package/src/__tests__/telemetry.test.ts +117 -0
  25. package/src/__tests__/worktree-merge-history.test.ts +492 -0
  26. package/src/__tests__/worktree.test.ts +982 -0
  27. package/src/adapters/http/format-event.ts +4 -0
  28. package/src/adapters/http/index.ts +7 -0
  29. package/src/adapters/index.ts +2 -0
  30. package/src/adapters/process/format-event.ts +4 -0
  31. package/src/adapters/process/index.ts +7 -0
  32. package/src/adapters/registry.ts +63 -0
  33. package/src/checks/agent-jwt-secret-check.ts +40 -0
  34. package/src/checks/config-check.ts +33 -0
  35. package/src/checks/database-check.ts +59 -0
  36. package/src/checks/deployment-auth-check.ts +88 -0
  37. package/src/checks/index.ts +18 -0
  38. package/src/checks/llm-check.ts +82 -0
  39. package/src/checks/log-check.ts +30 -0
  40. package/src/checks/path-resolver.ts +1 -0
  41. package/src/checks/port-check.ts +24 -0
  42. package/src/checks/secrets-check.ts +146 -0
  43. package/src/checks/storage-check.ts +51 -0
  44. package/src/client/board-auth.ts +282 -0
  45. package/src/client/command-label.ts +4 -0
  46. package/src/client/context.ts +175 -0
  47. package/src/client/http.ts +255 -0
  48. package/src/commands/allowed-hostname.ts +40 -0
  49. package/src/commands/auth-bootstrap-ceo.ts +138 -0
  50. package/src/commands/client/activity.ts +71 -0
  51. package/src/commands/client/agent.ts +315 -0
  52. package/src/commands/client/approval.ts +259 -0
  53. package/src/commands/client/auth.ts +113 -0
  54. package/src/commands/client/common.ts +221 -0
  55. package/src/commands/client/company.ts +1578 -0
  56. package/src/commands/client/context.ts +125 -0
  57. package/src/commands/client/dashboard.ts +34 -0
  58. package/src/commands/client/feedback.ts +645 -0
  59. package/src/commands/client/issue.ts +411 -0
  60. package/src/commands/client/plugin.ts +374 -0
  61. package/src/commands/client/zip.ts +129 -0
  62. package/src/commands/configure.ts +201 -0
  63. package/src/commands/db-backup.ts +102 -0
  64. package/src/commands/doctor.ts +203 -0
  65. package/src/commands/env.ts +411 -0
  66. package/src/commands/heartbeat-run.ts +344 -0
  67. package/src/commands/onboard.ts +692 -0
  68. package/src/commands/routines.ts +352 -0
  69. package/src/commands/run.ts +216 -0
  70. package/src/commands/worktree-lib.ts +279 -0
  71. package/src/commands/worktree-merge-history-lib.ts +764 -0
  72. package/src/commands/worktree.ts +2876 -0
  73. package/src/config/data-dir.ts +48 -0
  74. package/src/config/env.ts +125 -0
  75. package/src/config/home.ts +80 -0
  76. package/src/config/hostnames.ts +26 -0
  77. package/src/config/schema.ts +30 -0
  78. package/src/config/secrets-key.ts +48 -0
  79. package/src/config/server-bind.ts +183 -0
  80. package/src/config/store.ts +120 -0
  81. package/src/index.ts +182 -0
  82. package/src/prompts/database.ts +157 -0
  83. package/src/prompts/llm.ts +43 -0
  84. package/src/prompts/logging.ts +37 -0
  85. package/src/prompts/secrets.ts +99 -0
  86. package/src/prompts/server.ts +221 -0
  87. package/src/prompts/storage.ts +146 -0
  88. package/src/telemetry.ts +49 -0
  89. package/src/utils/banner.ts +24 -0
  90. package/src/utils/net.ts +18 -0
  91. package/src/utils/path-resolver.ts +25 -0
  92. package/src/version.ts +10 -0
  93. package/lib/downloader.js +0 -112
  94. package/postinstall.js +0 -23
@@ -0,0 +1,492 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildWorktreeMergePlan, parseWorktreeMergeScopes } from "../commands/worktree-merge-history-lib.js";
3
+
4
+ function makeIssue(overrides: Record<string, unknown> = {}) {
5
+ return {
6
+ id: "issue-1",
7
+ companyId: "company-1",
8
+ projectId: null,
9
+ projectWorkspaceId: null,
10
+ goalId: "goal-1",
11
+ parentId: null,
12
+ title: "Issue",
13
+ description: null,
14
+ status: "todo",
15
+ priority: "medium",
16
+ assigneeAgentId: null,
17
+ assigneeUserId: null,
18
+ checkoutRunId: null,
19
+ executionRunId: null,
20
+ executionAgentNameKey: null,
21
+ executionLockedAt: null,
22
+ createdByAgentId: null,
23
+ createdByUserId: "local-board",
24
+ issueNumber: 1,
25
+ identifier: "PAP-1",
26
+ requestDepth: 0,
27
+ billingCode: null,
28
+ assigneeAdapterOverrides: null,
29
+ executionWorkspaceId: null,
30
+ executionWorkspacePreference: null,
31
+ executionWorkspaceSettings: null,
32
+ startedAt: null,
33
+ completedAt: null,
34
+ cancelledAt: null,
35
+ hiddenAt: null,
36
+ createdAt: new Date("2026-03-20T00:00:00.000Z"),
37
+ updatedAt: new Date("2026-03-20T00:00:00.000Z"),
38
+ ...overrides,
39
+ } as any;
40
+ }
41
+
42
+ function makeComment(overrides: Record<string, unknown> = {}) {
43
+ return {
44
+ id: "comment-1",
45
+ companyId: "company-1",
46
+ issueId: "issue-1",
47
+ authorAgentId: null,
48
+ authorUserId: "local-board",
49
+ body: "hello",
50
+ createdAt: new Date("2026-03-20T00:00:00.000Z"),
51
+ updatedAt: new Date("2026-03-20T00:00:00.000Z"),
52
+ ...overrides,
53
+ } as any;
54
+ }
55
+
56
+ function makeIssueDocument(overrides: Record<string, unknown> = {}) {
57
+ return {
58
+ id: "issue-document-1",
59
+ companyId: "company-1",
60
+ issueId: "issue-1",
61
+ documentId: "document-1",
62
+ key: "plan",
63
+ linkCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
64
+ linkUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
65
+ title: "Plan",
66
+ format: "markdown",
67
+ latestBody: "# Plan",
68
+ latestRevisionId: "revision-1",
69
+ latestRevisionNumber: 1,
70
+ createdByAgentId: null,
71
+ createdByUserId: "local-board",
72
+ updatedByAgentId: null,
73
+ updatedByUserId: "local-board",
74
+ documentCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
75
+ documentUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
76
+ ...overrides,
77
+ } as any;
78
+ }
79
+
80
+ function makeDocumentRevision(overrides: Record<string, unknown> = {}) {
81
+ return {
82
+ id: "revision-1",
83
+ companyId: "company-1",
84
+ documentId: "document-1",
85
+ revisionNumber: 1,
86
+ body: "# Plan",
87
+ changeSummary: null,
88
+ createdByAgentId: null,
89
+ createdByUserId: "local-board",
90
+ createdAt: new Date("2026-03-20T00:00:00.000Z"),
91
+ ...overrides,
92
+ } as any;
93
+ }
94
+
95
+ function makeAttachment(overrides: Record<string, unknown> = {}) {
96
+ return {
97
+ id: "attachment-1",
98
+ companyId: "company-1",
99
+ issueId: "issue-1",
100
+ issueCommentId: null,
101
+ assetId: "asset-1",
102
+ provider: "local_disk",
103
+ objectKey: "company-1/issues/issue-1/2026/03/20/asset.png",
104
+ contentType: "image/png",
105
+ byteSize: 12,
106
+ sha256: "deadbeef",
107
+ originalFilename: "asset.png",
108
+ createdByAgentId: null,
109
+ createdByUserId: "local-board",
110
+ assetCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
111
+ assetUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
112
+ attachmentCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
113
+ attachmentUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
114
+ ...overrides,
115
+ } as any;
116
+ }
117
+
118
+ function makeProject(overrides: Record<string, unknown> = {}) {
119
+ return {
120
+ id: "project-1",
121
+ companyId: "company-1",
122
+ goalId: null,
123
+ name: "Project",
124
+ description: null,
125
+ status: "in_progress",
126
+ leadAgentId: null,
127
+ targetDate: null,
128
+ color: "#22c55e",
129
+ pauseReason: null,
130
+ pausedAt: null,
131
+ executionWorkspacePolicy: null,
132
+ archivedAt: null,
133
+ createdAt: new Date("2026-03-20T00:00:00.000Z"),
134
+ updatedAt: new Date("2026-03-20T00:00:00.000Z"),
135
+ ...overrides,
136
+ } as any;
137
+ }
138
+
139
+ function makeProjectWorkspace(overrides: Record<string, unknown> = {}) {
140
+ return {
141
+ id: "workspace-1",
142
+ companyId: "company-1",
143
+ projectId: "project-1",
144
+ name: "Workspace",
145
+ sourceType: "local_path",
146
+ cwd: "/tmp/project",
147
+ repoUrl: "https://github.com/example/project.git",
148
+ repoRef: "main",
149
+ defaultRef: "main",
150
+ visibility: "default",
151
+ setupCommand: null,
152
+ cleanupCommand: null,
153
+ remoteProvider: null,
154
+ remoteWorkspaceRef: null,
155
+ sharedWorkspaceKey: null,
156
+ metadata: null,
157
+ isPrimary: true,
158
+ createdAt: new Date("2026-03-20T00:00:00.000Z"),
159
+ updatedAt: new Date("2026-03-20T00:00:00.000Z"),
160
+ ...overrides,
161
+ } as any;
162
+ }
163
+
164
+ describe("worktree merge history planner", () => {
165
+ it("parses default scopes", () => {
166
+ expect(parseWorktreeMergeScopes(undefined)).toEqual(["issues", "comments"]);
167
+ expect(parseWorktreeMergeScopes("issues")).toEqual(["issues"]);
168
+ });
169
+
170
+ it("dedupes nested worktree issues by preserved source uuid", () => {
171
+ const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10", title: "Shared" });
172
+ const branchOneIssue = makeIssue({
173
+ id: "issue-b",
174
+ identifier: "PAP-22",
175
+ title: "Branch one issue",
176
+ createdAt: new Date("2026-03-20T01:00:00.000Z"),
177
+ });
178
+ const branchTwoIssue = makeIssue({
179
+ id: "issue-c",
180
+ identifier: "PAP-23",
181
+ title: "Branch two issue",
182
+ createdAt: new Date("2026-03-20T02:00:00.000Z"),
183
+ });
184
+
185
+ const plan = buildWorktreeMergePlan({
186
+ companyId: "company-1",
187
+ companyName: "Paperclip",
188
+ issuePrefix: "PAP",
189
+ previewIssueCounterStart: 500,
190
+ scopes: ["issues", "comments"],
191
+ sourceIssues: [sharedIssue, branchOneIssue, branchTwoIssue],
192
+ targetIssues: [sharedIssue, branchOneIssue],
193
+ sourceComments: [],
194
+ targetComments: [],
195
+ targetAgents: [],
196
+ targetProjects: [],
197
+ targetProjectWorkspaces: [],
198
+ targetGoals: [{ id: "goal-1" }] as any,
199
+ });
200
+
201
+ expect(plan.counts.issuesToInsert).toBe(1);
202
+ expect(plan.issuePlans.filter((item) => item.action === "insert").map((item) => item.source.id)).toEqual(["issue-c"]);
203
+ expect(plan.issuePlans.find((item) => item.source.id === "issue-c" && item.action === "insert")).toMatchObject({
204
+ previewIdentifier: "PAP-501",
205
+ });
206
+ });
207
+
208
+ it("clears missing references and coerces in_progress without an assignee", () => {
209
+ const plan = buildWorktreeMergePlan({
210
+ companyId: "company-1",
211
+ companyName: "Paperclip",
212
+ issuePrefix: "PAP",
213
+ previewIssueCounterStart: 10,
214
+ scopes: ["issues"],
215
+ sourceIssues: [
216
+ makeIssue({
217
+ id: "issue-x",
218
+ identifier: "PAP-99",
219
+ status: "in_progress",
220
+ assigneeAgentId: "agent-missing",
221
+ projectId: "project-missing",
222
+ projectWorkspaceId: "workspace-missing",
223
+ goalId: "goal-missing",
224
+ }),
225
+ ],
226
+ targetIssues: [],
227
+ sourceComments: [],
228
+ targetComments: [],
229
+ targetAgents: [],
230
+ targetProjects: [],
231
+ targetProjectWorkspaces: [],
232
+ targetGoals: [],
233
+ });
234
+
235
+ const insert = plan.issuePlans[0] as any;
236
+ expect(insert.targetStatus).toBe("todo");
237
+ expect(insert.targetAssigneeAgentId).toBeNull();
238
+ expect(insert.targetProjectId).toBeNull();
239
+ expect(insert.targetProjectWorkspaceId).toBeNull();
240
+ expect(insert.targetGoalId).toBeNull();
241
+ expect(insert.adjustments).toEqual([
242
+ "clear_assignee_agent",
243
+ "clear_project",
244
+ "clear_project_workspace",
245
+ "clear_goal",
246
+ "coerce_in_progress_to_todo",
247
+ ]);
248
+ });
249
+
250
+ it("applies an explicit project mapping override instead of clearing the project", () => {
251
+ const plan = buildWorktreeMergePlan({
252
+ companyId: "company-1",
253
+ companyName: "Paperclip",
254
+ issuePrefix: "PAP",
255
+ previewIssueCounterStart: 10,
256
+ scopes: ["issues"],
257
+ sourceIssues: [
258
+ makeIssue({
259
+ id: "issue-project-map",
260
+ identifier: "PAP-77",
261
+ projectId: "source-project-1",
262
+ projectWorkspaceId: "source-workspace-1",
263
+ }),
264
+ ],
265
+ targetIssues: [],
266
+ sourceComments: [],
267
+ targetComments: [],
268
+ targetAgents: [],
269
+ targetProjects: [{ id: "target-project-1", name: "Mapped project", status: "in_progress" }] as any,
270
+ targetProjectWorkspaces: [],
271
+ targetGoals: [{ id: "goal-1" }] as any,
272
+ projectIdOverrides: {
273
+ "source-project-1": "target-project-1",
274
+ },
275
+ });
276
+
277
+ const insert = plan.issuePlans[0] as any;
278
+ expect(insert.targetProjectId).toBe("target-project-1");
279
+ expect(insert.projectResolution).toBe("mapped");
280
+ expect(insert.mappedProjectName).toBe("Mapped project");
281
+ expect(insert.targetProjectWorkspaceId).toBeNull();
282
+ expect(insert.adjustments).toEqual(["clear_project_workspace"]);
283
+ });
284
+
285
+ it("plans selected project imports and preserves project workspace links", () => {
286
+ const sourceProject = makeProject({
287
+ id: "source-project-1",
288
+ name: "Paperclip Evals",
289
+ goalId: "goal-1",
290
+ });
291
+ const sourceWorkspace = makeProjectWorkspace({
292
+ id: "source-workspace-1",
293
+ projectId: "source-project-1",
294
+ cwd: "/Users/dotta/paperclip-evals",
295
+ repoUrl: "https://github.com/paperclipai/paperclip-evals.git",
296
+ });
297
+
298
+ const plan = buildWorktreeMergePlan({
299
+ companyId: "company-1",
300
+ companyName: "Paperclip",
301
+ issuePrefix: "PAP",
302
+ previewIssueCounterStart: 10,
303
+ scopes: ["issues"],
304
+ sourceIssues: [
305
+ makeIssue({
306
+ id: "issue-project-import",
307
+ identifier: "PAP-88",
308
+ projectId: "source-project-1",
309
+ projectWorkspaceId: "source-workspace-1",
310
+ }),
311
+ ],
312
+ targetIssues: [],
313
+ sourceComments: [],
314
+ targetComments: [],
315
+ sourceProjects: [sourceProject],
316
+ sourceProjectWorkspaces: [sourceWorkspace],
317
+ targetAgents: [],
318
+ targetProjects: [],
319
+ targetProjectWorkspaces: [],
320
+ targetGoals: [{ id: "goal-1" }] as any,
321
+ importProjectIds: ["source-project-1"],
322
+ });
323
+
324
+ expect(plan.counts.projectsToImport).toBe(1);
325
+ expect(plan.projectImports[0]).toMatchObject({
326
+ source: { id: "source-project-1", name: "Paperclip Evals" },
327
+ targetGoalId: "goal-1",
328
+ workspaces: [{ id: "source-workspace-1" }],
329
+ });
330
+
331
+ const insert = plan.issuePlans[0] as any;
332
+ expect(insert.targetProjectId).toBe("source-project-1");
333
+ expect(insert.targetProjectWorkspaceId).toBe("source-workspace-1");
334
+ expect(insert.projectResolution).toBe("imported");
335
+ expect(insert.mappedProjectName).toBe("Paperclip Evals");
336
+ expect(insert.adjustments).toEqual([]);
337
+ });
338
+
339
+ it("imports comments onto shared or newly imported issues while skipping existing comments", () => {
340
+ const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10" });
341
+ const newIssue = makeIssue({
342
+ id: "issue-b",
343
+ identifier: "PAP-11",
344
+ createdAt: new Date("2026-03-20T01:00:00.000Z"),
345
+ });
346
+ const existingComment = makeComment({ id: "comment-existing", issueId: "issue-a" });
347
+ const sharedIssueComment = makeComment({ id: "comment-shared", issueId: "issue-a" });
348
+ const newIssueComment = makeComment({
349
+ id: "comment-new-issue",
350
+ issueId: "issue-b",
351
+ authorAgentId: "missing-agent",
352
+ createdAt: new Date("2026-03-20T01:05:00.000Z"),
353
+ });
354
+
355
+ const plan = buildWorktreeMergePlan({
356
+ companyId: "company-1",
357
+ companyName: "Paperclip",
358
+ issuePrefix: "PAP",
359
+ previewIssueCounterStart: 10,
360
+ scopes: ["issues", "comments"],
361
+ sourceIssues: [sharedIssue, newIssue],
362
+ targetIssues: [sharedIssue],
363
+ sourceComments: [existingComment, sharedIssueComment, newIssueComment],
364
+ targetComments: [existingComment],
365
+ targetAgents: [],
366
+ targetProjects: [],
367
+ targetProjectWorkspaces: [],
368
+ targetGoals: [{ id: "goal-1" }] as any,
369
+ });
370
+
371
+ expect(plan.counts.commentsToInsert).toBe(2);
372
+ expect(plan.counts.commentsExisting).toBe(1);
373
+ expect(plan.commentPlans.filter((item) => item.action === "insert").map((item) => item.source.id)).toEqual([
374
+ "comment-shared",
375
+ "comment-new-issue",
376
+ ]);
377
+ expect(plan.adjustments.clear_author_agent).toBe(1);
378
+ });
379
+
380
+ it("merges document revisions onto an existing shared document and renumbers conflicts", () => {
381
+ const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10" });
382
+ const sourceDocument = makeIssueDocument({
383
+ issueId: "issue-a",
384
+ documentId: "document-a",
385
+ latestBody: "# Branch plan",
386
+ latestRevisionId: "revision-branch-2",
387
+ latestRevisionNumber: 2,
388
+ documentUpdatedAt: new Date("2026-03-20T02:00:00.000Z"),
389
+ linkUpdatedAt: new Date("2026-03-20T02:00:00.000Z"),
390
+ });
391
+ const targetDocument = makeIssueDocument({
392
+ issueId: "issue-a",
393
+ documentId: "document-a",
394
+ latestBody: "# Main plan",
395
+ latestRevisionId: "revision-main-2",
396
+ latestRevisionNumber: 2,
397
+ documentUpdatedAt: new Date("2026-03-20T01:00:00.000Z"),
398
+ linkUpdatedAt: new Date("2026-03-20T01:00:00.000Z"),
399
+ });
400
+ const sourceRevisionOne = makeDocumentRevision({ documentId: "document-a", id: "revision-1" });
401
+ const sourceRevisionTwo = makeDocumentRevision({
402
+ documentId: "document-a",
403
+ id: "revision-branch-2",
404
+ revisionNumber: 2,
405
+ body: "# Branch plan",
406
+ createdAt: new Date("2026-03-20T02:00:00.000Z"),
407
+ });
408
+ const targetRevisionOne = makeDocumentRevision({ documentId: "document-a", id: "revision-1" });
409
+ const targetRevisionTwo = makeDocumentRevision({
410
+ documentId: "document-a",
411
+ id: "revision-main-2",
412
+ revisionNumber: 2,
413
+ body: "# Main plan",
414
+ createdAt: new Date("2026-03-20T01:00:00.000Z"),
415
+ });
416
+
417
+ const plan = buildWorktreeMergePlan({
418
+ companyId: "company-1",
419
+ companyName: "Paperclip",
420
+ issuePrefix: "PAP",
421
+ previewIssueCounterStart: 10,
422
+ scopes: ["issues", "comments"],
423
+ sourceIssues: [sharedIssue],
424
+ targetIssues: [sharedIssue],
425
+ sourceComments: [],
426
+ targetComments: [],
427
+ sourceDocuments: [sourceDocument],
428
+ targetDocuments: [targetDocument],
429
+ sourceDocumentRevisions: [sourceRevisionOne, sourceRevisionTwo],
430
+ targetDocumentRevisions: [targetRevisionOne, targetRevisionTwo],
431
+ sourceAttachments: [],
432
+ targetAttachments: [],
433
+ targetAgents: [],
434
+ targetProjects: [],
435
+ targetProjectWorkspaces: [],
436
+ targetGoals: [{ id: "goal-1" }] as any,
437
+ });
438
+
439
+ expect(plan.counts.documentsToMerge).toBe(1);
440
+ expect(plan.counts.documentRevisionsToInsert).toBe(1);
441
+ expect(plan.documentPlans[0]).toMatchObject({
442
+ action: "merge_existing",
443
+ latestRevisionId: "revision-branch-2",
444
+ latestRevisionNumber: 3,
445
+ });
446
+ const mergePlan = plan.documentPlans[0] as any;
447
+ expect(mergePlan.revisionsToInsert).toHaveLength(1);
448
+ expect(mergePlan.revisionsToInsert[0]).toMatchObject({
449
+ source: { id: "revision-branch-2" },
450
+ targetRevisionNumber: 3,
451
+ });
452
+ });
453
+
454
+ it("imports attachments while clearing missing comment and author references", () => {
455
+ const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10" });
456
+ const attachment = makeAttachment({
457
+ issueId: "issue-a",
458
+ issueCommentId: "comment-missing",
459
+ createdByAgentId: "agent-missing",
460
+ });
461
+
462
+ const plan = buildWorktreeMergePlan({
463
+ companyId: "company-1",
464
+ companyName: "Paperclip",
465
+ issuePrefix: "PAP",
466
+ previewIssueCounterStart: 10,
467
+ scopes: ["issues"],
468
+ sourceIssues: [sharedIssue],
469
+ targetIssues: [sharedIssue],
470
+ sourceComments: [],
471
+ targetComments: [],
472
+ sourceDocuments: [],
473
+ targetDocuments: [],
474
+ sourceDocumentRevisions: [],
475
+ targetDocumentRevisions: [],
476
+ sourceAttachments: [attachment],
477
+ targetAttachments: [],
478
+ targetAgents: [],
479
+ targetProjects: [],
480
+ targetProjectWorkspaces: [],
481
+ targetGoals: [{ id: "goal-1" }] as any,
482
+ });
483
+
484
+ expect(plan.counts.attachmentsToInsert).toBe(1);
485
+ expect(plan.adjustments.clear_attachment_agent).toBe(1);
486
+ expect(plan.attachmentPlans[0]).toMatchObject({
487
+ action: "insert",
488
+ targetIssueCommentId: null,
489
+ targetCreatedByAgentId: null,
490
+ });
491
+ });
492
+ });