Reset snooze metadata when creating forks and provider subagent children, with focused regressions for both inheritance paths.
Showing the changes to apps/server/src/orchestration-v2/ThreadForkService.test.ts (1 of 4 files). Show all
Unified · Split
| @@ -0,0 +1,147 @@ | |||
| 1 | import { assert, it } from "@effect/vitest"; | ||
| 2 | import { | ||
| 3 | ContextTransferId, | ||
| 4 | MessageId, | ||
| 5 | type ModelSelection, | ||
| 6 | type OrchestrationV2AppThread, | ||
| 7 | type OrchestrationV2Run, | ||
| 8 | type OrchestrationV2ThreadProjection, | ||
| 9 | ProjectId, | ||
| 10 | ProviderInstanceId, | ||
| 11 | ProviderThreadId, | ||
| 12 | RunId, | ||
| 13 | ThreadId, | ||
| 14 | } from "@t3tools/contracts"; | ||
| 15 | import * as DateTime from "effect/DateTime"; | ||
| 16 | import * as Effect from "effect/Effect"; | ||
| 17 | |||
| 18 | import { layer, ThreadForkServiceV2 } from "./ThreadForkService.ts"; | ||
| 19 | |||
| 20 | const sourceThreadId = ThreadId.make("thread:fork-snoozed-source"); | ||
| 21 | const targetThreadId = ThreadId.make("thread:fork-awake-target"); | ||
| 22 | const sourceRunId = RunId.make("run:fork-snoozed-source"); | ||
| 23 | const providerInstanceId = ProviderInstanceId.make("codex"); | ||
| 24 | const modelSelection = { | ||
| 25 | instanceId: providerInstanceId, | ||
| 26 | model: "gpt-5.4", | ||
| 27 | } satisfies ModelSelection; | ||
| 28 | const sourceCreatedAt = DateTime.makeUnsafe("2026-07-24T09:00:00.000Z"); | ||
| 29 | const snoozedAt = DateTime.makeUnsafe("2026-07-24T09:05:00.000Z"); | ||
| 30 | const snoozedUntil = DateTime.makeUnsafe("2026-07-25T09:00:00.000Z"); | ||
| 31 | const forkCreatedAt = DateTime.makeUnsafe("2026-07-24T09:10:00.000Z"); | ||
| 32 | |||
| 33 | function makeSourceThread(): OrchestrationV2AppThread { | ||
| 34 | return { | ||
| 35 | createdBy: "user", | ||
| 36 | creationSource: "web", | ||
| 37 | id: sourceThreadId, | ||
| 38 | projectId: ProjectId.make("project:fork-snooze"), | ||
| 39 | title: "Snoozed source", | ||
| 40 | providerInstanceId, | ||
| 41 | modelSelection, | ||
| 42 | runtimeMode: "full-access", | ||
| 43 | interactionMode: "plan", | ||
| 44 | branch: "feature/source", | ||
| 45 | worktreePath: "/tmp/source-worktree", | ||
| 46 | activeProviderThreadId: ProviderThreadId.make("provider-thread:fork-snoozed-source"), | ||
| 47 | lineage: { | ||
| 48 | parentThreadId: null, | ||
| 49 | relationshipToParent: null, | ||
| 50 | rootThreadId: sourceThreadId, | ||
| 51 | }, | ||
| 52 | forkedFrom: null, | ||
| 53 | createdAt: sourceCreatedAt, | ||
| 54 | updatedAt: snoozedAt, | ||
| 55 | archivedAt: null, | ||
| 56 | settledOverride: null, | ||
| 57 | settledAt: null, | ||
| 58 | snoozedUntil, | ||
| 59 | snoozedAt, | ||
| 60 | deletedAt: null, | ||
| 61 | }; | ||
| 62 | } | ||
| 63 | |||
| 64 | function makeCompletedSourceRun(): OrchestrationV2Run { | ||
| 65 | return { | ||
| 66 | id: sourceRunId, | ||
| 67 | threadId: sourceThreadId, | ||
| 68 | ordinal: 1, | ||
| 69 | providerInstanceId, | ||
| 70 | modelSelection, | ||
| 71 | providerThreadId: ProviderThreadId.make("provider-thread:fork-snoozed-source"), | ||
| 72 | userMessageId: MessageId.make("message:fork-snoozed-source"), | ||
| 73 | rootNodeId: null, | ||
| 74 | activeAttemptId: null, | ||
| 75 | status: "completed", | ||
| 76 | queuePosition: null, | ||
| 77 | requestedAt: sourceCreatedAt, | ||
| 78 | startedAt: sourceCreatedAt, | ||
| 79 | completedAt: snoozedAt, | ||
| 80 | checkpointId: null, | ||
| 81 | contextHandoffId: null, | ||
| 82 | }; | ||
| 83 | } | ||
| 84 | |||
| 85 | it.effect("keeps a fork awake when its source thread is snoozed", () => | ||
| 86 | Effect.gen(function* () { | ||
| 87 | const sourceThread = makeSourceThread(); | ||
| 88 | const sourceRun = makeCompletedSourceRun(); | ||
| 89 | const sourceProjection: OrchestrationV2ThreadProjection = { | ||
| 90 | thread: sourceThread, | ||
| 91 | runs: [sourceRun], | ||
| 92 | attempts: [], | ||
| 93 | nodes: [], | ||
| 94 | subagents: [], | ||
| 95 | providerSessions: [], | ||
| 96 | providerThreads: [], | ||
| 97 | providerTurns: [], | ||
| 98 | runtimeRequests: [], | ||
| 99 | messages: [], | ||
| 100 | plans: [], | ||
| 101 | turnItems: [], | ||
| 102 | checkpointScopes: [], | ||
| 103 | checkpoints: [], | ||
| 104 | contextHandoffs: [], | ||
| 105 | contextTransfers: [], | ||
| 106 | visibleTurnItems: [], | ||
| 107 | updatedAt: snoozedAt, | ||
| 108 | }; | ||
| 109 | const service = yield* ThreadForkServiceV2; | ||
| 110 | const result = yield* service.plan({ | ||
| 111 | sourceProjection, | ||
| 112 | sourceRun, | ||
| 113 | sourceProviderThread: undefined, | ||
| 114 | canonicalSourcePoint: { | ||
| 115 | threadId: sourceThreadId, | ||
| 116 | runId: sourceRunId, | ||
| 117 | }, | ||
| 118 | transferId: ContextTransferId.make("context-transfer:fork-snoozed-source"), | ||
| 119 | targetThreadId, | ||
| 120 | title: "Awake fork", | ||
| 121 | createdBy: "user", | ||
| 122 | creationSource: "mobile", | ||
| 123 | createdAt: forkCreatedAt, | ||
| 124 | }); | ||
| 125 | |||
| 126 | assert.isNull(result.targetThread.snoozedUntil); | ||
| 127 | assert.isNull(result.targetThread.snoozedAt); | ||
| 128 | assert.equal(result.targetThread.projectId, sourceThread.projectId); | ||
| 129 | assert.equal(result.targetThread.providerInstanceId, sourceThread.providerInstanceId); | ||
| 130 | assert.deepEqual(result.targetThread.modelSelection, sourceThread.modelSelection); | ||
| 131 | assert.equal(result.targetThread.runtimeMode, sourceThread.runtimeMode); | ||
| 132 | assert.equal(result.targetThread.interactionMode, sourceThread.interactionMode); | ||
| 133 | assert.equal(result.targetThread.branch, sourceThread.branch); | ||
| 134 | assert.equal(result.targetThread.worktreePath, sourceThread.worktreePath); | ||
| 135 | assert.isNull(result.targetThread.activeProviderThreadId); | ||
| 136 | assert.deepEqual(result.targetThread.lineage, { | ||
| 137 | parentThreadId: sourceThreadId, | ||
| 138 | relationshipToParent: "fork", | ||
| 139 | rootThreadId: sourceThreadId, | ||
| 140 | }); | ||
| 141 | assert.deepEqual(result.targetThread.forkedFrom, { | ||
| 142 | type: "run", | ||
| 143 | threadId: sourceThreadId, | ||
| 144 | runId: sourceRunId, | ||
| 145 | }); | ||
| 146 | }).pipe(Effect.provide(layer)), | ||
| 147 | ); | ||