| ... | ... | @@ -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 | 626 | it.effect("does not persist attachment files when projector transaction rolls back", () => |
| 484 | 627 | Effect.gen(function* () { |
| 485 | 628 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |