Skip to content

Commit 6567721

Browse files
committed
Merge F2: extract shared SessionPrompt test layer to test/lib/prompt-harness
Diamond review: codex-5.3 spec 9/9 (initial false-positive hook-timeout flake verified non-reproducing 4/4 runs), Opus quality APPROVE WITH NITS (duplicate import merge applied), Copilot clean. Closes audit finding F2. Copilot review PR #5 (closed as review-only).
2 parents 2309cc8 + 191df38 commit 6567721

3 files changed

Lines changed: 154 additions & 275 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Shared test harness for SessionPrompt.loop tests. Both prompt.test.ts
2+
// (upstream, positive-path semantics) and subagent-hang-regression.test.ts
3+
// (ours, hang gates) compose the same service graph — real Session,
4+
// SessionPrompt, ToolRegistry, Question, Permission, plus stubbed
5+
// Summary, MCP, LSP. Consolidating here removes ~85 LOC of duplication
6+
// and ensures divergence is impossible.
7+
8+
import { NodeFileSystem } from "@effect/platform-node"
9+
import { FetchHttpClient } from "effect/unstable/http"
10+
import { Effect, Layer } from "effect"
11+
import { Agent as AgentSvc } from "../../src/agent/agent"
12+
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
13+
import { Bus } from "../../src/bus"
14+
import { Command } from "../../src/command"
15+
import { Config } from "../../src/config"
16+
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
17+
import { Env } from "../../src/env"
18+
import { Format } from "../../src/format"
19+
import { Instruction } from "../../src/session/instruction"
20+
import { LLM } from "../../src/session/llm"
21+
import { LSP } from "../../src/lsp"
22+
import { MCP } from "../../src/mcp"
23+
import { Permission } from "../../src/permission"
24+
import { Plugin } from "../../src/plugin"
25+
import { Provider as ProviderSvc } from "../../src/provider"
26+
import { Question } from "../../src/question"
27+
import { Ripgrep } from "../../src/file/ripgrep"
28+
import { Session } from "../../src/session"
29+
import { SessionCompaction } from "../../src/session/compaction"
30+
import { SessionProcessor } from "../../src/session/processor"
31+
import { SessionPrompt } from "../../src/session/prompt"
32+
import { SessionRevert } from "../../src/session/revert"
33+
import { SessionRunState } from "../../src/session/run-state"
34+
import { SessionStatus } from "../../src/session/status"
35+
import { SessionSummary } from "../../src/session/summary"
36+
import { Skill } from "../../src/skill"
37+
import { Snapshot } from "../../src/snapshot"
38+
import { SystemPrompt } from "../../src/session/system"
39+
import { Todo } from "../../src/session/todo"
40+
import { ToolRegistry, Truncate } from "../../src/tool"
41+
import { TestLLMServer } from "./llm-server"
42+
43+
const summaryStub = Layer.succeed(
44+
SessionSummary.Service,
45+
SessionSummary.Service.of({
46+
summarize: () => Effect.void,
47+
diff: () => Effect.succeed([]),
48+
computeDiff: () => Effect.succeed([]),
49+
}),
50+
)
51+
52+
const mcpStub = Layer.succeed(
53+
MCP.Service,
54+
MCP.Service.of({
55+
status: () => Effect.succeed({}),
56+
clients: () => Effect.succeed({}),
57+
tools: () => Effect.succeed({}),
58+
prompts: () => Effect.succeed({}),
59+
resources: () => Effect.succeed({}),
60+
add: () => Effect.succeed({ status: { status: "disabled" as const } }),
61+
connect: () => Effect.void,
62+
disconnect: () => Effect.void,
63+
getPrompt: () => Effect.succeed(undefined),
64+
readResource: () => Effect.succeed(undefined),
65+
startAuth: () => Effect.die("unexpected MCP auth in prompt tests"),
66+
authenticate: () => Effect.die("unexpected MCP auth in prompt tests"),
67+
finishAuth: () => Effect.die("unexpected MCP auth in prompt tests"),
68+
removeAuth: () => Effect.void,
69+
supportsOAuth: () => Effect.succeed(false),
70+
hasStoredTokens: () => Effect.succeed(false),
71+
getAuthStatus: () => Effect.succeed("not_authenticated" as const),
72+
}),
73+
)
74+
75+
const lspStub = Layer.succeed(
76+
LSP.Service,
77+
LSP.Service.of({
78+
init: () => Effect.void,
79+
status: () => Effect.succeed([]),
80+
hasClients: () => Effect.succeed(false),
81+
touchFile: () => Effect.void,
82+
diagnostics: () => Effect.succeed({}),
83+
hover: () => Effect.succeed(undefined),
84+
definition: () => Effect.succeed([]),
85+
references: () => Effect.succeed([]),
86+
implementation: () => Effect.succeed([]),
87+
documentSymbol: () => Effect.succeed([]),
88+
workspaceSymbol: () => Effect.succeed([]),
89+
prepareCallHierarchy: () => Effect.succeed([]),
90+
incomingCalls: () => Effect.succeed([]),
91+
outgoingCalls: () => Effect.succeed([]),
92+
}),
93+
)
94+
95+
const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer))
96+
const runState = SessionRunState.layer.pipe(Layer.provide(status))
97+
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
98+
99+
export function makePromptLayer() {
100+
const deps = Layer.mergeAll(
101+
Session.defaultLayer,
102+
Snapshot.defaultLayer,
103+
LLM.defaultLayer,
104+
Env.defaultLayer,
105+
AgentSvc.defaultLayer,
106+
Command.defaultLayer,
107+
Permission.defaultLayer,
108+
Plugin.defaultLayer,
109+
Config.defaultLayer,
110+
ProviderSvc.defaultLayer,
111+
lspStub,
112+
mcpStub,
113+
AppFileSystem.defaultLayer,
114+
status,
115+
).pipe(Layer.provideMerge(infra))
116+
const question = Question.layer.pipe(Layer.provideMerge(deps))
117+
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
118+
const registry = ToolRegistry.layer.pipe(
119+
Layer.provide(Skill.defaultLayer),
120+
Layer.provide(FetchHttpClient.layer),
121+
Layer.provide(CrossSpawnSpawner.defaultLayer),
122+
Layer.provide(Ripgrep.defaultLayer),
123+
Layer.provide(Format.defaultLayer),
124+
Layer.provideMerge(todo),
125+
Layer.provideMerge(question),
126+
Layer.provideMerge(deps),
127+
)
128+
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
129+
const proc = SessionProcessor.layer.pipe(Layer.provide(summaryStub), Layer.provideMerge(deps))
130+
const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps))
131+
return Layer.mergeAll(
132+
TestLLMServer.layer,
133+
SessionPrompt.layer.pipe(
134+
Layer.provide(SessionRevert.defaultLayer),
135+
Layer.provide(summaryStub),
136+
Layer.provideMerge(runState),
137+
Layer.provideMerge(compact),
138+
Layer.provideMerge(proc),
139+
Layer.provideMerge(registry),
140+
Layer.provideMerge(trunc),
141+
Layer.provide(Instruction.defaultLayer),
142+
Layer.provide(SystemPrompt.defaultLayer),
143+
Layer.provideMerge(deps),
144+
),
145+
).pipe(Layer.provide(summaryStub))
146+
}

