| ... | ... | @@ -1877,6 +1877,142 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { |
| 1877 | 1877 | }), |
| 1878 | 1878 | ); |
| 1879 | 1879 | |
| 1880 | it.effect("clears stale pending user input from projected shell summaries", () => |
| 1881 | Effect.gen(function* () { |
| 1882 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |
| 1883 | const eventStore = yield* OrchestrationEventStore; |
| 1884 | const sql = yield* SqlClient.SqlClient; |
| 1885 | const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) => |
| 1886 | eventStore |
| 1887 | .append(event) |
| 1888 | .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); |
| 1889 | |
| 1890 | yield* appendAndProject({ |
| 1891 | type: "project.created", |
| 1892 | eventId: EventId.make("evt-stale-user-input-1"), |
| 1893 | aggregateKind: "project", |
| 1894 | aggregateId: ProjectId.make("project-stale-user-input"), |
| 1895 | occurredAt: "2026-02-26T12:35:00.000Z", |
| 1896 | commandId: CommandId.make("cmd-stale-user-input-1"), |
| 1897 | causationEventId: null, |
| 1898 | correlationId: CorrelationId.make("cmd-stale-user-input-1"), |
| 1899 | metadata: {}, |
| 1900 | payload: { |
| 1901 | projectId: ProjectId.make("project-stale-user-input"), |
| 1902 | title: "Project Stale User Input", |
| 1903 | workspaceRoot: "/tmp/project-stale-user-input", |
| 1904 | defaultModelSelection: null, |
| 1905 | scripts: [], |
| 1906 | createdAt: "2026-02-26T12:35:00.000Z", |
| 1907 | updatedAt: "2026-02-26T12:35:00.000Z", |
| 1908 | }, |
| 1909 | }); |
| 1910 | |
| 1911 | yield* appendAndProject({ |
| 1912 | type: "thread.created", |
| 1913 | eventId: EventId.make("evt-stale-user-input-2"), |
| 1914 | aggregateKind: "thread", |
| 1915 | aggregateId: ThreadId.make("thread-stale-user-input"), |
| 1916 | occurredAt: "2026-02-26T12:35:01.000Z", |
| 1917 | commandId: CommandId.make("cmd-stale-user-input-2"), |
| 1918 | causationEventId: null, |
| 1919 | correlationId: CorrelationId.make("cmd-stale-user-input-2"), |
| 1920 | metadata: {}, |
| 1921 | payload: { |
| 1922 | threadId: ThreadId.make("thread-stale-user-input"), |
| 1923 | projectId: ProjectId.make("project-stale-user-input"), |
| 1924 | title: "Thread Stale User Input", |
| 1925 | modelSelection: { |
| 1926 | instanceId: ProviderInstanceId.make("codex"), |
| 1927 | model: "gpt-5-codex", |
| 1928 | }, |
| 1929 | runtimeMode: "approval-required", |
| 1930 | interactionMode: "default", |
| 1931 | branch: null, |
| 1932 | worktreePath: null, |
| 1933 | createdAt: "2026-02-26T12:35:01.000Z", |
| 1934 | updatedAt: "2026-02-26T12:35:01.000Z", |
| 1935 | }, |
| 1936 | }); |
| 1937 | |
| 1938 | yield* appendAndProject({ |
| 1939 | type: "thread.activity-appended", |
| 1940 | eventId: EventId.make("evt-stale-user-input-3"), |
| 1941 | aggregateKind: "thread", |
| 1942 | aggregateId: ThreadId.make("thread-stale-user-input"), |
| 1943 | occurredAt: "2026-02-26T12:35:02.000Z", |
| 1944 | commandId: CommandId.make("cmd-stale-user-input-3"), |
| 1945 | causationEventId: null, |
| 1946 | correlationId: CorrelationId.make("cmd-stale-user-input-3"), |
| 1947 | metadata: {}, |
| 1948 | payload: { |
| 1949 | threadId: ThreadId.make("thread-stale-user-input"), |
| 1950 | activity: { |
| 1951 | id: EventId.make("activity-stale-user-input-requested"), |
| 1952 | tone: "info", |
| 1953 | kind: "user-input.requested", |
| 1954 | summary: "User input requested", |
| 1955 | payload: { |
| 1956 | requestId: "user-input-request-stale-1", |
| 1957 | questions: [ |
| 1958 | { |
| 1959 | id: "sandbox_mode", |
| 1960 | header: "Sandbox", |
| 1961 | question: "Which mode should be used?", |
| 1962 | options: [ |
| 1963 | { |
| 1964 | label: "workspace-write", |
| 1965 | description: "Allow workspace writes only", |
| 1966 | }, |
| 1967 | ], |
| 1968 | }, |
| 1969 | ], |
| 1970 | }, |
| 1971 | turnId: null, |
| 1972 | createdAt: "2026-02-26T12:35:02.000Z", |
| 1973 | }, |
| 1974 | }, |
| 1975 | }); |
| 1976 | |
| 1977 | yield* appendAndProject({ |
| 1978 | type: "thread.activity-appended", |
| 1979 | eventId: EventId.make("evt-stale-user-input-4"), |
| 1980 | aggregateKind: "thread", |
| 1981 | aggregateId: ThreadId.make("thread-stale-user-input"), |
| 1982 | occurredAt: "2026-02-26T12:35:03.000Z", |
| 1983 | commandId: CommandId.make("cmd-stale-user-input-4"), |
| 1984 | causationEventId: null, |
| 1985 | correlationId: CorrelationId.make("cmd-stale-user-input-4"), |
| 1986 | metadata: {}, |
| 1987 | payload: { |
| 1988 | threadId: ThreadId.make("thread-stale-user-input"), |
| 1989 | activity: { |
| 1990 | id: EventId.make("activity-stale-user-input-failed"), |
| 1991 | tone: "error", |
| 1992 | kind: "provider.user-input.respond.failed", |
| 1993 | summary: "Provider user input response failed", |
| 1994 | payload: { |
| 1995 | requestId: "user-input-request-stale-1", |
| 1996 | detail: |
| 1997 | "Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: user-input-request-stale-1", |
| 1998 | }, |
| 1999 | turnId: null, |
| 2000 | createdAt: "2026-02-26T12:35:03.000Z", |
| 2001 | }, |
| 2002 | }, |
| 2003 | }); |
| 2004 | |
| 2005 | const threadRows = yield* sql<{ |
| 2006 | readonly pendingUserInputCount: number; |
| 2007 | }>` |
| 2008 | SELECT pending_user_input_count AS "pendingUserInputCount" |
| 2009 | FROM projection_threads |
| 2010 | WHERE thread_id = 'thread-stale-user-input' |
| 2011 | `; |
| 2012 | assert.deepEqual(threadRows, [{ pendingUserInputCount: 0 }]); |
| 2013 | }), |
| 2014 | ); |
| 2015 | |
| 1880 | 2016 | it.effect("ignores non-stale provider approval response failures", () => |
| 1881 | 2017 | Effect.gen(function* () { |
| 1882 | 2018 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |