| ... | @@ -48,6 +48,10 @@ import { | ... | @@ -48,6 +48,10 @@ import { |
| 48 | t3McpServerName, | 48 | t3McpServerName, |
| 49 | } from "@t3tools/provider-opencode/testing"; | 49 | } from "@t3tools/provider-opencode/testing"; |
| 50 | import { openCode2ReplayRuntime } from "./OpenCode2AdapterV2.testkit.ts"; | 50 | import { openCode2ReplayRuntime } from "./OpenCode2AdapterV2.testkit.ts"; |
| | 51 | import { |
| | 52 | makeProviderReplayGate, |
| | 53 | type ProviderReplayGate, |
| | 54 | } from "@t3tools/provider-testing/replayGate"; |
| 51 | | 55 | |
| 52 | const SESSION = "ses_f148ca2deffeJcwCnRQtb0YFNX"; | 56 | const SESSION = "ses_f148ca2deffeJcwCnRQtb0YFNX"; |
| 53 | const WORK = "/work/opencode2"; | 57 | const WORK = "/work/opencode2"; |
| ... | @@ -64,12 +68,18 @@ const reply = (operation: string, data: unknown): ProviderReplayEntry => ({ | ... | @@ -64,12 +68,18 @@ const reply = (operation: string, data: unknown): ProviderReplayEntry => ({ |
| 64 | frame: { type: "sdk.response", operation, data }, | 68 | frame: { type: "sdk.response", operation, data }, |
| 65 | }); | 69 | }); |
| 66 | const replyData = (operation: string, data: unknown) => reply(operation, { data }); | 70 | const replyData = (operation: string, data: unknown) => reply(operation, { data }); |
| 67 | const event = (type: string, data: Record<string, unknown>): ProviderReplayEntry => ({ | 71 | const event = ( |
| | 72 | type: string, |
| | 73 | data: Record<string, unknown>, |
| | 74 | /** Holds the event until a test's replay gate releases this label. */ |
| | 75 | label?: string, |
| | 76 | ): ProviderReplayEntry => ({ |
| 68 | type: "emit_inbound", | 77 | type: "emit_inbound", |
| 69 | frame: { | 78 | frame: { |
| 70 | type: "sdk.event", | 79 | type: "sdk.event", |
| 71 | event: { id: `evt_${type.replaceAll(".", "")}0000`, created: 1, type, data, ...durable }, | 80 | event: { id: `evt_${type.replaceAll(".", "")}0000`, created: 1, type, data, ...durable }, |
| 72 | }, | 81 | }, |
| | 82 | ...(label === undefined ? {} : { label }), |
| 73 | }); | 83 | }); |
| 74 | const durable = { durable: { aggregateID: SESSION, seq: 1, version: 1 } }; | 84 | const durable = { durable: { aggregateID: SESSION, seq: 1, version: 1 } }; |
| 75 | | 85 | |
| ... | @@ -316,7 +326,11 @@ const colorForm = { | ... | @@ -316,7 +326,11 @@ const colorForm = { |
| 316 | */ | 326 | */ |
| 317 | const resumed = ( | 327 | const resumed = ( |
| 318 | entries: ReadonlyArray<ProviderReplayEntry>, | 328 | entries: ReadonlyArray<ProviderReplayEntry>, |
| 319 | options?: { readonly external?: boolean; readonly supervised?: boolean }, | 329 | options?: { |
| | 330 | readonly external?: boolean; |
| | 331 | readonly supervised?: boolean; |
| | 332 | readonly replayGate?: ProviderReplayGate; |
| | 333 | }, |
| 320 | ) => | 334 | ) => |
| 321 | Effect.gen(function* () { | 335 | Effect.gen(function* () { |
| 322 | const runtime = yield* openCode2ReplayRuntime( | 336 | const runtime = yield* openCode2ReplayRuntime( |
| ... | @@ -335,7 +349,10 @@ const resumed = ( | ... | @@ -335,7 +349,10 @@ const resumed = ( |
| 335 | : []), | 349 | : []), |
| 336 | ...entries, | 350 | ...entries, |
| 337 | ]), | 351 | ]), |
| 338 | options?.external === undefined ? undefined : { external: options.external }, | 352 | { |
| | 353 | ...(options?.external === undefined ? {} : { external: options.external }), |
| | 354 | ...(options?.replayGate === undefined ? {} : { replayGate: options.replayGate }), |
| | 355 | }, |
| 339 | ); | 356 | ); |
| 340 | const thread = yield* runtime.resumeThread({ | 357 | const thread = yield* runtime.resumeThread({ |
| 341 | providerThread: providerThread(yield* DateTime.now), | 358 | providerThread: providerThread(yield* DateTime.now), |
| ... | @@ -1724,6 +1741,88 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { | ... | @@ -1724,6 +1741,88 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { |
| 1724 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), | 1741 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), |
| 1725 | ); | 1742 | ); |
| 1726 | | 1743 | |
| | 1744 | it.effect("still checks the server when a failed prompt's execution starts late", () => |
| | 1745 | Effect.gen(function* () { |
| | 1746 | const offered = yield* Deferred.make<void>(); |
| | 1747 | const { runtime, thread } = yield* resumed([ |
| | 1748 | out("session.prompt", { sessionID: SESSION, text: "<any>" }), |
| | 1749 | // The request failed, but the server took the prompt: its execution |
| | 1750 | // starts only after the turn already failed. |
| | 1751 | reply("session.prompt", { |
| | 1752 | status: 502, |
| | 1753 | body: { _tag: "UnknownError", message: "bad gateway" }, |
| | 1754 | }), |
| | 1755 | event("session.execution.started", { sessionID: SESSION }), |
| | 1756 | // That run is still going, so the next turn stops it and fails. |
| | 1757 | out("session.active"), |
| | 1758 | reply("session.active", { data: { [SESSION]: { type: "running" } } }), |
| | 1759 | out("session.interrupt", { sessionID: SESSION }), |
| | 1760 | reply("session.interrupt", { interrupted: true }), |
| | 1761 | ]).pipe( |
| | 1762 | Effect.provideService(ProviderContinuationRequests.ProviderContinuationRequests, { |
| | 1763 | offer: () => Deferred.succeed(offered, undefined).pipe(Effect.asVoid), |
| | 1764 | take: Effect.never, |
| | 1765 | }), |
| | 1766 | ); |
| | 1767 | const ended = yield* terminals(runtime, 2); |
| | 1768 | yield* runtime.startTurn(turnInput(thread)).pipe(Effect.ignore); |
| | 1769 | // The late start has been read. |
| | 1770 | yield* Deferred.await(offered); |
| | 1771 | yield* runtime.startTurn(secondTurn(thread)); |
| | 1772 | const [first, second] = yield* Fiber.join(ended); |
| | 1773 | assert.equal(first?.status, "failed"); |
| | 1774 | assert.equal(second?.failure?.message, OPENCODE_2_STILL_STOPPING); |
| | 1775 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), |
| | 1776 | ); |
| | 1777 | |
| | 1778 | it.effect("still checks the server when a failed prompt starts after a reconnect", () => |
| | 1779 | Effect.gen(function* () { |
| | 1780 | const offered = yield* Deferred.make<void>(); |
| | 1781 | const gate = makeProviderReplayGate(["drop"]); |
| | 1782 | const { runtime, thread } = yield* resumed( |
| | 1783 | [ |
| | 1784 | out("session.prompt", { sessionID: SESSION, text: "<any>" }), |
| | 1785 | reply("session.prompt", { |
| | 1786 | status: 502, |
| | 1787 | body: { _tag: "UnknownError", message: "bad gateway" }, |
| | 1788 | }), |
| | 1789 | // The stream drops before the prompt's execution starts, and the |
| | 1790 | // reconnect finds the session active: the prompt's run is still to come. |
| | 1791 | event("session.usage.updated", { sessionID: SESSION }, "drop"), |
| | 1792 | { type: "runtime_exit", status: "success" } as const, |
| | 1793 | out("event.subscribe"), |
| | 1794 | out("session.active"), |
| | 1795 | replyData("session.active", { [SESSION]: { type: "running" } }), |
| | 1796 | out("permission.list", { sessionID: SESSION }), |
| | 1797 | replyData("permission.list", []), |
| | 1798 | out("session.form.list", { sessionID: SESSION }), |
| | 1799 | replyData("session.form.list", []), |
| | 1800 | // Its own start: that run is still going, so the next turn stops it and fails. |
| | 1801 | event("session.execution.started", { sessionID: SESSION }), |
| | 1802 | out("session.active"), |
| | 1803 | reply("session.active", { data: { [SESSION]: { type: "running" } } }), |
| | 1804 | out("session.interrupt", { sessionID: SESSION }), |
| | 1805 | reply("session.interrupt", { interrupted: true }), |
| | 1806 | ], |
| | 1807 | { replayGate: gate }, |
| | 1808 | ).pipe( |
| | 1809 | Effect.provideService(ProviderContinuationRequests.ProviderContinuationRequests, { |
| | 1810 | offer: () => Deferred.succeed(offered, undefined).pipe(Effect.asVoid), |
| | 1811 | take: Effect.never, |
| | 1812 | }), |
| | 1813 | ); |
| | 1814 | const ended = yield* terminals(runtime, 2); |
| | 1815 | yield* runtime.startTurn(turnInput(thread)).pipe(Effect.ignore); |
| | 1816 | gate.release("drop"); |
| | 1817 | // The late start has been read. |
| | 1818 | yield* Deferred.await(offered); |
| | 1819 | yield* runtime.startTurn(secondTurn(thread)); |
| | 1820 | const [first, second] = yield* Fiber.join(ended); |
| | 1821 | assert.equal(first?.status, "failed"); |
| | 1822 | assert.equal(second?.failure?.message, OPENCODE_2_STILL_STOPPING); |
| | 1823 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), |
| | 1824 | ); |
| | 1825 | |
| 1727 | it.effect("prompts again without a check after the server refused a prompt", () => | 1826 | it.effect("prompts again without a check after the server refused a prompt", () => |
| 1728 | Effect.gen(function* () { | 1827 | Effect.gen(function* () { |
| 1729 | const { runtime, thread } = yield* resumed([ | 1828 | const { runtime, thread } = yield* resumed([ |
| ... | @@ -1764,6 +1863,257 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { | ... | @@ -1764,6 +1863,257 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { |
| 1764 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), | 1863 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), |
| 1765 | ); | 1864 | ); |
| 1766 | | 1865 | |
| | 1866 | it.effect("ends a background reply's continuation after a lost stream took a Stop's end", () => |
| | 1867 | Effect.gen(function* () { |
| | 1868 | const offered = yield* Deferred.make<void>(); |
| | 1869 | const gate = makeProviderReplayGate(["drop", "reply"]); |
| | 1870 | const { runtime, thread } = yield* resumed( |
| | 1871 | [ |
| | 1872 | ...backgroundLaunch(CHILD), |
| | 1873 | // Restarting the turn stops only the parent, and the server never answers. |
| | 1874 | out("session.interrupt", { sessionID: SESSION }), |
| | 1875 | reply("session.interrupt", "<hang>"), |
| | 1876 | // The restarted turn finds the parent still stopping and fails. |
| | 1877 | out("session.active"), |
| | 1878 | replyData("session.active", { [SESSION]: { type: "running" } }), |
| | 1879 | out("session.interrupt", { sessionID: SESSION }), |
| | 1880 | reply("session.interrupt", { interrupted: true }), |
| | 1881 | // The stream drops before the parent's end; the reconnect finds it idle. |
| | 1882 | event("session.usage.updated", { sessionID: SESSION }, "drop"), |
| | 1883 | { type: "runtime_exit", status: "success" } as const, |
| | 1884 | out("event.subscribe"), |
| | 1885 | out("session.active"), |
| | 1886 | replyData("session.active", { [CHILD]: { type: "running" } }), |
| | 1887 | out("permission.list", { sessionID: CHILD }), |
| | 1888 | replyData("permission.list", []), |
| | 1889 | out("session.form.list", { sessionID: CHILD }), |
| | 1890 | replyData("session.form.list", []), |
| | 1891 | // The subagent ends and OpenCode answers its report on its own. |
| | 1892 | event("session.execution.succeeded", { sessionID: CHILD }), |
| | 1893 | event("session.inbox.enqueued", { |
| | 1894 | inboxID: "msg_report", |
| | 1895 | sessionID: SESSION, |
| | 1896 | item: { |
| | 1897 | type: "synthetic", |
| | 1898 | payload: { |
| | 1899 | text: `<subagent sessionID="${CHILD}" state="completed" description="Sleep">\nCHILD_OK\n</subagent>`, |
| | 1900 | description: "Sleep", |
| | 1901 | metadata: { |
| | 1902 | source: "subagent", |
| | 1903 | childID: CHILD, |
| | 1904 | agent: "General", |
| | 1905 | state: "completed", |
| | 1906 | }, |
| | 1907 | }, |
| | 1908 | delivery: "steer", |
| | 1909 | }, |
| | 1910 | }), |
| | 1911 | event("session.execution.started", { sessionID: SESSION }), |
| | 1912 | event( |
| | 1913 | "session.text.ended", |
| | 1914 | { |
| | 1915 | sessionID: SESSION, |
| | 1916 | assistantMessageID: "msg_followup", |
| | 1917 | ordinal: 0, |
| | 1918 | text: "CHILD_OK", |
| | 1919 | }, |
| | 1920 | "reply", |
| | 1921 | ), |
| | 1922 | event("session.execution.succeeded", { sessionID: SESSION }), |
| | 1923 | // The subagent's session runs again: its turn marks that the reply's end was handled. |
| | 1924 | event("session.execution.started", { sessionID: CHILD }), |
| | 1925 | ], |
| | 1926 | { replayGate: gate }, |
| | 1927 | ).pipe( |
| | 1928 | Effect.provideService(ProviderContinuationRequests.ProviderContinuationRequests, { |
| | 1929 | offer: () => Deferred.succeed(offered, undefined).pipe(Effect.asVoid), |
| | 1930 | take: Effect.never, |
| | 1931 | }), |
| | 1932 | ); |
| | 1933 | // The single reader of the runtime's events. |
| | 1934 | const attached = yield* Deferred.make<void>(); |
| | 1935 | const collected = yield* runtime.events.pipe( |
| | 1936 | Stream.tap((event) => |
| | 1937 | event.type === "subagent.updated" && event.subagent.childThreadId !== null |
| | 1938 | ? Deferred.succeed(attached, undefined) |
| | 1939 | : Effect.void, |
| | 1940 | ), |
| | 1941 | Stream.takeUntil( |
| | 1942 | (event) => |
| | 1943 | event.type === "provider_turn.updated" && |
| | 1944 | event.providerTurn.nativeTurnRef?.nativeId?.startsWith(`${CHILD}:turn:`) === true, |
| | 1945 | ), |
| | 1946 | Stream.runCollect, |
| | 1947 | Effect.forkScoped, |
| | 1948 | ); |
| | 1949 | yield* runtime.startTurn(withLineage(thread)); |
| | 1950 | yield* Deferred.await(attached); |
| | 1951 | const interrupt = yield* runtime |
| | 1952 | .interruptTurn({ providerThread: thread, providerTurnId: yield* providerTurnId }) |
| | 1953 | .pipe(Effect.forkScoped); |
| | 1954 | yield* TestClock.adjust("11 seconds"); |
| | 1955 | yield* Fiber.join(interrupt); |
| | 1956 | yield* runtime.startTurn({ ...secondTurn(thread), appThread: withLineage(thread).appThread }); |
| | 1957 | gate.release("drop"); |
| | 1958 | yield* Deferred.await(offered); |
| | 1959 | yield* runtime.startTurn({ |
| | 1960 | ...withLineage(thread), |
| | 1961 | runId: RunId.make("run:opencode2-adapter:wake"), |
| | 1962 | runOrdinal: 3, |
| | 1963 | providerTurnOrdinal: 3, |
| | 1964 | attemptId: RunAttemptId.make("attempt:opencode2-adapter:wake"), |
| | 1965 | message: { |
| | 1966 | ...turnInput(thread).message, |
| | 1967 | messageId: MessageId.make("message:opencode2-adapter:wake"), |
| | 1968 | createdBy: "agent" as const, |
| | 1969 | creationSource: "provider" as const, |
| | 1970 | }, |
| | 1971 | }); |
| | 1972 | gate.release("reply"); |
| | 1973 | // The reply's own end finishes its continuation: the lost Stop's end does not take it. |
| | 1974 | assert.deepEqual( |
| | 1975 | [...(yield* Fiber.join(collected))].flatMap((event) => |
| | 1976 | event.type === "turn.terminal" ? [event.status] : [], |
| | 1977 | ), |
| | 1978 | ["interrupted", "failed", "completed"], |
| | 1979 | ); |
| | 1980 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), |
| | 1981 | ); |
| | 1982 | |
| | 1983 | /** |
| | 1984 | * A prompt fails without a clear rejection, OpenCode never took it, and a |
| | 1985 | * background subagent's report then wakes the parent into a reply. With |
| | 1986 | * `endedFirst` that reply ends before its continuation takes it. |
| | 1987 | */ |
| | 1988 | const replyAfterUnclearFailure = (endedFirst: boolean) => |
| | 1989 | Effect.gen(function* () { |
| | 1990 | const offered = yield* Deferred.make<void>(); |
| | 1991 | const gate = makeProviderReplayGate(endedFirst ? ["wake"] : ["wake", "reply"]); |
| | 1992 | const { runtime, thread } = yield* resumed( |
| | 1993 | [ |
| | 1994 | ...backgroundLaunch(CHILD), |
| | 1995 | event("session.execution.succeeded", { sessionID: SESSION }), |
| | 1996 | out("session.update", { sessionID: CHILD, permissions: t3Rules }), |
| | 1997 | reply("session.update", null), |
| | 1998 | out("session.prompt", { sessionID: SESSION, text: "<any>" }), |
| | 1999 | reply("session.prompt", { |
| | 2000 | status: 502, |
| | 2001 | body: { _tag: "UnknownError", message: "bad gateway" }, |
| | 2002 | }), |
| | 2003 | // The subagent ends and OpenCode answers its report on its own. |
| | 2004 | event("session.execution.succeeded", { sessionID: CHILD }, "wake"), |
| | 2005 | event("session.inbox.enqueued", { |
| | 2006 | inboxID: "msg_report", |
| | 2007 | sessionID: SESSION, |
| | 2008 | item: { |
| | 2009 | type: "synthetic", |
| | 2010 | payload: { |
| | 2011 | text: `<subagent sessionID="${CHILD}" state="completed" description="Sleep">\nCHILD_OK\n</subagent>`, |
| | 2012 | description: "Sleep", |
| | 2013 | metadata: { |
| | 2014 | source: "subagent", |
| | 2015 | childID: CHILD, |
| | 2016 | agent: "General", |
| | 2017 | state: "completed", |
| | 2018 | }, |
| | 2019 | }, |
| | 2020 | delivery: "steer", |
| | 2021 | }, |
| | 2022 | }), |
| | 2023 | event("session.execution.started", { sessionID: SESSION }), |
| | 2024 | event( |
| | 2025 | "session.text.ended", |
| | 2026 | { |
| | 2027 | sessionID: SESSION, |
| | 2028 | assistantMessageID: "msg_followup", |
| | 2029 | ordinal: 0, |
| | 2030 | text: "CHILD_OK", |
| | 2031 | }, |
| | 2032 | endedFirst ? undefined : "reply", |
| | 2033 | ), |
| | 2034 | event("session.execution.succeeded", { sessionID: SESSION }), |
| | 2035 | // The subagent's session runs again: a turn of its own, after the reply's end. |
| | 2036 | event("session.execution.started", { sessionID: CHILD }), |
| | 2037 | ], |
| | 2038 | { replayGate: gate }, |
| | 2039 | ).pipe( |
| | 2040 | Effect.provideService(ProviderContinuationRequests.ProviderContinuationRequests, { |
| | 2041 | offer: () => Deferred.succeed(offered, undefined).pipe(Effect.asVoid), |
| | 2042 | take: Effect.never, |
| | 2043 | }), |
| | 2044 | ); |
| | 2045 | // The single reader of the runtime's events. |
| | 2046 | const launched = yield* Deferred.make<void>(); |
| | 2047 | const childRan = yield* Deferred.make<void>(); |
| | 2048 | const ended: Array<ProviderAdapter.ProviderAdapterV2Event> = []; |
| | 2049 | const allEnded = yield* Deferred.make<void>(); |
| | 2050 | yield* runtime.events.pipe( |
| | 2051 | Stream.tap((event) => |
| | 2052 | Effect.gen(function* () { |
| | 2053 | if ( |
| | 2054 | event.type === "provider_turn.updated" && |
| | 2055 | event.providerTurn.nativeTurnRef?.nativeId?.startsWith(`${CHILD}:turn:`) === true |
| | 2056 | ) { |
| | 2057 | yield* Deferred.succeed(childRan, undefined); |
| | 2058 | } |
| | 2059 | if (event.type !== "turn_item.updated" && event.type !== "turn.terminal") return; |
| | 2060 | ended.push(event); |
| | 2061 | if (event.type === "turn.terminal") yield* Deferred.succeed(launched, undefined); |
| | 2062 | if (ended.filter((entry) => entry.type === "turn.terminal").length === 3) { |
| | 2063 | yield* Deferred.succeed(allEnded, undefined); |
| | 2064 | } |
| | 2065 | }), |
| | 2066 | ), |
| | 2067 | Stream.runDrain, |
| | 2068 | Effect.forkScoped, |
| | 2069 | ); |
| | 2070 | yield* runtime.startTurn(withLineage(thread)); |
| | 2071 | yield* Deferred.await(launched); |
| | 2072 | yield* runtime |
| | 2073 | .startTurn({ ...secondTurn(thread), appThread: withLineage(thread).appThread }) |
| | 2074 | .pipe(Effect.ignore); |
| | 2075 | gate.release("wake"); |
| | 2076 | yield* Deferred.await(offered); |
| | 2077 | // Everything up to the subagent's next run has been read: the reply ended. |
| | 2078 | if (endedFirst) yield* Deferred.await(childRan); |
| | 2079 | yield* runtime.startTurn({ |
| | 2080 | ...withLineage(thread), |
| | 2081 | runId: RunId.make("run:opencode2-adapter:wake"), |
| | 2082 | runOrdinal: 3, |
| | 2083 | providerTurnOrdinal: 3, |
| | 2084 | attemptId: RunAttemptId.make("attempt:opencode2-adapter:wake"), |
| | 2085 | message: { |
| | 2086 | ...turnInput(thread).message, |
| | 2087 | messageId: MessageId.make("message:opencode2-adapter:wake"), |
| | 2088 | createdBy: "agent" as const, |
| | 2089 | creationSource: "provider" as const, |
| | 2090 | }, |
| | 2091 | }); |
| | 2092 | if (!endedFirst) gate.release("reply"); |
| | 2093 | // The reply's own end finishes its continuation: the failed prompt's mark does not take it. |
| | 2094 | yield* Deferred.await(allEnded); |
| | 2095 | assert.deepEqual( |
| | 2096 | ended.flatMap((event) => (event.type === "turn.terminal" ? [event.status] : [])), |
| | 2097 | ["completed", "failed", "completed"], |
| | 2098 | ); |
| | 2099 | assert.include( |
| | 2100 | ended.flatMap((event) => |
| | 2101 | event.type === "turn_item.updated" && event.turnItem.type === "assistant_message" |
| | 2102 | ? [event.turnItem.text] |
| | 2103 | : [], |
| | 2104 | ), |
| | 2105 | "CHILD_OK", |
| | 2106 | ); |
| | 2107 | }).pipe(Effect.scoped, Effect.provide(TestClock.layer())); |
| | 2108 | |
| | 2109 | it.effect("ends a background reply's continuation after a prompt failed unclearly", () => |
| | 2110 | replyAfterUnclearFailure(false), |
| | 2111 | ); |
| | 2112 | |
| | 2113 | it.effect("ends a finished background reply's continuation after a prompt failed unclearly", () => |
| | 2114 | replyAfterUnclearFailure(true), |
| | 2115 | ); |
| | 2116 | |
| 1767 | it.effect("does not report a Stop the server says did nothing", () => | 2117 | it.effect("does not report a Stop the server says did nothing", () => |
| 1768 | Effect.gen(function* () { | 2118 | Effect.gen(function* () { |
| 1769 | const { runtime, thread } = yield* resumed([ | 2119 | const { runtime, thread } = yield* resumed([ |