packages/opencode/test/session/prompt.test.ts

Lines changed: 4 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,25 @@
1-
import { NodeFileSystem } from "@effect/platform-node"
2-
import { FetchHttpClient } from "effect/unstable/http"
31
import { expect } from "bun:test"
4-
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
2+
import { Cause, Effect, Exit, Fiber } from "effect"
53
import path from "path"
64
import { fileURLToPath } from "url"
75
import { NamedError } from "@opencode-ai/shared/util/error"
8-
import { Agent as AgentSvc } from "../../src/agent/agent"
9-
import { Bus } from "../../src/bus"
10-
import { Command } from "../../src/command"
11-
import { Config } from "../../src/config"
12-
import { LSP } from "../../src/lsp"
13-
import { MCP } from "../../src/mcp"
14-
import { Permission } from "../../src/permission"
15-
import { Plugin } from "../../src/plugin"
16-
import { Provider as ProviderSvc } from "../../src/provider"
17-
import { Env } from "../../src/env"
186
import { ModelID, ProviderID } from "../../src/provider/schema"
19-
import { Question } from "../../src/question"
20-
import { Todo } from "../../src/session/todo"
217
import { Session } from "../../src/session"
22-
import { LLM } from "../../src/session/llm"
238
import { MessageV2 } from "../../src/session/message-v2"
24-
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
25-
import { SessionCompaction } from "../../src/session/compaction"
26-
import { SessionSummary } from "../../src/session/summary"
27-
import { Instruction } from "../../src/session/instruction"
28-
import { SessionProcessor } from "../../src/session/processor"
299
import { SessionPrompt } from "../../src/session/prompt"
30-
import { SessionRevert } from "../../src/session/revert"
3110
import { SessionRunState } from "../../src/session/run-state"
3211
import { MessageID, PartID, SessionID } from "../../src/session/schema"
3312
import { SessionStatus } from "../../src/session/status"
34-
import { Skill } from "../../src/skill"
35-
import { SystemPrompt } from "../../src/session/system"
3613
import { Shell } from "../../src/shell/shell"
37-
import { Snapshot } from "../../src/snapshot"
3814
import { ToolRegistry } from "../../src/tool"
39-
import { Truncate } from "../../src/tool"
4015
import { Log } from "../../src/util"
41-
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
42-
import { Ripgrep } from "../../src/file/ripgrep"
43-
import { Format } from "../../src/format"
4416
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
4517
import { testEffect } from "../lib/effect"
46-
import { reply, TestLLMServer } from "../lib/llm-server"
18+
import { reply } from "../lib/llm-server"
19+
import { makePromptLayer } from "../lib/prompt-harness"
4720

