|
| 1 | +import { |
| 2 | + ProjectId, |
| 3 | + ThreadId, |
| 4 | + type OrchestrationV2AppThread, |
| 5 | + type OrchestrationV2DomainEvent, |
| 6 | + type ThreadPullRequestLink, |
| 7 | +} from "@t3tools/contracts"; |
1 | 8 | import { assert, expect, it } from "@effect/vitest"; |
| 9 | +import * as DateTime from "effect/DateTime"; |
2 | 10 | import * as Effect from "effect/Effect"; |
3 | 11 | import * as Schema from "effect/Schema"; |
4 | 12 | import * as SqlClient from "effect/sql/SqlClient"; |
5 | 13 |
|
6 | 14 | import * as SqlitePersistence from "../persistence/Sqlite.ts"; |
7 | | -import { listLinkedPullRequestThreads } from "./linkedThreads.ts"; |
| 15 | +import { listLinkedPullRequestThreads, makeLinkedThreadsChangeDetector } from "./linkedThreads.ts"; |
8 | 16 |
|
9 | 17 | const encodePayload = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); |
10 | 18 |
|
@@ -209,3 +217,53 @@ it.effect( |
209 | 217 | ); |
210 | 218 | }).pipe(Effect.provide(SqlitePersistence.layerMemory)), |
211 | 219 | ); |
| 220 | + |
| 221 | +it("signals linked-thread changes only when a linked answer could change", () => { |
| 222 | + const now = DateTime.makeUnsafe("2026-11-12T00:00:00.000Z"); |
| 223 | + const threadId = ThreadId.make("thread-a"); |
| 224 | + const link: ThreadPullRequestLink = { |
| 225 | + host: "github.com", |
| 226 | + repository: "acme/web", |
| 227 | + number: 7, |
| 228 | + url: "https://github.com/acme/web/pull/7", |
| 229 | + source: "agent", |
| 230 | + linkedAt: "2026-11-12T00:00:00.000Z", |
| 231 | + snapshot: null, |
| 232 | + stack: null, |
| 233 | + }; |
| 234 | + const base = { |
| 235 | + id: threadId, |
| 236 | + projectId: ProjectId.make("project-a"), |
| 237 | + title: "Thread A", |
| 238 | + lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, |
| 239 | + archivedAt: null, |
| 240 | + settledOverride: null, |
| 241 | + settledAt: null, |
| 242 | + deletedAt: null, |
| 243 | + } as unknown as OrchestrationV2AppThread; |
| 244 | + const event = ( |
| 245 | + type: OrchestrationV2DomainEvent["type"], |
| 246 | + payload: Partial<OrchestrationV2AppThread>, |
| 247 | + ) => ({ type, threadId, payload: { ...base, ...payload } }) as OrchestrationV2DomainEvent; |
| 248 | + const changed = makeLinkedThreadsChangeDetector(); |
| 249 | + |
| 250 | + expect( |
| 251 | + [ |
| 252 | + event("thread.metadata-updated", { title: "Unlinked rename" }), |
| 253 | + event("thread.pull-request-synced", { pullRequests: [link] }), |
| 254 | + event("thread.pull-request-synced", { |
| 255 | + pullRequests: [{ ...link, watch: { since: "2026-11-12T00:00:00.000Z" } as never }], |
| 256 | + }), |
| 257 | + event("thread.visited", { pullRequests: [link], title: "Ignored event" }), |
| 258 | + { type: "thread.pull-request-link-updated", threadId, payload: link } as never, |
| 259 | + event("thread.metadata-updated", { pullRequests: [link], title: "Renamed" }), |
| 260 | + event("thread.archived", { pullRequests: [link], title: "Renamed", archivedAt: now }), |
| 261 | + event("thread.pull-request-synced", { |
| 262 | + pullRequests: [{ ...link, source: "stack-dismissed" }], |
| 263 | + title: "Renamed", |
| 264 | + archivedAt: now, |
| 265 | + }), |
| 266 | + event("thread.deleted", { pullRequests: [], deletedAt: now }), |
| 267 | + ].map(changed), |
| 268 | + ).toEqual([false, true, false, false, false, true, true, true, false]); |
| 269 | +}); |
0 commit comments