feat: 品牌替换 + 启动优化 + AGENTS.md 模板定制
- 品牌替换:OpenCode/opencode → AirCoding/aircoding(16+ 文件) - Logo ASCII art:修复 left/right 行数不匹配导致的启动崩溃 - 启动诊断:添加 OPENCODE_PRINT_TIMING 计时探针 - dev 模式默认 --pure 跳过外部插件加载 - AGENTS.md 模板:追加 AirCoding 多 Agent 专项段落 - architect prompt + plugin:强化 AGENTS.md 产出验证
This commit is contained in:
115
packages/core/test/question.test.ts
Normal file
115
packages/core/test/question.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const database = Database.layerFromPath(":memory:")
|
||||
const events = EventV2.layer.pipe(Layer.provide(database))
|
||||
const questions = QuestionV2.layer.pipe(Layer.provide(events))
|
||||
const it = testEffect(Layer.mergeAll(database, events, questions))
|
||||
|
||||
const sessionID = SessionV2.ID.make("ses_question_test")
|
||||
const question: QuestionV2.Info = {
|
||||
question: "Which option?",
|
||||
header: "Option",
|
||||
options: [{ label: "One", description: "First option" }],
|
||||
}
|
||||
|
||||
const waitForAsk = Effect.fn("QuestionV2Test.waitForAsk")(function* (
|
||||
service: QuestionV2.Interface,
|
||||
input: QuestionV2.AskInput,
|
||||
) {
|
||||
const events = yield* EventV2.Service
|
||||
const asked = yield* Deferred.make<QuestionV2.Request>()
|
||||
const unsubscribe = yield* events.listen((event) =>
|
||||
event.type === QuestionV2.Event.Asked.type
|
||||
? Deferred.succeed(asked, event.data as QuestionV2.Request).pipe(Effect.asVoid)
|
||||
: Effect.void,
|
||||
)
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
const fiber = yield* service.ask(input).pipe(Effect.forkScoped)
|
||||
return { fiber, request: yield* Deferred.await(asked) }
|
||||
})
|
||||
|
||||
describe("QuestionV2", () => {
|
||||
it.effect("publishes lifecycle events and settles a pending reply", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* QuestionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const published: EventV2.Payload[] = []
|
||||
const unsubscribe = yield* events.listen((event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.type.startsWith("question.v2.")) published.push(event)
|
||||
}),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
const { fiber, request } = yield* waitForAsk(service, { sessionID, questions: [question] })
|
||||
|
||||
expect(request.id).toMatch(/^que_/)
|
||||
expect(yield* service.list()).toEqual([request])
|
||||
yield* service.reply({ requestID: request.id, answers: [["One"]] })
|
||||
|
||||
expect(yield* Fiber.join(fiber)).toEqual([["One"]])
|
||||
expect(yield* service.list()).toEqual([])
|
||||
expect(published.map((event) => [event.type, event.data])).toEqual([
|
||||
[QuestionV2.Event.Asked.type, request],
|
||||
[QuestionV2.Event.Replied.type, { sessionID, requestID: request.id, answers: [["One"]] }],
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("publishes rejection, fails the ask, and rejects unknown IDs", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* QuestionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const published: EventV2.Payload[] = []
|
||||
const unsubscribe = yield* events.listen((event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.type === QuestionV2.Event.Rejected.type) published.push(event)
|
||||
}),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
const { fiber, request } = yield* waitForAsk(service, { sessionID, questions: [question] })
|
||||
|
||||
yield* service.reject(request.id)
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError")
|
||||
expect(published.map((event) => event.data)).toEqual([{ sessionID, requestID: request.id }])
|
||||
|
||||
const unknown = QuestionV2.ID.ascending("que_unknown")
|
||||
expect(yield* service.reply({ requestID: unknown, answers: [] }).pipe(Effect.flip)).toEqual(
|
||||
new QuestionV2.NotFoundError({ requestID: unknown }),
|
||||
)
|
||||
expect(yield* service.reject(unknown).pipe(Effect.flip)).toEqual(
|
||||
new QuestionV2.NotFoundError({ requestID: unknown }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("isolates pending requests by location-layer instance and rejects them on finalization", () =>
|
||||
Effect.gen(function* () {
|
||||
const firstScope = yield* Scope.make()
|
||||
const secondScope = yield* Scope.make()
|
||||
const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), firstScope), QuestionV2.Service)
|
||||
const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), QuestionV2.Service)
|
||||
const fiber = yield* first.ask({ sessionID, questions: [question] }).pipe(Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
const request = (yield* first.list())[0]!
|
||||
|
||||
expect(yield* second.list()).toEqual([])
|
||||
expect(yield* second.reply({ requestID: request.id, answers: [["One"]] }).pipe(Effect.flip)).toEqual(
|
||||
new QuestionV2.NotFoundError({ requestID: request.id }),
|
||||
)
|
||||
|
||||
yield* Scope.close(firstScope, Exit.void)
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError")
|
||||
yield* Scope.close(secondScope, Exit.void)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user