| ... | @@ -480,6 +480,149 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -480,6 +480,149 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 480 | }), | 480 | }), |
| 481 | ); | 481 | ); |
| 482 | | 482 | |
| | 483 | it.effect("overwrites attachment file bytes when a message updates the same attachment index", () => |
| | 484 | Effect.gen(function* () { |
| | 485 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |
| | 486 | const eventStore = yield* OrchestrationEventStore; |
| | 487 | const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-projection-attachments-overwrite-")); |
| | 488 | const now = new Date().toISOString(); |
| | 489 | const later = new Date(Date.now() + 1_000).toISOString(); |
| | 490 | |
| | 491 | const serverConfig = { |
| | 492 | mode: "web", |
| | 493 | port: 0, |
| | 494 | host: undefined, |
| | 495 | cwd: "/tmp/project-attachments", |
| | 496 | keybindingsConfigPath: path.join(stateDir, "keybindings.json"), |
| | 497 | stateDir, |
| | 498 | staticDir: undefined, |
| | 499 | devUrl: undefined, |
| | 500 | noBrowser: true, |
| | 501 | authToken: undefined, |
| | 502 | autoBootstrapProjectFromCwd: false, |
| | 503 | logWebSocketEvents: false, |
| | 504 | } satisfies ServerConfigShape; |
| | 505 | |
| | 506 | yield* eventStore.append({ |
| | 507 | type: "project.created", |
| | 508 | eventId: EventId.makeUnsafe("evt-overwrite-1"), |
| | 509 | aggregateKind: "project", |
| | 510 | aggregateId: ProjectId.makeUnsafe("project-overwrite"), |
| | 511 | occurredAt: now, |
| | 512 | commandId: CommandId.makeUnsafe("cmd-overwrite-1"), |
| | 513 | causationEventId: null, |
| | 514 | correlationId: CommandId.makeUnsafe("cmd-overwrite-1"), |
| | 515 | metadata: {}, |
| | 516 | payload: { |
| | 517 | projectId: ProjectId.makeUnsafe("project-overwrite"), |
| | 518 | title: "Project Overwrite", |
| | 519 | workspaceRoot: "/tmp/project-overwrite", |
| | 520 | defaultModel: null, |
| | 521 | scripts: [], |
| | 522 | createdAt: now, |
| | 523 | updatedAt: now, |
| | 524 | }, |
| | 525 | }); |
| | 526 | |
| | 527 | yield* eventStore.append({ |
| | 528 | type: "thread.created", |
| | 529 | eventId: EventId.makeUnsafe("evt-overwrite-2"), |
| | 530 | aggregateKind: "thread", |
| | 531 | aggregateId: ThreadId.makeUnsafe("thread-overwrite"), |
| | 532 | occurredAt: now, |
| | 533 | commandId: CommandId.makeUnsafe("cmd-overwrite-2"), |
| | 534 | causationEventId: null, |
| | 535 | correlationId: CommandId.makeUnsafe("cmd-overwrite-2"), |
| | 536 | metadata: {}, |
| | 537 | payload: { |
| | 538 | threadId: ThreadId.makeUnsafe("thread-overwrite"), |
| | 539 | projectId: ProjectId.makeUnsafe("project-overwrite"), |
| | 540 | title: "Thread Overwrite", |
| | 541 | model: "gpt-5-codex", |
| | 542 | branch: null, |
| | 543 | worktreePath: null, |
| | 544 | createdAt: now, |
| | 545 | updatedAt: now, |
| | 546 | }, |
| | 547 | }); |
| | 548 | |
| | 549 | yield* eventStore.append({ |
| | 550 | type: "thread.message-sent", |
| | 551 | eventId: EventId.makeUnsafe("evt-overwrite-3"), |
| | 552 | aggregateKind: "thread", |
| | 553 | aggregateId: ThreadId.makeUnsafe("thread-overwrite"), |
| | 554 | occurredAt: now, |
| | 555 | commandId: CommandId.makeUnsafe("cmd-overwrite-3"), |
| | 556 | causationEventId: null, |
| | 557 | correlationId: CommandId.makeUnsafe("cmd-overwrite-3"), |
| | 558 | metadata: {}, |
| | 559 | payload: { |
| | 560 | threadId: ThreadId.makeUnsafe("thread-overwrite"), |
| | 561 | messageId: MessageId.makeUnsafe("message-overwrite"), |
| | 562 | role: "user", |
| | 563 | text: "first image", |
| | 564 | attachments: [ |
| | 565 | { |
| | 566 | type: "image", |
| | 567 | name: "file.png", |
| | 568 | mimeType: "image/png", |
| | 569 | sizeBytes: 5, |
| | 570 | dataUrl: "data:image/png;base64,SGVsbG8=", |
| | 571 | }, |
| | 572 | ], |
| | 573 | turnId: null, |
| | 574 | streaming: false, |
| | 575 | createdAt: now, |
| | 576 | updatedAt: now, |
| | 577 | }, |
| | 578 | }); |
| | 579 | |
| | 580 | yield* eventStore.append({ |
| | 581 | type: "thread.message-sent", |
| | 582 | eventId: EventId.makeUnsafe("evt-overwrite-4"), |
| | 583 | aggregateKind: "thread", |
| | 584 | aggregateId: ThreadId.makeUnsafe("thread-overwrite"), |
| | 585 | occurredAt: later, |
| | 586 | commandId: CommandId.makeUnsafe("cmd-overwrite-4"), |
| | 587 | causationEventId: null, |
| | 588 | correlationId: CommandId.makeUnsafe("cmd-overwrite-4"), |
| | 589 | metadata: {}, |
| | 590 | payload: { |
| | 591 | threadId: ThreadId.makeUnsafe("thread-overwrite"), |
| | 592 | messageId: MessageId.makeUnsafe("message-overwrite"), |
| | 593 | role: "user", |
| | 594 | text: "", |
| | 595 | attachments: [ |
| | 596 | { |
| | 597 | type: "image", |
| | 598 | name: "file.png", |
| | 599 | mimeType: "image/png", |
| | 600 | sizeBytes: 5, |
| | 601 | dataUrl: "data:image/png;base64,V29ybGQ=", |
| | 602 | }, |
| | 603 | ], |
| | 604 | turnId: null, |
| | 605 | streaming: false, |
| | 606 | createdAt: now, |
| | 607 | updatedAt: later, |
| | 608 | }, |
| | 609 | }); |
| | 610 | |
| | 611 | yield* projectionPipeline.bootstrap.pipe(Effect.provideService(ServerConfig, serverConfig)); |
| | 612 | |
| | 613 | const attachmentPath = path.join( |
| | 614 | stateDir, |
| | 615 | "attachments", |
| | 616 | "thread-overwrite", |
| | 617 | "message-overwrite", |
| | 618 | "0.png", |
| | 619 | ); |
| | 620 | assert.equal(fs.existsSync(attachmentPath), true); |
| | 621 | assert.deepEqual(fs.readFileSync(attachmentPath), Buffer.from("V29ybGQ=", "base64")); |
| | 622 | fs.rmSync(stateDir, { recursive: true, force: true }); |
| | 623 | }), |
| | 624 | ); |
| | 625 | |
| 483 | it.effect("does not persist attachment files when projector transaction rolls back", () => | 626 | it.effect("does not persist attachment files when projector transaction rolls back", () => |
| 484 | Effect.gen(function* () { | 627 | Effect.gen(function* () { |
| 485 | const projectionPipeline = yield* OrchestrationProjectionPipeline; | 628 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |