|
@@ -14,6 +14,8 @@ import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/m
|
|
|
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
|
|
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
|
|
|
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
|
|
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
|
|
|
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
|
|
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
|
|
|
|
|
+import resetV2SessionStateMigration from "@opencode-ai/core/database/migration/20260622170816_reset_v2_session_state"
|
|
|
|
|
+import { EventV2 } from "@opencode-ai/core/event"
|
|
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
|
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
|
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
|
@@ -22,6 +24,8 @@ import { SessionTable } from "@opencode-ai/core/session/sql"
|
|
|
import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata"
|
|
import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata"
|
|
|
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
|
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
|
|
import { Database } from "@opencode-ai/core/database/database"
|
|
import { Database } from "@opencode-ai/core/database/database"
|
|
|
|
|
+import { SessionProjector } from "@opencode-ai/core/session/projector"
|
|
|
|
|
+import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|
|
import { tmpdir } from "./fixture/tmpdir"
|
|
import { tmpdir } from "./fixture/tmpdir"
|
|
|
|
|
|
|
|
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
|
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
|
@@ -226,6 +230,94 @@ describe("DatabaseMigration", () => {
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ test("preserves canonical V1 state and restarts its event stream", async () => {
|
|
|
|
|
+ await run(
|
|
|
|
|
+ Effect.gen(function* () {
|
|
|
|
|
+ const db = yield* makeDb
|
|
|
|
|
+ yield* db.run(sql`PRAGMA foreign_keys = ON`)
|
|
|
|
|
+ yield* DatabaseMigration.apply(db)
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('global', '/project', 1, 1, '[]')`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO workspace (id, type, project_id, time_used) VALUES ('workspace', 'local', 'global', 1)`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO session (id, project_id, workspace_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'global', 'workspace', 'session', '/project', 'Before', 'test', 1, 1)`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('message', 'session', 1, 1, '{}')`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('part', 'message', 'session', 1, 1, '{}')`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(sql`INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 9)`)
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO event (id, aggregate_id, seq, type, data) VALUES ('event', 'session', 9, 'session.updated.1', '{}')`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO session_input (id, session_id, prompt, delivery, admitted_seq, time_created) VALUES ('input', 'session', '{}', 'steer', 9, 1)`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(
|
|
|
|
|
+ sql`INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
|
|
|
|
|
+ )
|
|
|
|
|
+ yield* db.run(sql`DELETE FROM migration WHERE id = ${resetV2SessionStateMigration.id}`)
|
|
|
|
|
+ yield* DatabaseMigration.applyOnly(db, [resetV2SessionStateMigration])
|
|
|
|
|
+
|
|
|
|
|
+ const database = Layer.succeed(Database.Service, { db })
|
|
|
|
|
+ const events = EventV2.layer.pipe(Layer.provide(database))
|
|
|
|
|
+ yield* EventV2.Service.use((service) =>
|
|
|
|
|
+ service.publish(SessionV1.Event.Updated, {
|
|
|
|
|
+ sessionID: SessionSchema.ID.make("session"),
|
|
|
|
|
+ info: {
|
|
|
|
|
+ id: SessionSchema.ID.make("session"),
|
|
|
|
|
+ slug: "session",
|
|
|
|
|
+ projectID: ProjectV2.ID.global,
|
|
|
|
|
+ directory: "/project",
|
|
|
|
|
+ title: "After",
|
|
|
|
|
+ version: "test",
|
|
|
|
|
+ time: { created: 1, updated: 2 },
|
|
|
|
|
+ },
|
|
|
|
|
+ }),
|
|
|
|
|
+ ).pipe(
|
|
|
|
|
+ Effect.provide(
|
|
|
|
|
+ Layer.merge(events, SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))),
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ expect(
|
|
|
|
|
+ yield* db.get(sql`
|
|
|
|
|
+ SELECT
|
|
|
|
|
+ (SELECT title FROM session WHERE id = 'session') AS title,
|
|
|
|
|
+ (SELECT workspace_id FROM session WHERE id = 'session') AS workspaceID,
|
|
|
|
|
+ (SELECT COUNT(*) FROM message WHERE id = 'message') AS messages,
|
|
|
|
|
+ (SELECT COUNT(*) FROM part WHERE id = 'part') AS parts,
|
|
|
|
|
+ (SELECT COUNT(*) FROM workspace) AS workspaces,
|
|
|
|
|
+ (SELECT COUNT(*) FROM session_input) AS sessionInputs,
|
|
|
|
|
+ (SELECT COUNT(*) FROM session_message) AS sessionMessages,
|
|
|
|
|
+ (SELECT COUNT(*) FROM session_context_epoch) AS contextEpochs,
|
|
|
|
|
+ (SELECT seq FROM event_sequence WHERE aggregate_id = 'session') AS seq,
|
|
|
|
|
+ (SELECT type FROM event WHERE aggregate_id = 'session') AS eventType
|
|
|
|
|
+ `),
|
|
|
|
|
+ ).toEqual({
|
|
|
|
|
+ title: "After",
|
|
|
|
|
+ workspaceID: null,
|
|
|
|
|
+ messages: 1,
|
|
|
|
|
+ parts: 1,
|
|
|
|
|
+ workspaces: 0,
|
|
|
|
|
+ sessionInputs: 0,
|
|
|
|
|
+ sessionMessages: 0,
|
|
|
|
|
+ contextEpochs: 0,
|
|
|
|
|
+ seq: 0,
|
|
|
|
|
+ eventType: "session.updated.1",
|
|
|
|
|
+ })
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
test("resets incompatible projected Session messages before adding sequence order", async () => {
|
|
test("resets incompatible projected Session messages before adding sequence order", async () => {
|
|
|
await run(
|
|
await run(
|
|
|
Effect.gen(function* () {
|
|
Effect.gen(function* () {
|