| ... | ... | @@ -358,6 +358,128 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 358 | 358 | }), |
| 359 | 359 | ); |
| 360 | 360 | |
| 361 | it.effect( |
| 362 | "passes explicit empty attachment arrays through the projection pipeline to clear attachments", |
| 363 | () => |
| 364 | Effect.gen(function* () { |
| 365 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |
| 366 | const eventStore = yield* OrchestrationEventStore; |
| 367 | const sql = yield* SqlClient.SqlClient; |
| 368 | const now = new Date().toISOString(); |
| 369 | const later = new Date(Date.now() + 1_000).toISOString(); |
| 370 | |
| 371 | yield* eventStore.append({ |
| 372 | type: "project.created", |
| 373 | eventId: EventId.makeUnsafe("evt-clear-attachments-1"), |
| 374 | aggregateKind: "project", |
| 375 | aggregateId: ProjectId.makeUnsafe("project-clear-attachments"), |
| 376 | occurredAt: now, |
| 377 | commandId: CommandId.makeUnsafe("cmd-clear-attachments-1"), |
| 378 | causationEventId: null, |
| 379 | correlationId: CommandId.makeUnsafe("cmd-clear-attachments-1"), |
| 380 | metadata: {}, |
| 381 | payload: { |
| 382 | projectId: ProjectId.makeUnsafe("project-clear-attachments"), |
| 383 | title: "Project Clear Attachments", |
| 384 | workspaceRoot: "/tmp/project-clear-attachments", |
| 385 | defaultModel: null, |
| 386 | scripts: [], |
| 387 | createdAt: now, |
| 388 | updatedAt: now, |
| 389 | }, |
| 390 | }); |
| 391 | |
| 392 | yield* eventStore.append({ |
| 393 | type: "thread.created", |
| 394 | eventId: EventId.makeUnsafe("evt-clear-attachments-2"), |
| 395 | aggregateKind: "thread", |
| 396 | aggregateId: ThreadId.makeUnsafe("thread-clear-attachments"), |
| 397 | occurredAt: now, |
| 398 | commandId: CommandId.makeUnsafe("cmd-clear-attachments-2"), |
| 399 | causationEventId: null, |
| 400 | correlationId: CommandId.makeUnsafe("cmd-clear-attachments-2"), |
| 401 | metadata: {}, |
| 402 | payload: { |
| 403 | threadId: ThreadId.makeUnsafe("thread-clear-attachments"), |
| 404 | projectId: ProjectId.makeUnsafe("project-clear-attachments"), |
| 405 | title: "Thread Clear Attachments", |
| 406 | model: "gpt-5-codex", |
| 407 | branch: null, |
| 408 | worktreePath: null, |
| 409 | createdAt: now, |
| 410 | updatedAt: now, |
| 411 | }, |
| 412 | }); |
| 413 | |
| 414 | yield* eventStore.append({ |
| 415 | type: "thread.message-sent", |
| 416 | eventId: EventId.makeUnsafe("evt-clear-attachments-3"), |
| 417 | aggregateKind: "thread", |
| 418 | aggregateId: ThreadId.makeUnsafe("thread-clear-attachments"), |
| 419 | occurredAt: now, |
| 420 | commandId: CommandId.makeUnsafe("cmd-clear-attachments-3"), |
| 421 | causationEventId: null, |
| 422 | correlationId: CommandId.makeUnsafe("cmd-clear-attachments-3"), |
| 423 | metadata: {}, |
| 424 | payload: { |
| 425 | threadId: ThreadId.makeUnsafe("thread-clear-attachments"), |
| 426 | messageId: MessageId.makeUnsafe("message-clear-attachments"), |
| 427 | role: "user", |
| 428 | text: "Has attachments", |
| 429 | attachments: [ |
| 430 | { |
| 431 | type: "image", |
| 432 | name: "clear.png", |
| 433 | mimeType: "image/png", |
| 434 | sizeBytes: 5, |
| 435 | dataUrl: "data:image/png;base64,SGVsbG8=", |
| 436 | }, |
| 437 | ], |
| 438 | turnId: null, |
| 439 | streaming: false, |
| 440 | createdAt: now, |
| 441 | updatedAt: now, |
| 442 | }, |
| 443 | }); |
| 444 | |
| 445 | yield* eventStore.append({ |
| 446 | type: "thread.message-sent", |
| 447 | eventId: EventId.makeUnsafe("evt-clear-attachments-4"), |
| 448 | aggregateKind: "thread", |
| 449 | aggregateId: ThreadId.makeUnsafe("thread-clear-attachments"), |
| 450 | occurredAt: later, |
| 451 | commandId: CommandId.makeUnsafe("cmd-clear-attachments-4"), |
| 452 | causationEventId: null, |
| 453 | correlationId: CommandId.makeUnsafe("cmd-clear-attachments-4"), |
| 454 | metadata: {}, |
| 455 | payload: { |
| 456 | threadId: ThreadId.makeUnsafe("thread-clear-attachments"), |
| 457 | messageId: MessageId.makeUnsafe("message-clear-attachments"), |
| 458 | role: "user", |
| 459 | text: "", |
| 460 | attachments: [], |
| 461 | turnId: null, |
| 462 | streaming: false, |
| 463 | createdAt: now, |
| 464 | updatedAt: later, |
| 465 | }, |
| 466 | }); |
| 467 | |
| 468 | yield* projectionPipeline.bootstrap; |
| 469 | |
| 470 | const rows = yield* sql<{ |
| 471 | readonly attachmentsJson: string | null; |
| 472 | }>` |
| 473 | SELECT |
| 474 | attachments_json AS "attachmentsJson" |
| 475 | FROM projection_thread_messages |
| 476 | WHERE message_id = 'message-clear-attachments' |
| 477 | `; |
| 478 | assert.equal(rows.length, 1); |
| 479 | assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), []); |
| 480 | }), |
| 481 | ); |
| 482 | |
| 361 | 483 | it.effect("does not persist attachment files when projector transaction rolls back", () => |
| 362 | 484 | Effect.gen(function* () { |
| 363 | 485 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |