Skip to content

Commit d97ef16

Browse files
committed
Restore desktop parity and preserve interrupted chat progress
1 parent 2db2fdc commit d97ef16

28 files changed

Lines changed: 1028 additions & 53 deletions

File tree

‎.github/workflows/test-build.yml‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,151 @@ permissions:
88
contents: read
99

1010
jobs:
11+
oauth-postgres:
12+
name: PostgreSQL integration (${{ matrix.provision }})
13+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}
14+
timeout-minutes: 15
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
provision: [push, migrate]
19+
services:
20+
postgres:
21+
image: pgvector/pgvector:pg17
22+
env:
23+
POSTGRES_USER: postgres
24+
POSTGRES_PASSWORD: postgres
25+
POSTGRES_DB: sim_auth_scim
26+
ports:
27+
- 5432:5432
28+
options: >-
29+
--health-cmd "pg_isready -U postgres -d sim_auth_scim"
30+
--health-interval 5s
31+
--health-timeout 5s
32+
--health-retries 10
33+
env:
34+
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
35+
OAUTH_TOKEN_FAMILY_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
36+
BETTER_AUTH_SECRET: oauth-postgres-ci-secret-at-least-32-characters
37+
NEXT_PUBLIC_APP_URL: https://test.sim.ai
38+
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
39+
40+
steps:
41+
- name: Checkout code
42+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
43+
44+
- name: Setup Bun
45+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
46+
with:
47+
bun-version: 1.4.1
48+
49+
- name: Setup Node
50+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
51+
with:
52+
node-version: 24
53+
54+
- name: Mount Bun cache
55+
uses: ./.github/actions/cache-mount
56+
with:
57+
provider: ${{ vars.CI_PROVIDER }}
58+
key: ${{ github.repository }}-bun-cache-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
59+
path: ~/.bun/install/cache
60+
61+
- name: Install dependencies
62+
run: bun install --frozen-lockfile --ignore-scripts
63+
64+
- name: Provision a fresh database through the supported command
65+
working-directory: packages/db
66+
run: |
67+
bun -e 'import postgres from "postgres"; const sql = postgres(process.env.DATABASE_URL); for (const extension of ["vector", "btree_gin", "pg_trgm"]) await sql`CREATE EXTENSION IF NOT EXISTS ${sql(extension)}`; await sql.end()'
68+
bun run db:${{ matrix.provision }}
69+
70+
- name: Verify migration replay is a no-op
71+
if: matrix.provision == 'migrate'
72+
working-directory: packages/db
73+
run: bun run db:migrate
74+
75+
- name: Verify OAuth lifecycle and SCIM membership guards in PostgreSQL
76+
working-directory: apps/sim
77+
run: >-
78+
bunx vitest run
79+
lib/auth/oauth-token-family.postgres.test.ts
80+
lib/auth/oauth-provider-lifecycle.postgres.test.ts
81+
app/api/auth/oauth2/token/route.postgres.test.ts
82+
lib/auth/sim-auth-adapter.test.ts
83+
ee/scim/lib/managed-membership.postgres.test.ts
84+
lib/auth/sso/application/admit-sso-user.postgres.test.ts
85+
86+
- name: Verify SCIM and administration over real HTTP
87+
working-directory: apps/sim
88+
env:
89+
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3017
90+
BETTER_AUTH_URL: http://127.0.0.1:3017
91+
NEXT_PUBLIC_FORCE_HOSTED: 'true'
92+
BILLING_ENABLED: 'true'
93+
NEXT_PUBLIC_BILLING_ENABLED: 'true'
94+
ENTERPRISE_ENABLED: 'true'
95+
NEXT_PUBLIC_ENTERPRISE_ENABLED: 'true'
96+
SCIM_ENABLED: 'true'
97+
NEXT_PUBLIC_SCIM_ENABLED: 'true'
98+
SSO_ENABLED: 'true'
99+
NEXT_PUBLIC_SSO_ENABLED: 'true'
100+
ORGANIZATIONS_ENABLED: 'true'
101+
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: 'true'
102+
INTERNAL_API_SECRET: scim-http-ci-local-secret-at-least-32-characters
103+
DB_TX_TRIPWIRE: throw
104+
DISABLE_TELEMETRY: 'true'
105+
NEXT_TELEMETRY_DISABLED: '1'
106+
NEXT_PUBLIC_CHAT_DISABLED: 'true'
107+
run: |
108+
server_log="$RUNNER_TEMP/scim-next.log"
109+
node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 --port 3017 > "$server_log" 2>&1 &
110+
server_pid=$!
111+
finish() {
112+
kill "$server_pid" 2>/dev/null || true
113+
wait "$server_pid" 2>/dev/null || true
114+
awk '/^ (GET|POST|PUT|PATCH|DELETE|HEAD) \/api\// { print }' "$server_log" > "$RUNNER_TEMP/scim-http-status.log"
115+
}
116+
trap finish EXIT
117+
deadline=$((SECONDS + 120))
118+
until curl --fail --silent --max-time 3 http://127.0.0.1:3017/api/health > /dev/null; do
119+
if ! kill -0 "$server_pid" 2>/dev/null; then
120+
echo 'Local SCIM app exited during startup.'
121+
exit 1
122+
fi
123+
if [ "$SECONDS" -ge "$deadline" ]; then
124+
echo 'Local SCIM app did not become ready within 120 seconds.'
125+
exit 1
126+
fi
127+
sleep 2
128+
done
129+
SCIM_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
130+
SCIM_E2E_DATABASE_URL="$DATABASE_URL" \
131+
SCIM_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
132+
SCIM_E2E_REPORT_PATH="$RUNNER_TEMP/scim-e2e-report.json" \
133+
bun run test:scim:e2e
134+
135+
- name: Upload SCIM failure report and HTTP status log
136+
if: failure()
137+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
138+
with:
139+
name: scim-failure-${{ matrix.provision }}
140+
path: |
141+
${{ runner.temp }}/scim-e2e-report.json
142+
${{ runner.temp }}/scim-http-status.log
143+
if-no-files-found: ignore
144+
retention-days: 7
145+
146+
- name: Verify durable provenance bindings and concurrent memory writes
147+
working-directory: apps/sim
148+
env:
149+
TABLE_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
150+
MEMORY_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
151+
run: >-
152+
bunx vitest run
153+
lib/table/rows/secret-provenance.postgres.test.ts
154+
lib/memory/message-provenance.postgres.test.ts
155+
11156
test-build:
12157
name: Lint and Test
13158
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}

