| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- import { afterEach, test, expect } from "bun:test"
- import { Question } from "../../src/question"
- import { Instance } from "../../src/project/instance"
- import { QuestionID } from "../../src/question/schema"
- import { tmpdir } from "../fixture/fixture"
- import { SessionID } from "../../src/session/schema"
- afterEach(async () => {
- await Instance.disposeAll()
- })
- /** Reject all pending questions so dangling Deferred fibers don't hang the test. */
- async function rejectAll() {
- const pending = await Question.list()
- for (const req of pending) {
- await Question.reject(req.id)
- }
- }
- test("ask - returns pending promise", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const promise = Question.ask({
- sessionID: SessionID.make("ses_test"),
- questions: [
- {
- question: "What would you like to do?",
- header: "Action",
- options: [
- { label: "Option 1", description: "First option" },
- { label: "Option 2", description: "Second option" },
- ],
- },
- ],
- })
- expect(promise).toBeInstanceOf(Promise)
- await rejectAll()
- await promise.catch(() => {})
- },
- })
- })
- test("ask - adds to pending list", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const questions = [
- {
- question: "What would you like to do?",
- header: "Action",
- options: [
- { label: "Option 1", description: "First option" },
- { label: "Option 2", description: "Second option" },
- ],
- },
- ]
- const askPromise = Question.ask({
- sessionID: SessionID.make("ses_test"),
- questions,
- })
- const pending = await Question.list()
- expect(pending.length).toBe(1)
- expect(pending[0].questions).toEqual(questions)
- await rejectAll()
- await askPromise.catch(() => {})
- },
- })
- })
- // reply tests
- test("reply - resolves the pending ask with answers", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const questions = [
- {
- question: "What would you like to do?",
- header: "Action",
- options: [
- { label: "Option 1", description: "First option" },
- { label: "Option 2", description: "Second option" },
- ],
- },
- ]
- const askPromise = Question.ask({
- sessionID: SessionID.make("ses_test"),
- questions,
- })
- const pending = await Question.list()
- const requestID = pending[0].id
- await Question.reply({
- requestID,
- answers: [["Option 1"]],
- })
- const answers = await askPromise
- expect(answers).toEqual([["Option 1"]])
- },
- })
- })
- test("reply - removes from pending list", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const askPromise = Question.ask({
- sessionID: SessionID.make("ses_test"),
- questions: [
- {
- question: "What would you like to do?",
- header: "Action",
- options: [
- { label: "Option 1", description: "First option" },
- { label: "Option 2", description: "Second option" },
- ],
- },
- ],
- })
- const pending = await Question.list()
- expect(pending.length).toBe(1)
- await Question.reply({
- requestID: pending[0].id,
- answers: [["Option 1"]],
- })
- await askPromise
- const pendingAfter = await Question.list()
- expect(pendingAfter.length).toBe(0)
- },
- })
- })
- test("reply - does nothing for unknown requestID", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- await Question.reply({
- requestID: QuestionID.make("que_unknown"),
- answers: [["Option 1"]],
- })
- // Should not throw
- },
- })
- })
- // reject tests
- test("reject - throws RejectedError", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const askPromise = Question.ask({
- sessionID: SessionID.make("ses_test"),
- questions: [
- {
- question: "What would you like to do?",
- header: "Action",
- options: [
- { label: "Option 1", description: "First option" },
- { label: "Option 2", description: "Second option" },
- ],
- },
- ],
- })
- const pending = await Question.list()
- await Question.reject(pending[0].id)
- await expect(askPromise).rejects.toBeInstanceOf(Question.RejectedError)
- },
- })
- })
- test("reject - removes from pending list", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const askPromise = Question.ask({
- sessionID: SessionID.make("ses_test"),
- questions: [
- {
- question: "What would you like to do?",
- header: "Action",
- options: [
- { label: "Option 1", description: "First option" },
- { label: "Option 2", description: "Second option" },
- ],
- },
- ],
- })
- const pending = await Question.list()
- expect(pending.length).toBe(1)
- await Question.reject(pending[0].id)
- askPromise.catch(() => {}) // Ignore rejection
- const pendingAfter = await Question.list()
- expect(pendingAfter.length).toBe(0)
- },
- })
- })
- test("reject - does nothing for unknown requestID", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- await Question.reject(QuestionID.make("que_unknown"))
- // Should not throw
- },
- })
- })
- // multiple questions tests
- test("ask - handles multiple questions", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const questions = [
- {
- question: "What would you like to do?",
- header: "Action",
- options: [
- { label: "Build", description: "Build the project" },
- { label: "Test", description: "Run tests" },
- ],
- },
- {
- question: "Which environment?",
- header: "Env",
- options: [
- { label: "Dev", description: "Development" },
- { label: "Prod", description: "Production" },
- ],
- },
- ]
- const askPromise = Question.ask({
- sessionID: SessionID.make("ses_test"),
- questions,
- })
- const pending = await Question.list()
- await Question.reply({
- requestID: pending[0].id,
- answers: [["Build"], ["Dev"]],
- })
- const answers = await askPromise
- expect(answers).toEqual([["Build"], ["Dev"]])
- },
- })
- })
- // list tests
- test("list - returns all pending requests", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const p1 = Question.ask({
- sessionID: SessionID.make("ses_test1"),
- questions: [
- {
- question: "Question 1?",
- header: "Q1",
- options: [{ label: "A", description: "A" }],
- },
- ],
- })
- const p2 = Question.ask({
- sessionID: SessionID.make("ses_test2"),
- questions: [
- {
- question: "Question 2?",
- header: "Q2",
- options: [{ label: "B", description: "B" }],
- },
- ],
- })
- const pending = await Question.list()
- expect(pending.length).toBe(2)
- await rejectAll()
- p1.catch(() => {})
- p2.catch(() => {})
- },
- })
- })
- test("list - returns empty when no pending", async () => {
- await using tmp = await tmpdir({ git: true })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const pending = await Question.list()
- expect(pending.length).toBe(0)
- },
- })
- })
- test("questions stay isolated by directory", async () => {
- await using one = await tmpdir({ git: true })
- await using two = await tmpdir({ git: true })
- const p1 = Instance.provide({
- directory: one.path,
- fn: () =>
- Question.ask({
- sessionID: SessionID.make("ses_one"),
- questions: [
- {
- question: "Question 1?",
- header: "Q1",
- options: [{ label: "A", description: "A" }],
- },
- ],
- }),
- })
- const p2 = Instance.provide({
- directory: two.path,
- fn: () =>
- Question.ask({
- sessionID: SessionID.make("ses_two"),
- questions: [
- {
- question: "Question 2?",
- header: "Q2",
- options: [{ label: "B", description: "B" }],
- },
- ],
- }),
- })
- const onePending = await Instance.provide({
- directory: one.path,
- fn: () => Question.list(),
- })
- const twoPending = await Instance.provide({
- directory: two.path,
- fn: () => Question.list(),
- })
- expect(onePending.length).toBe(1)
- expect(twoPending.length).toBe(1)
- expect(onePending[0].sessionID).toBe(SessionID.make("ses_one"))
- expect(twoPending[0].sessionID).toBe(SessionID.make("ses_two"))
- await Instance.provide({
- directory: one.path,
- fn: () => Question.reject(onePending[0].id),
- })
- await Instance.provide({
- directory: two.path,
- fn: () => Question.reject(twoPending[0].id),
- })
- await p1.catch(() => {})
- await p2.catch(() => {})
- })
- test("pending question rejects on instance dispose", async () => {
- await using tmp = await tmpdir({ git: true })
- const ask = Instance.provide({
- directory: tmp.path,
- fn: () => {
- return Question.ask({
- sessionID: SessionID.make("ses_dispose"),
- questions: [
- {
- question: "Dispose me?",
- header: "Dispose",
- options: [{ label: "Yes", description: "Yes" }],
- },
- ],
- })
- },
- })
- const result = ask.then(
- () => "resolved" as const,
- (err) => err,
- )
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const pending = await Question.list()
- expect(pending).toHaveLength(1)
- await Instance.dispose()
- },
- })
- expect(await result).toBeInstanceOf(Question.RejectedError)
- })
- test("pending question rejects on instance reload", async () => {
- await using tmp = await tmpdir({ git: true })
- const ask = Instance.provide({
- directory: tmp.path,
- fn: () => {
- return Question.ask({
- sessionID: SessionID.make("ses_reload"),
- questions: [
- {
- question: "Reload me?",
- header: "Reload",
- options: [{ label: "Yes", description: "Yes" }],
- },
- ],
- })
- },
- })
- const result = ask.then(
- () => "resolved" as const,
- (err) => err,
- )
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const pending = await Question.list()
- expect(pending).toHaveLength(1)
- await Instance.reload({ directory: tmp.path })
- },
- })
- expect(await result).toBeInstanceOf(Question.RejectedError)
- })
|