refactor(core): simplify event model (#33238)
This commit is contained in:
@@ -30,7 +30,7 @@ const Message = EventV2.define({
|
||||
|
||||
const SyncMessage = EventV2.define({
|
||||
type: "test.sync",
|
||||
sync: {
|
||||
durable: {
|
||||
version: 1,
|
||||
aggregate: "id",
|
||||
},
|
||||
@@ -42,7 +42,7 @@ const SyncMessage = EventV2.define({
|
||||
|
||||
const SyncSent = EventV2.define({
|
||||
type: "test.sent",
|
||||
sync: {
|
||||
durable: {
|
||||
version: 1,
|
||||
aggregate: "messageID",
|
||||
},
|
||||
@@ -61,7 +61,7 @@ const GlobalMessage = EventV2.define({
|
||||
|
||||
const VersionedMessage = EventV2.define({
|
||||
type: "test.versioned",
|
||||
sync: {
|
||||
durable: {
|
||||
version: 2,
|
||||
aggregate: "id",
|
||||
},
|
||||
@@ -73,7 +73,7 @@ const VersionedMessage = EventV2.define({
|
||||
|
||||
const SyncTimestamp = EventV2.define({
|
||||
type: "test.timestamp",
|
||||
sync: {
|
||||
durable: {
|
||||
version: 1,
|
||||
aggregate: "id",
|
||||
},
|
||||
@@ -132,7 +132,7 @@ describe("EventV2", () => {
|
||||
const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" })
|
||||
|
||||
expect(event.type).toBe("test.versioned")
|
||||
expect(event.version).toBe(2)
|
||||
expect(event.durable?.version).toBe(2)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -146,12 +146,12 @@ describe("EventV2", () => {
|
||||
Effect.sync(() => {
|
||||
const latest = EventV2.define({
|
||||
type: "test.out-of-order",
|
||||
sync: { version: 2, aggregate: "id" },
|
||||
durable: { version: 2, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
EventV2.define({
|
||||
type: "test.out-of-order",
|
||||
sync: { version: 1, aggregate: "id" },
|
||||
durable: { version: 1, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
|
||||
@@ -190,7 +190,7 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("commits local operational state inside a new synchronized event transaction", () =>
|
||||
it.effect("commits local operational state inside a new durable event transaction", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<string>()
|
||||
@@ -207,7 +207,7 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rolls back the synchronized event and projector when the local commit fails", () =>
|
||||
it.effect("rolls back the durable event and projector when the local commit fails", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
@@ -236,7 +236,7 @@ describe("EventV2", () => {
|
||||
const events = yield* EventV2.Service
|
||||
const exit = yield* events.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit)
|
||||
|
||||
expect(String(exit)).toContain("Local commit hooks require a synchronized event")
|
||||
expect(String(exit)).toContain("Local commit hooks require a durable event")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -290,7 +290,6 @@ describe("EventV2", () => {
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<string>()
|
||||
yield* events.sync(() => Effect.die("sync defect"))
|
||||
yield* events.listen(() => {
|
||||
throw new Error("listener defect")
|
||||
})
|
||||
@@ -303,7 +302,7 @@ describe("EventV2", () => {
|
||||
const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" })
|
||||
|
||||
expect(received).toEqual([SyncMessage.type])
|
||||
expect(event.seq).toBeNumber()
|
||||
expect(event.durable?.seq).toBeNumber()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -336,49 +335,7 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not synchronize live-only events", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const synchronized = new Array<string>()
|
||||
const unsubscribe = yield* events.sync((event) =>
|
||||
Effect.sync(() => {
|
||||
synchronized.push(event.type)
|
||||
}),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
|
||||
yield* events.publish(Message, { text: "live only" })
|
||||
yield* events.publish(SyncMessage, { id: "one", text: "durable" })
|
||||
|
||||
expect(synchronized).toEqual([SyncMessage.type])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("synchronizes only after the durable event commits", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
const synchronized = new Array<boolean>()
|
||||
yield* events.sync((event) =>
|
||||
db
|
||||
.select({ id: EventTable.id })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.id, event.id))
|
||||
.get()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((row) => synchronized.push(row !== undefined)),
|
||||
Effect.asVoid,
|
||||
),
|
||||
)
|
||||
|
||||
yield* events.publish(SyncMessage, { id: EventV2.ID.create(), text: "durable" })
|
||||
|
||||
expect(synchronized).toEqual([true])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("inserts sync event rows on publish", () =>
|
||||
it.effect("inserts durable event rows on publish", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
@@ -398,7 +355,7 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("increments sync event seq per aggregate", () =>
|
||||
it.effect("increments durable event seq per aggregate", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
@@ -417,22 +374,22 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("replays durable aggregate events after a cursor and tails new events", () =>
|
||||
it.effect("replays durable aggregate events after a sequence and tails new events", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const aggregateID = EventV2.ID.create()
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
||||
const fiber = yield* events
|
||||
.aggregateEvents({ aggregateID, after: EventV2.Cursor.make(0) })
|
||||
.durable({ aggregateID, after: 0 })
|
||||
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
|
||||
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual([
|
||||
[EventV2.Cursor.make(1), { id: aggregateID, text: "one" }],
|
||||
[EventV2.Cursor.make(2), { id: aggregateID, text: "two" }],
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
|
||||
[1, { id: aggregateID, text: "one" }],
|
||||
[2, { id: aggregateID, text: "two" }],
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -443,19 +400,19 @@ describe("EventV2", () => {
|
||||
const aggregateID = EventV2.ID.create()
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
||||
const fiber = yield* events
|
||||
.aggregateEvents({ aggregateID })
|
||||
.durable({ aggregateID })
|
||||
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
||||
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
||||
|
||||
expect(
|
||||
Array.from(yield* Fiber.join(fiber)).map((event) => [
|
||||
event.cursor,
|
||||
(event.event.data as { text: string }).text,
|
||||
event.durable?.seq,
|
||||
(event.data as { text: string }).text,
|
||||
]),
|
||||
).toEqual([
|
||||
[EventV2.Cursor.make(0), "zero"],
|
||||
[EventV2.Cursor.make(1), "one"],
|
||||
[0, "zero"],
|
||||
[1, "one"],
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -477,7 +434,7 @@ describe("EventV2", () => {
|
||||
const events = yield* EventV2.Service
|
||||
const aggregateID = EventV2.ID.create()
|
||||
const fiber = yield* events
|
||||
.aggregateEvents({ aggregateID })
|
||||
.durable({ aggregateID })
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Deferred.await(readStarted)
|
||||
|
||||
@@ -485,8 +442,8 @@ describe("EventV2", () => {
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" })
|
||||
yield* Deferred.succeed(continueRead, undefined)
|
||||
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual([
|
||||
[EventV2.Cursor.make(0), { id: aggregateID, text: "during handoff" }],
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
|
||||
[0, { id: aggregateID, text: "during handoff" }],
|
||||
])
|
||||
}).pipe(Effect.provide(Layer.mergeAll(database, eventLayer)))
|
||||
}),
|
||||
@@ -498,7 +455,7 @@ describe("EventV2", () => {
|
||||
const aggregateID = EventV2.ID.create()
|
||||
const count = 64
|
||||
const fiber = yield* events
|
||||
.aggregateEvents({ aggregateID })
|
||||
.durable({ aggregateID })
|
||||
.pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
@@ -506,9 +463,9 @@ describe("EventV2", () => {
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: String(index) })
|
||||
}
|
||||
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual(
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual(
|
||||
Array.from({ length: count }, (_, index) => [
|
||||
EventV2.Cursor.make(index),
|
||||
index,
|
||||
{ id: aggregateID, text: String(index) },
|
||||
]),
|
||||
)
|
||||
@@ -520,14 +477,14 @@ describe("EventV2", () => {
|
||||
const events = yield* EventV2.Service
|
||||
const aggregateID = EventV2.ID.create()
|
||||
const fiber = yield* events
|
||||
.aggregateEvents({ aggregateID })
|
||||
.durable({ aggregateID })
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* events.publish(Message, { text: "live only" })
|
||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "durable" })
|
||||
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.event.type)).toEqual([SyncMessage.type])
|
||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([SyncMessage.type])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -550,7 +507,7 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("replays sync events through projectors", () =>
|
||||
it.effect("replays durable events through projectors", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<EventV2.Payload>()
|
||||
@@ -706,7 +663,7 @@ describe("EventV2", () => {
|
||||
})
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(String(exit)).toContain("Unknown sync event type")
|
||||
expect(String(exit)).toContain("Unknown durable event type")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -843,7 +800,7 @@ describe("EventV2", () => {
|
||||
const replayed = {
|
||||
id: published.id,
|
||||
type: EventV2.versionedType(SyncMessage.type, 1),
|
||||
seq: published.seq!,
|
||||
seq: published.durable!.seq,
|
||||
aggregateID,
|
||||
data: published.data,
|
||||
}
|
||||
@@ -988,7 +945,7 @@ describe("EventV2", () => {
|
||||
yield* events.replay(replayed, { publish: true })
|
||||
yield* events.replay(replayed, { publish: true })
|
||||
|
||||
expect(received).toMatchObject([{ id: replayed.id, seq: 0, data: replayed.data }])
|
||||
expect(received).toMatchObject([{ id: replayed.id, durable: { seq: 0, version: 1 }, data: replayed.data }])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1110,7 +1067,7 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("remove clears sync event sequence", () =>
|
||||
it.effect("remove clears durable event sequence", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<EventV2.Payload>()
|
||||
|
||||
@@ -220,8 +220,8 @@ describe("SessionV2.create", () => {
|
||||
expect(
|
||||
Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(2), Stream.runCollect)),
|
||||
).toMatchObject([
|
||||
{ cursor: 1, event: { type: "session.next.prompt.admitted", data: { prompt: { text: "Hello" } } } },
|
||||
{ cursor: 2, event: { type: "session.next.prompt.promoted" } },
|
||||
{ durable: { seq: 1 }, type: "session.next.prompt.admitted", data: { prompt: { text: "Hello" } } },
|
||||
{ durable: { seq: 2 }, type: "session.next.prompt.promoted" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -355,7 +355,7 @@ describe("SessionV2.create", () => {
|
||||
expect(yield* session.get(created.id)).toMatchObject({ model })
|
||||
expect(
|
||||
Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(1), Stream.runCollect)),
|
||||
).toMatchObject([{ event: { type: "session.next.model.switched", data: { model } } }])
|
||||
).toMatchObject([{ type: "session.next.model.switched", data: { model } }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ describe("SessionProjector", () => {
|
||||
|
||||
expect(
|
||||
yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toMatchObject({ promoted_seq: event.seq })
|
||||
).toMatchObject({ promoted_seq: event.durable?.seq })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -334,134 +334,6 @@ describe("SessionProjector", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects a Prompted event that conflicts with an admitted inbox row", () =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: sessionID,
|
||||
project_id: Project.ID.global,
|
||||
slug: "test",
|
||||
directory: "/project",
|
||||
title: "test",
|
||||
version: "test",
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
const events = yield* EventV2.Service
|
||||
const id = SessionMessage.ID.make("msg_conflict")
|
||||
yield* SessionInput.admit(db, events, {
|
||||
id,
|
||||
sessionID,
|
||||
prompt: new Prompt({ text: "admitted" }),
|
||||
delivery: "steer",
|
||||
})
|
||||
|
||||
const exit = yield* events
|
||||
.publish(SessionEvent.Prompted, {
|
||||
sessionID,
|
||||
messageID: id,
|
||||
timestamp: created,
|
||||
prompt: new Prompt({ text: "different" }),
|
||||
delivery: "steer",
|
||||
})
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(String(exit)).toContain("SessionInput.LifecycleConflict")
|
||||
expect(
|
||||
yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toMatchObject({ promoted_seq: null })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects an assistant message ID that conflicts with an admitted inbox row", () =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: sessionID,
|
||||
project_id: Project.ID.global,
|
||||
slug: "test",
|
||||
directory: "/project",
|
||||
title: "test",
|
||||
version: "test",
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
const events = yield* EventV2.Service
|
||||
const id = SessionMessage.ID.make("msg_conflict")
|
||||
yield* SessionInput.admit(db, events, {
|
||||
id,
|
||||
sessionID,
|
||||
prompt: new Prompt({ text: "admitted" }),
|
||||
delivery: "steer",
|
||||
})
|
||||
|
||||
const exit = yield* events
|
||||
.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
timestamp: created,
|
||||
assistantMessageID: id,
|
||||
agent: "build",
|
||||
model,
|
||||
})
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(String(exit)).toContain("SessionInput.LifecycleConflict")
|
||||
expect(
|
||||
yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects a Prompted delivery mode that conflicts with an admitted inbox row", () =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: sessionID,
|
||||
project_id: Project.ID.global,
|
||||
slug: "test",
|
||||
directory: "/project",
|
||||
title: "test",
|
||||
version: "test",
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
const events = yield* EventV2.Service
|
||||
const id = SessionMessage.ID.make("msg_delivery_conflict")
|
||||
const prompt = new Prompt({ text: "admitted" })
|
||||
yield* SessionInput.admit(db, events, { id, sessionID, prompt, delivery: "queue" })
|
||||
|
||||
const exit = yield* events
|
||||
.publish(SessionEvent.Prompted, { sessionID, messageID: id, timestamp: created, prompt, delivery: "steer" })
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(String(exit)).toContain("SessionInput.LifecycleConflict")
|
||||
expect(
|
||||
yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toMatchObject({ delivery: "queue", promoted_seq: null })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not revive a stale incomplete in-memory assistant projection", () =>
|
||||
Effect.gen(function* () {
|
||||
const stale = new SessionMessage.Assistant({
|
||||
|
||||
@@ -177,7 +177,7 @@ describe("SessionV2.prompt", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("streams durable Session events after an aggregate cursor", () =>
|
||||
it.effect("streams durable Session events after an aggregate sequence", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
@@ -191,17 +191,17 @@ describe("SessionV2.prompt", () => {
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID, Number.MAX_SAFE_INTEGER)
|
||||
const streamed = Array.from(yield* Fiber.join(fiber))
|
||||
|
||||
expect(streamed.map((event) => [event.cursor, event.event.type])).toEqual([
|
||||
[EventV2.Cursor.make(0), "session.next.prompt.admitted"],
|
||||
[EventV2.Cursor.make(1), "session.next.prompt.admitted"],
|
||||
[EventV2.Cursor.make(2), "session.next.prompt.promoted"],
|
||||
[EventV2.Cursor.make(3), "session.next.prompt.promoted"],
|
||||
expect(streamed.map((event) => [event.durable?.seq, event.type])).toEqual([
|
||||
[0, "session.next.prompt.admitted"],
|
||||
[1, "session.next.prompt.admitted"],
|
||||
[2, "session.next.prompt.promoted"],
|
||||
[3, "session.next.prompt.promoted"],
|
||||
])
|
||||
expect(
|
||||
Array.from(
|
||||
yield* session.events({ sessionID, after: streamed[0]!.cursor }).pipe(Stream.take(1), Stream.runCollect),
|
||||
).map((event) => [event.cursor, event.event.type]),
|
||||
).toEqual([[EventV2.Cursor.make(1), "session.next.prompt.admitted"]])
|
||||
yield* session.events({ sessionID, after: streamed[0]!.durable?.seq }).pipe(Stream.take(1), Stream.runCollect),
|
||||
).map((event) => [event.durable?.seq, event.type]),
|
||||
).toEqual([[1, "session.next.prompt.admitted"]])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -472,58 +472,6 @@ describe("SessionV2.prompt", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects an input ID already used by a durable non-prompt event", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* events.publish(SessionEvent.Synthetic, {
|
||||
sessionID,
|
||||
messageID,
|
||||
timestamp: yield* DateTime.now,
|
||||
text: "Collision",
|
||||
})
|
||||
|
||||
const failure = yield* session
|
||||
.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Collision" }), resume: false })
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(failure._tag).toBe("Session.PromptConflictError")
|
||||
expect(yield* admitted(messageID)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects a durable event ID reserved by an admitted prompt without poisoning promotion", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const { db } = yield* Database.Service
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const prompt = new Prompt({ text: "Reserved prompt" })
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt, resume: false })
|
||||
|
||||
const failure = yield* events
|
||||
.publish(SessionEvent.Synthetic, {
|
||||
sessionID,
|
||||
messageID,
|
||||
timestamp: yield* DateTime.now,
|
||||
text: "Conflicting synthetic",
|
||||
})
|
||||
.pipe(Effect.catchDefect(Effect.succeed))
|
||||
|
||||
expect(String(failure)).toContain("SessionInput.LifecycleConflict")
|
||||
expect(yield* admitted(messageID)).not.toHaveProperty("promotedSeq")
|
||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID, Number.MAX_SAFE_INTEGER)
|
||||
|
||||
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 1 })
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{ id: messageID, type: "user", text: "Reserved prompt" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects reuse of one globally unique message ID across sessions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
|
||||
@@ -19,17 +19,17 @@ const capture = () => {
|
||||
Effect.sync(() => {
|
||||
const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload<typeof definition>
|
||||
published.push({
|
||||
type: definition.sync ? EventV2.versionedType(definition.type, definition.sync.version) : definition.type,
|
||||
type: definition.durable
|
||||
? EventV2.versionedType(definition.type, definition.durable.version)
|
||||
: definition.type,
|
||||
data,
|
||||
})
|
||||
return event
|
||||
}),
|
||||
subscribe: () => Stream.empty,
|
||||
all: () => Stream.empty,
|
||||
aggregateEvents: () => Stream.empty,
|
||||
sync: () => Effect.succeed(Effect.void),
|
||||
durable: () => Stream.empty,
|
||||
listen: () => Effect.succeed(Effect.void),
|
||||
beforeCommit: () => Effect.void,
|
||||
project: () => Effect.void,
|
||||
replay: () => Effect.void,
|
||||
replayAll: () => Effect.succeed(undefined),
|
||||
|
||||
@@ -1355,34 +1355,6 @@ describe("SessionRunnerLLM", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("replays retained context projections while replacement is pending", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
systemBaseline = "Changed context"
|
||||
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
yield* events.publish(SessionEvent.ModelSwitched, {
|
||||
sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
|
||||
})
|
||||
|
||||
yield* replaySessionProjection(sessionID)
|
||||
systemBaseline = "Replacement context"
|
||||
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Replacement context"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("replaces the baseline lazily after completed compaction without reopening replacement on replay", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
|
||||
Reference in New Issue
Block a user