| ... | @@ -1877,6 +1877,213 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { | ... | @@ -1877,6 +1877,213 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 1877 | }), | 1877 | }), |
| 1878 | ); | 1878 | ); |
| 1879 | | 1879 | |
| | 1880 | it.effect("names the edited files in an apply-patch approval without a reason", () => |
| | 1881 | Effect.gen(function* () { |
| | 1882 | const { adapter, runtime } = yield* startLifecycleRuntime(); |
| | 1883 | const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); |
| | 1884 | |
| | 1885 | yield* runtime.emit({ |
| | 1886 | id: asEventId("evt-apply-patch"), |
| | 1887 | kind: "request", |
| | 1888 | provider: ProviderDriverKind.make("codex"), |
| | 1889 | threadId: asThreadId("thread-1"), |
| | 1890 | createdAt: "2026-08-24T00:00:00.000Z", |
| | 1891 | method: "applyPatchApproval", |
| | 1892 | requestKind: "file-change", |
| | 1893 | requestId: ApprovalRequestId.make("req-patch"), |
| | 1894 | turnId: asTurnId("turn-1"), |
| | 1895 | payload: { |
| | 1896 | callId: "call-1", |
| | 1897 | conversationId: "provider-thread-1", |
| | 1898 | fileChanges: { |
| | 1899 | "/tmp/removed.md": { type: "delete", content: "gone" }, |
| | 1900 | "/tmp/added.ts": { type: "add", content: "export {};" }, |
| | 1901 | }, |
| | 1902 | }, |
| | 1903 | } satisfies ProviderEvent); |
| | 1904 | |
| | 1905 | const firstEvent = yield* Fiber.join(firstEventFiber); |
| | 1906 | |
| | 1907 | NodeAssert.equal(firstEvent._tag, "Some"); |
| | 1908 | if (firstEvent._tag !== "Some" || firstEvent.value.type !== "request.opened") { |
| | 1909 | return; |
| | 1910 | } |
| | 1911 | NodeAssert.equal(firstEvent.value.payload.requestType, "apply_patch_approval"); |
| | 1912 | NodeAssert.equal( |
| | 1913 | firstEvent.value.payload.detail, |
| | 1914 | "add /tmp/added.ts\ndelete /tmp/removed.md", |
| | 1915 | ); |
| | 1916 | }), |
| | 1917 | ); |
| | 1918 | |
| | 1919 | it.effect("keeps the reason when an apply-patch approval carries one", () => |
| | 1920 | Effect.gen(function* () { |
| | 1921 | const { adapter, runtime } = yield* startLifecycleRuntime(); |
| | 1922 | const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); |
| | 1923 | |
| | 1924 | yield* runtime.emit({ |
| | 1925 | id: asEventId("evt-apply-patch-reason"), |
| | 1926 | kind: "request", |
| | 1927 | provider: ProviderDriverKind.make("codex"), |
| | 1928 | threadId: asThreadId("thread-1"), |
| | 1929 | createdAt: "2026-08-24T00:00:00.000Z", |
| | 1930 | method: "applyPatchApproval", |
| | 1931 | requestKind: "file-change", |
| | 1932 | requestId: ApprovalRequestId.make("req-patch-reason"), |
| | 1933 | turnId: asTurnId("turn-1"), |
| | 1934 | payload: { |
| | 1935 | callId: "call-2", |
| | 1936 | conversationId: "provider-thread-1", |
| | 1937 | reason: "Needs to rewrite the changelog", |
| | 1938 | fileChanges: { "/tmp/CHANGELOG.md": { type: "add", content: "x" } }, |
| | 1939 | }, |
| | 1940 | } satisfies ProviderEvent); |
| | 1941 | |
| | 1942 | const firstEvent = yield* Fiber.join(firstEventFiber); |
| | 1943 | |
| | 1944 | NodeAssert.equal(firstEvent._tag, "Some"); |
| | 1945 | if (firstEvent._tag !== "Some" || firstEvent.value.type !== "request.opened") { |
| | 1946 | return; |
| | 1947 | } |
| | 1948 | NodeAssert.equal(firstEvent.value.payload.detail, "Needs to rewrite the changelog"); |
| | 1949 | }), |
| | 1950 | ); |
| | 1951 | |
| | 1952 | it.effect("falls back to the grant root for a file-change approval without a reason", () => |
| | 1953 | Effect.gen(function* () { |
| | 1954 | const { adapter, runtime } = yield* startLifecycleRuntime(); |
| | 1955 | const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); |
| | 1956 | |
| | 1957 | yield* runtime.emit({ |
| | 1958 | id: asEventId("evt-file-change"), |
| | 1959 | kind: "request", |
| | 1960 | provider: ProviderDriverKind.make("codex"), |
| | 1961 | threadId: asThreadId("thread-1"), |
| | 1962 | createdAt: "2026-08-24T00:00:00.000Z", |
| | 1963 | method: "item/fileChange/requestApproval", |
| | 1964 | requestKind: "file-change", |
| | 1965 | requestId: ApprovalRequestId.make("req-file-change"), |
| | 1966 | turnId: asTurnId("turn-1"), |
| | 1967 | payload: { |
| | 1968 | itemId: "item-1", |
| | 1969 | grantRoot: "/tmp/workspace", |
| | 1970 | startedAtMs: 0, |
| | 1971 | threadId: "provider-thread-1", |
| | 1972 | turnId: "turn-1", |
| | 1973 | }, |
| | 1974 | } satisfies ProviderEvent); |
| | 1975 | |
| | 1976 | const firstEvent = yield* Fiber.join(firstEventFiber); |
| | 1977 | |
| | 1978 | NodeAssert.equal(firstEvent._tag, "Some"); |
| | 1979 | if (firstEvent._tag !== "Some" || firstEvent.value.type !== "request.opened") { |
| | 1980 | return; |
| | 1981 | } |
| | 1982 | NodeAssert.equal(firstEvent.value.payload.requestType, "file_change_approval"); |
| | 1983 | NodeAssert.equal(firstEvent.value.payload.detail, "/tmp/workspace"); |
| | 1984 | }), |
| | 1985 | ); |
| | 1986 | |
| | 1987 | it.effect("prefers the edited files over a blank apply-patch reason", () => |
| | 1988 | Effect.gen(function* () { |
| | 1989 | const { adapter, runtime } = yield* startLifecycleRuntime(); |
| | 1990 | const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); |
| | 1991 | |
| | 1992 | yield* runtime.emit({ |
| | 1993 | id: asEventId("evt-apply-patch-blank"), |
| | 1994 | kind: "request", |
| | 1995 | provider: ProviderDriverKind.make("codex"), |
| | 1996 | threadId: asThreadId("thread-1"), |
| | 1997 | createdAt: "2026-08-24T00:00:00.000Z", |
| | 1998 | method: "applyPatchApproval", |
| | 1999 | requestKind: "file-change", |
| | 2000 | requestId: ApprovalRequestId.make("req-patch-blank"), |
| | 2001 | turnId: asTurnId("turn-1"), |
| | 2002 | payload: { |
| | 2003 | callId: "call-3", |
| | 2004 | conversationId: "provider-thread-1", |
| | 2005 | reason: " ", |
| | 2006 | fileChanges: { |
| | 2007 | "/tmp/moved.ts": { type: "update", unified_diff: "@@", move_path: "/tmp/renamed.ts" }, |
| | 2008 | }, |
| | 2009 | }, |
| | 2010 | } satisfies ProviderEvent); |
| | 2011 | |
| | 2012 | const firstEvent = yield* Fiber.join(firstEventFiber); |
| | 2013 | |
| | 2014 | NodeAssert.equal(firstEvent._tag, "Some"); |
| | 2015 | if (firstEvent._tag !== "Some" || firstEvent.value.type !== "request.opened") { |
| | 2016 | return; |
| | 2017 | } |
| | 2018 | NodeAssert.equal(firstEvent.value.payload.detail, "update /tmp/moved.ts -> /tmp/renamed.ts"); |
| | 2019 | }), |
| | 2020 | ); |
| | 2021 | |
| | 2022 | it.effect("caps the described files in an oversized apply-patch approval", () => |
| | 2023 | Effect.gen(function* () { |
| | 2024 | const { adapter, runtime } = yield* startLifecycleRuntime(); |
| | 2025 | const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); |
| | 2026 | |
| | 2027 | const fileChanges = Object.fromEntries( |
| | 2028 | Array.from({ length: 25 }, (_unused, index) => [ |
| | 2029 | `/tmp/file-${String(index).padStart(2, "0")}.ts`, |
| | 2030 | { type: "add", content: "x" }, |
| | 2031 | ]), |
| | 2032 | ); |
| | 2033 | |
| | 2034 | yield* runtime.emit({ |
| | 2035 | id: asEventId("evt-apply-patch-many"), |
| | 2036 | kind: "request", |
| | 2037 | provider: ProviderDriverKind.make("codex"), |
| | 2038 | threadId: asThreadId("thread-1"), |
| | 2039 | createdAt: "2026-08-24T00:00:00.000Z", |
| | 2040 | method: "applyPatchApproval", |
| | 2041 | requestKind: "file-change", |
| | 2042 | requestId: ApprovalRequestId.make("req-patch-many"), |
| | 2043 | turnId: asTurnId("turn-1"), |
| | 2044 | payload: { callId: "call-4", conversationId: "provider-thread-1", fileChanges }, |
| | 2045 | } satisfies ProviderEvent); |
| | 2046 | |
| | 2047 | const firstEvent = yield* Fiber.join(firstEventFiber); |
| | 2048 | |
| | 2049 | NodeAssert.equal(firstEvent._tag, "Some"); |
| | 2050 | if (firstEvent._tag !== "Some" || firstEvent.value.type !== "request.opened") { |
| | 2051 | return; |
| | 2052 | } |
| | 2053 | const detail = firstEvent.value.payload.detail ?? ""; |
| | 2054 | NodeAssert.equal(detail.split("\n").length, 21); |
| | 2055 | NodeAssert.ok(detail.endsWith("+5 more")); |
| | 2056 | }), |
| | 2057 | ); |
| | 2058 | |
| | 2059 | it.effect("leaves an apply-patch approval without changes or a reason undetailed", () => |
| | 2060 | Effect.gen(function* () { |
| | 2061 | const { adapter, runtime } = yield* startLifecycleRuntime(); |
| | 2062 | const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); |
| | 2063 | |
| | 2064 | yield* runtime.emit({ |
| | 2065 | id: asEventId("evt-apply-patch-empty"), |
| | 2066 | kind: "request", |
| | 2067 | provider: ProviderDriverKind.make("codex"), |
| | 2068 | threadId: asThreadId("thread-1"), |
| | 2069 | createdAt: "2026-08-24T00:00:00.000Z", |
| | 2070 | method: "applyPatchApproval", |
| | 2071 | requestKind: "file-change", |
| | 2072 | requestId: ApprovalRequestId.make("req-patch-empty"), |
| | 2073 | turnId: asTurnId("turn-1"), |
| | 2074 | payload: { callId: "call-5", conversationId: "provider-thread-1", fileChanges: {} }, |
| | 2075 | } satisfies ProviderEvent); |
| | 2076 | |
| | 2077 | const firstEvent = yield* Fiber.join(firstEventFiber); |
| | 2078 | |
| | 2079 | NodeAssert.equal(firstEvent._tag, "Some"); |
| | 2080 | if (firstEvent._tag !== "Some" || firstEvent.value.type !== "request.opened") { |
| | 2081 | return; |
| | 2082 | } |
| | 2083 | NodeAssert.equal(firstEvent.value.payload.detail, undefined); |
| | 2084 | }), |
| | 2085 | ); |
| | 2086 | |
| 1880 | it.effect("maps MCP elicitation requests into app access approvals", () => | 2087 | it.effect("maps MCP elicitation requests into app access approvals", () => |
| 1881 | Effect.gen(function* () { | 2088 | Effect.gen(function* () { |
| 1882 | const { adapter, runtime } = yield* startLifecycleRuntime(); | 2089 | const { adapter, runtime } = yield* startLifecycleRuntime(); |