4821
void Log.init({ print: false })
4922

50-
const summary = Layer.succeed(
51-
SessionSummary.Service,
52-
SessionSummary.Service.of({
53-
summarize: () => Effect.void,
54-
diff: () => Effect.succeed([]),
55-
computeDiff: () => Effect.succeed([]),
56-
}),
57-
)
58-
5923
const ref = {
6024
providerID: ProviderID.make("test"),
6125
modelID: ModelID.make("test-model"),
@@ -106,102 +70,7 @@ function errorTool(parts: MessageV2.Part[]) {
10670
return part?.state.status === "error" ? (part as ErrorToolPart) : undefined
10771
}
10872

109-
const mcp = Layer.succeed(
110-
MCP.Service,
111-
MCP.Service.of({
112-
status: () => Effect.succeed({}),
113-
clients: () => Effect.succeed({}),
114-
tools: () => Effect.succeed({}),
115-
prompts: () => Effect.succeed({}),
116-
resources: () => Effect.succeed({}),
117-
add: () => Effect.succeed({ status: { status: "disabled" as const } }),
118-
connect: () => Effect.void,
119-
disconnect: () => Effect.void,
120-
getPrompt: () => Effect.succeed(undefined),
121-
readResource: () => Effect.succeed(undefined),
122-
startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
123-
authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
124-
finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
125-
removeAuth: () => Effect.void,
126-
supportsOAuth: () => Effect.succeed(false),
127-
hasStoredTokens: () => Effect.succeed(false),
128-
getAuthStatus: () => Effect.succeed("not_authenticated" as const),
129-
}),
130-
)
131-
132-
const lsp = Layer.succeed(
133-
LSP.Service,
134-
LSP.Service.of({
135-
init: () => Effect.void,
136-
status: () => Effect.succeed([]),
137-
hasClients: () => Effect.succeed(false),
138-
touchFile: () => Effect.void,
139-
diagnostics: () => Effect.succeed({}),
140-
hover: () => Effect.succeed(undefined),
141-
definition: () => Effect.succeed([]),
142-
references: () => Effect.succeed([]),
143-
implementation: () => Effect.succeed([]),
144-
documentSymbol: () => Effect.succeed([]),
145-
workspaceSymbol: () => Effect.succeed([]),
146-
prepareCallHierarchy: () => Effect.succeed([]),
147-
incomingCalls: () => Effect.succeed([]),
148-
outgoingCalls: () => Effect.succeed([]),
149-
}),
150-
)
151-
152-
const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer))
153-
const run = SessionRunState.layer.pipe(Layer.provide(status))
154-
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
155-
function makeHttp() {
156-
const deps = Layer.mergeAll(
157-
Session.defaultLayer,
158-
Snapshot.defaultLayer,
159-
LLM.defaultLayer,
160-
Env.defaultLayer,
161-
AgentSvc.defaultLayer,
162-
Command.defaultLayer,
163-
Permission.defaultLayer,
164-
Plugin.defaultLayer,
165-
Config.defaultLayer,
166-
ProviderSvc.defaultLayer,
167-
lsp,
168-
mcp,
169-
AppFileSystem.defaultLayer,
170-
status,
171-
).pipe(Layer.provideMerge(infra))
172-
const question = Question.layer.pipe(Layer.provideMerge(deps))
173-
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
174-
const registry = ToolRegistry.layer.pipe(
175-
Layer.provide(Skill.defaultLayer),
176-
Layer.provide(FetchHttpClient.layer),
177-
Layer.provide(CrossSpawnSpawner.defaultLayer),
178-
Layer.provide(Ripgrep.defaultLayer),
179-
Layer.provide(Format.defaultLayer),
180-
Layer.provideMerge(todo),
181-
Layer.provideMerge(question),
182-
Layer.provideMerge(deps),
183-
)
184-
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
185-
const proc = SessionProcessor.layer.pipe(Layer.provide(summary), Layer.provideMerge(deps))
186-
const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps))
187-
return Layer.mergeAll(
188-
TestLLMServer.layer,
189-
SessionPrompt.layer.pipe(
190-
Layer.provide(SessionRevert.defaultLayer),
191-
Layer.provide(summary),
192-
Layer.provideMerge(run),
193-
Layer.provideMerge(compact),
194-
Layer.provideMerge(proc),
195-
Layer.provideMerge(registry),
196-
Layer.provideMerge(trunc),
197-
Layer.provide(Instruction.defaultLayer),
198-
Layer.provide(SystemPrompt.defaultLayer),
199-
Layer.provideMerge(deps),
200-
),
201-
).pipe(Layer.provide(summary))
202-
}
203-
204-
const it = testEffect(makeHttp())
73+
const it = testEffect(makePromptLayer())
20574
const unix = process.platform !== "win32" ? it.live : it.live.skip
20675

20776
// Config that registers a custom "test" provider with a "test-model" model

0 commit comments

Comments
 (0)