‎apps/desktop/e2e/browser-tools.spec.ts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const FORM = `<!doctype html><html><head><title>Form fixture</title></head><body
2929
test.describe('browser tools', () => {
3030
const calls = new Map<
3131
string,
32-
{ chatId: string; toolName: BrowserToolName; args: Record<string, unknown> }
32+
{ chatId: string; toolName: BrowserToolName | 'terminal'; args: Record<string, unknown> }
3333
>()
3434
let server: Server
3535
let origin: string
@@ -44,6 +44,7 @@ test.describe('browser tools', () => {
4444
let body = ''
4545
for await (const chunk of request) body += chunk.toString()
4646
const authorization = calls.get(JSON.parse(body).toolCallId)
47+
calls.delete(JSON.parse(body).toolCallId)
4748
response.writeHead(authorization ? 200 : 403, { 'Content-Type': 'application/json' })
4849
response.end(JSON.stringify(authorization ?? {}))
4950
return
@@ -241,4 +242,37 @@ test.describe('browser tools', () => {
241242
expect(fill.result).toMatchObject({ completed: false, completedCount: 0 })
242243
expect(await formState()).toMatchObject({ name: '', password: '' })
243244
})
245+
test('local terminal executes through its native PTY and refuses repeated authorization', async () => {
246+
await window.evaluate(async (scope) => {
247+
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
248+
await api.terminal.activateScope(scope)
249+
await api.terminal.start({ cols: 100, rows: 30 }, scope)
250+
}, SCOPE)
251+
calls.set('local-cwd', {
252+
chatId: SCOPE,
253+
toolName: 'terminal',
254+
args: { operation: 'cwd', args: {} },
255+
})
256+
const cwd = await window.evaluate(async (scope) => {
257+
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
258+
return api.terminal.executeTool('local-cwd', 'cwd', {}, scope)
259+
}, SCOPE)
260+
expect(cwd.ok).toBe(true)
261+
calls.set('local-run', {
262+
chatId: SCOPE,
263+
toolName: 'terminal',
264+
args: { operation: 'run', args: { command: "printf 'SIM_NATIVE_TERMINAL_VERIFIED\\n'" } },
265+
})
266+
const result = await window.evaluate(async (scope) => {
267+
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
268+
return api.terminal.executeTool('local-run', 'run', {}, scope)
269+
}, SCOPE)
270+
expect(result.ok).toBe(true)
271+
expect(JSON.stringify(result)).toContain('SIM_NATIVE_TERMINAL_VERIFIED')
272+
const replay = await window.evaluate(async (scope) => {
273+
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
274+
return api.terminal.executeTool('local-run', 'run', {}, scope)
275+
}, SCOPE)
276+
expect(replay.ok).toBe(false)
277+
})
244278
})

‎apps/desktop/src/main/browser-agent/session.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,6 +2512,26 @@ describe('browser-agent session', () => {
25122512
expect(session.listTabs()).toHaveLength(13)
25132513
})
25142514

2515+
it('gives background automation a viewport without taking panel ownership', () => {
2516+
const tab = session.withBrowserScope('background-chat', () => session.ensureTab())
2517+
2518+
expect(tab.view.setBounds).toHaveBeenCalledWith({
2519+
x: 0,
2520+
y: 0,
2521+
width: 1180,
2522+
height: 850,
2523+
})
2524+
expect(win.contentView.addChildView).not.toHaveBeenCalledWith(tab.view)
2525+
expect(session.getActiveBrowserScopeId()).toBe('chat-test')
2526+
})
2527+
2528+
it('initializes a detached viewport when no application window exists', () => {
2529+
const headlessSession = freshSession(null)
2530+
const tab = headlessSession.ensureTab()
2531+
2532+
expect(tab.view.setBounds).toHaveBeenCalledWith({ x: 0, y: 0, width: 1280, height: 720 })
2533+
})
2534+
25152535
it('embeds the active view in the MAIN window only while panel bounds are reported', () => {
25162536
const tab = session.ensureTab()
25172537
const view = tab.view as unknown as MockView

‎apps/desktop/src/main/browser-agent/session.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2217,6 +2217,9 @@ function createTabView(): WebContentsView {
22172217
},
22182218
})
22192219
try {
2220+
/** Detached tabs must lay out before a foreground panel owns their native view. */
2221+
const [width, height] = getMainWindow()?.getContentSize() ?? [1280, 720]
2222+
view.setBounds({ x: 0, y: 0, width: Math.max(1, width), height: Math.max(1, height) })
22202223
return initializeTabView(view, scopeId)
22212224
} catch (error) {
22222225
if (!view.webContents.isDestroyed()) view.webContents.close()

‎apps/sim/app/api/copilot/chat/stop/route.test.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ import { authMockFns, dbChainMockFns, resetDbChainMock } from '@sim/testing'
55
import { NextRequest } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
77

8-
const { mockAppendCopilotChatMessages, mockPublishStatusChanged } = vi.hoisted(() => ({
9-
mockAppendCopilotChatMessages: vi.fn(),
10-
mockPublishStatusChanged: vi.fn(),
11-
}))
8+
const { mockAppendCopilotChatMessages, mockPublishStatusChanged, mockReadEvents } = vi.hoisted(
9+
() => ({
10+
mockAppendCopilotChatMessages: vi.fn(),
11+
mockPublishStatusChanged: vi.fn(),
12+
mockReadEvents: vi.fn(),
13+
})
14+
)
15+
16+
vi.mock('@/lib/mothership/request/session/buffer', () => ({ readEvents: mockReadEvents }))
1217

1318
vi.mock('@/lib/mothership/chat/messages-store', () => ({
1419
appendCopilotChatMessages: mockAppendCopilotChatMessages,
@@ -52,6 +57,7 @@ describe('copilot chat stop route', () => {
5257
dbChainMockFns.limit.mockReset()
5358
resetDbChainMock()
5459
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
60+
mockReadEvents.mockResolvedValue([])
5561
})
5662

5763
it('preserves task and subagent identity through the partial-response contract', async () => {
@@ -133,6 +139,7 @@ describe('copilot chat stop route', () => {
133139
createRequest({ chatId: 'chat-1', streamId: 'stream-1', content: '' })
134140
)
135141

142+
expect(mockReadEvents).toHaveBeenCalledWith('stream-1', '0')
136143
expect(response.status).toBe(200)
137144
expect(await response.json()).toEqual({ success: true })
138145

‎apps/sim/app/api/copilot/chat/stop/route.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const POST = withRouteHandler((req: NextRequest) =>
7272
userMessageId: streamId,
7373
assistantMessage,
7474
streamMarkerPolicy: 'active-or-cleared',
75+
preferServerReplay: true,
7576
})
7677
span.setAttribute(TraceAttr.CopilotStopAppendedAssistant, result.appendedAssistant)
7778
const stopOutcome = !result.found

0 commit comments

Comments
 (0)