Skip to content

Commit 7adf342

Browse files
mzxchandraclaude
andcommitted
Merge origin/staging into feat/fork-sync-opt-in
Three conflicts, resolved as follows. `application/revision.ts` - staging narrowed `lockForkRevision` from `DbOrTx` to `DbTransaction`; this branch added the rank-5 TSDoc. Both wanted, so the resolution keeps staging's signature and this branch's documentation. The migration collided: staging shipped its own 0385 and has since reached 0390. Rather than hand-editing the generated snapshot, dropped this branch's 0385, took staging's migration files verbatim, and regenerated from the merged schema. Drizzle emitted the identical single `ADD COLUMN`, now at 0391, renamed to the repo's descriptive convention with its migration-safe rationale restored. One follow-on the merge did not flag but the refactor implies: staging moved the fork advisory locks onto `acquireAdvisoryXactLock`, which adds caller attribution for query insights and, more importantly, takes a `DbTransaction` because on a pooled connection the lock statement autocommits and releases before the caller's work runs. `acquireForkLineageLock` was still hand-rolling the raw statement against `DbOrTx`, so it moved onto the shared helper too and its lock-order test followed the narrowed type. Verified on the merged tree: 52 audits, type-check, lint, 2980 unit tests, and the lock-order integration suite, whose pre-fix control still deadlocks at Postgres's 1s detector through the refactored helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 parents 439319b + 07c3ffe commit 7adf342

694 files changed

Lines changed: 209845 additions & 10985 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/memory-load-check/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Read these when doing a deeper pass:
4545
- dispatch concrete chunks (`workspaceIds`, retention, label) instead of one giant scope
4646
- prefer Trigger.dev queue/concurrency keys when available
4747
- execute inline fallback chunks sequentially, not with unbounded `Promise.all`
48-
- File parse pattern in `apps/sim/lib/internal/file/parser.ts` and `apps/sim/lib/uploads/contexts/workspace/fetch-external-url.ts`
48+
- File parse pattern in `apps/sim/lib/internal/file/parser.ts` and `apps/sim/lib/uploads/utils/fetch-external-url.server.ts`
4949
- cap downloads and parsed output separately
5050
- preserve partial results when a later item exceeds the cap
5151
- never read untrusted response bodies without a byte cap

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ jobs:
129129
if-no-files-found: warn
130130
retention-days: 14
131131

132+
- name: Verify Google document reads over real HTTP
133+
if: matrix.provision == 'push'
134+
working-directory: apps/sim
135+
env:
136+
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3040
137+
NEXT_PUBLIC_FORCE_HOSTED: 'false'
138+
SEARCH_GOOGLE_CONTENT_REPORT_PATH: ${{ runner.temp }}/search-google-content.json
139+
run: bun scripts/test-search-google-content-e2e.ts
140+
141+
- name: Upload Google content acceptance report
142+
if: failure() && matrix.provision == 'push'
143+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
144+
with:
145+
name: search-google-content
146+
path: ${{ runner.temp }}/search-google-content.json
147+
if-no-files-found: ignore
148+
retention-days: 7
149+
132150
- name: Verify SCIM and administration over real HTTP
133151
working-directory: apps/sim
134152
env:
@@ -359,7 +377,7 @@ jobs:
359377
- name: Install ripgrep
360378
run: command -v rg || (sudo apt-get update && sudo apt-get install -y ripgrep)
361379

362-
# Runs the setup CLI's Bun tests plus each workspace's Vitest suite,
380+
# Runs the root scripts and each workspace's Vitest suite,
363381
# without `--coverage`.
364382
- name: Run tests
365383
env:

‎apps/desktop/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ Raw local file bytes are never exposed through the preload bridge and cannot be
194194

195195
- `electron-updater` reads the deployment's `/api/desktop/update` feed; production resolves stable releases from `simstudioai/sim`, while dev/staging resolve prereleases from `simstudioai/sim-desktop-releases`. Artifact downloads go directly to GitHub and deltas use `.zip.blockmap`. Sim validates every candidate before starting its download. Developer ID builds installed under `/Applications` use a prompt (Restart and update / Later; Later installs on quit); other packaged builds offer a validated installer download — never forced mid-session. A staged or offered update keeps being re-checked on the normal cadence, and a newer release replaces it, so a shell left running across several releases installs the latest build in one restart instead of the stale one followed by another prompt.
196196
- Streams: production follows stable `X.Y.Z` releases, dev follows `-dev.N`, and staging follows `-staging.N`. The feed still recognizes legacy `-alpha.N`/`-beta.N` releases during migration.
197+
- Restart becomes available only after Squirrel confirms native staging. Replacing a staged update returns the UI to downloading; a failed transfer can retain the previous staged build, but a failure after the native feed is replaced requires a retry. Diagnostics record `update_downloaded` after native staging, `update_install` when an explicit restart is committed, and `update_install_result` on the next launch with the expected and installed versions. The staging checkpoint also covers updates installed on a normal quit.
198+
- `bun run test:e2e e2e/updater.spec.ts` exercises the real Electron process, MacUpdater, downloads, retries, and installation checkpoints. Native verification and bundle replacement are simulated. Set `DESKTOP_UPDATER_REPORT_PATH` to choose the JSON report path; by default it is included in Playwright's test results and uploaded by CI on failure.
197199
- Staged rollout: after publishing, edit `stagingPercentage: 10` into the release's `latest-mac.yml`, then raise as crash metrics stay clean.
198200
- Rollback: a pulled release must be superseded by a **higher** version — users on the broken build will not reinstall an equal one. (A blocked-versions kill-switch was removed as unwired dead code; reintroduce it in `updater.ts` if a remote config source ever exists to feed it.)
199201
- Ship the DMG and tell users to install to `/Applications` — App Translocation breaks Squirrel.Mac updates from quarantined paths.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { writeFileSync } from 'node:fs'
2+
import { homedir } from 'node:os'
3+
import { join, relative } from 'node:path'
4+
import type { DesktopUpdateState } from '@sim/desktop-bridge'
5+
import { app, autoUpdater as nativeUpdater, net } from 'electron'
6+
import { MacUpdater } from 'electron-updater'
7+
import { initUpdater } from '@/main/updater'
8+
9+
declare global {
10+
var desktopUpdaterFixture: {
11+
check(): void
12+
install(): void
13+
finishStaging(): void
14+
failStaging(): void
15+
read(): {
16+
state: DesktopUpdateState
17+
nativeArchive: string | null
18+
installed: string[]
19+
events: { name: string; data: unknown }[]
20+
}
21+
}
22+
}
23+
24+
const directory = process.env.SIM_UPDATER_FIXTURE_DIR
25+
const origin = process.env.SIM_UPDATER_FIXTURE_ORIGIN
26+
if (!directory || !origin) throw new Error('Updater fixture configuration is missing')
27+
app.setPath('userData', join(directory, 'user-data'))
28+
29+
void app.whenReady().then(() => {
30+
const configPath = join(directory, 'updater.yml')
31+
const cache = relative(join(homedir(), 'Library', 'Caches'), join(directory, 'cache'))
32+
writeFileSync(configPath, `updaterCacheDirName: ${JSON.stringify(cache)}\n`)
33+
34+
let feed: Electron.FeedURLOptions | undefined
35+
let nativeArchive: string | null = null
36+
const installed: string[] = []
37+
const events: { name: string; data: unknown }[] = []
38+
39+
/** Native verification and installation are simulated; MacUpdater and both HTTP transfers run. */
40+
nativeUpdater.setFeedURL = (options) => {
41+
feed = options
42+
nativeArchive = null
43+
}
44+
nativeUpdater.checkForUpdates = () => {
45+
void (async () => {
46+
if (!feed) throw new Error('Native feed was not configured')
47+
const response = await net.fetch(feed.url, { headers: feed.headers })
48+
const manifest: { url: string } = await response.json()
49+
const archive = await net.fetch(manifest.url)
50+
nativeArchive = await archive.text()
51+
})().catch((error) => nativeUpdater.emit('error', error))
52+
}
53+
nativeUpdater.quitAndInstall = () => {
54+
if (nativeArchive === null) throw new Error('Cannot install before native staging')
55+
installed.push(nativeArchive)
56+
}
57+
58+
const updater = new MacUpdater()
59+
updater.forceDevUpdateConfig = true
60+
updater.disableDifferentialDownload = true
61+
updater.updateConfigPath = configPath
62+
updater.setFeedURL({ provider: 'generic', url: origin })
63+
const handle = initUpdater({
64+
getWindow: () => null,
65+
events: { filePath: '', record: (name, data) => events.push({ name, data }) },
66+
appOrigin: () => origin,
67+
loadAutoUpdater: () => updater,
68+
canSelfUpdate: async () => true,
69+
probeOriginFeed: async () => false,
70+
installStatePath: join(directory, 'update-install.json'),
71+
})
72+
73+
globalThis.desktopUpdaterFixture = {
74+
check: () => handle.check(),
75+
install: () => handle.install(),
76+
finishStaging: () => {
77+
if (nativeArchive === null) throw new Error('Native transfer has not finished')
78+
nativeUpdater.emit('update-downloaded', {}, '', nativeArchive, new Date(), '')
79+
},
80+
failStaging: () => nativeUpdater.emit('error', new Error('Native verification failed')),
81+
read: () => ({ state: handle.getState(), nativeArchive, installed, events }),
82+
}
83+
})
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { mkdtempSync, rmSync } from 'node:fs'
2+
import { createServer, type Server } from 'node:http'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { fileURLToPath } from 'node:url'
6+
import { type ElectronApplication, _electron as electron, expect, test } from '@playwright/test'
7+
import type { SimDesktopApi } from '@sim/desktop-bridge'
8+
9+
const DESKTOP_DIR = fileURLToPath(new URL('..', import.meta.url))
10+
const SCOPE = 'session-cookies-fixture'
11+
12+
/**
13+
* Electron drops every expiry-less cookie on quit, so a site that keeps its
14+
* login in a session cookie signed the user out of the built-in browser on
15+
* every restart. This drives a real quit and relaunch against one profile.
16+
*/
17+
test.describe('built-in browser session cookies', () => {
18+
let server: Server
19+
let origin: string
20+
let site: string
21+
let userData: string
22+
let app: ElectronApplication | undefined
23+
let serial = 0
24+
let lastCookieHeader: string | undefined
25+
const calls = new Map<
26+
string,
27+
{ chatId: string; toolName: string; args: Record<string, unknown> }
28+
>()
29+
30+
test.beforeAll(async () => {
31+
server = createServer(async (request, response) => {
32+
const path = new URL(request.url ?? '/', 'http://localhost').pathname
33+
if (path === '/api/auth/get-session') {
34+
response.writeHead(200, { 'Content-Type': 'application/json' })
35+
response.end(
36+
JSON.stringify({ user: { id: 'fixture-user' }, session: { id: 'fixture-session' } })
37+
)
38+
return
39+
}
40+
if (path === '/api/desktop/tool/authorize') {
41+
let body = ''
42+
for await (const chunk of request) body += chunk.toString()
43+
const call = calls.get(JSON.parse(body).toolCallId)
44+
response.writeHead(call ? 200 : 403, { 'Content-Type': 'application/json' })
45+
response.end(JSON.stringify(call ?? {}))
46+
return
47+
}
48+
if (path.startsWith('/api/')) {
49+
response.writeHead(200, { 'Content-Type': 'application/json' })
50+
response.end('{}')
51+
return
52+
}
53+
if (path === '/sign-in') {
54+
// A login redirect that sets a short-lived cookie the next hop deletes.
55+
response.writeHead(302, {
56+
'Set-Cookie': 'oauth_state=pending; HttpOnly; Path=/',
57+
Location: '/signed-in',
58+
})
59+
response.end()
60+
return
61+
}
62+
if (path === '/signed-in') {
63+
response.writeHead(200, {
64+
'Content-Type': 'text/html',
65+
'Set-Cookie': [
66+
'oauth_state=; Path=/; Max-Age=0',
67+
'login=fixture; HttpOnly; SameSite=Lax; Path=/',
68+
'remember=1; Path=/; Max-Age=3600',
69+
],
70+
})
71+
response.end('<!doctype html><title>Signed in</title>')
72+
return
73+
}
74+
if (path === '/account') {
75+
lastCookieHeader = request.headers.cookie ?? ''
76+
response.writeHead(200, { 'Content-Type': 'text/html' })
77+
response.end('<!doctype html><title>Account</title>')
78+
return
79+
}
80+
if (request.headers.host?.startsWith('localhost')) {
81+
// Favicon and other stray site requests must not set the app session cookie on the site.
82+
response.writeHead(404)
83+
response.end()
84+
return
85+
}
86+
response.writeHead(200, {
87+
'Content-Type': 'text/html',
88+
'Set-Cookie': 'better-auth.session_token=fixture; HttpOnly; SameSite=Lax; Path=/',
89+
})
90+
response.end('<!doctype html><title>Sim fixture</title><h1>Session cookie fixture</h1>')
91+
})
92+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
93+
const address = server.address()
94+
if (!address || typeof address === 'string') throw new Error('Missing fixture address')
95+
origin = `http://127.0.0.1:${address.port}`
96+
/** Pages outside the app origin browse in the built-in browser's own partition. */
97+
site = `http://localhost:${address.port}`
98+
})
99+
100+
test.beforeEach(() => {
101+
userData = mkdtempSync(join(tmpdir(), 'sim-session-cookies-e2e-'))
102+
})
103+
104+
test.afterEach(async () => {
105+
await app?.close()
106+
app = undefined
107+
rmSync(userData, { recursive: true, force: true })
108+
calls.clear()
109+
})
110+
111+
test.afterAll(async () => {
112+
await new Promise<void>((resolve, reject) =>
113+
server.close((error) => (error ? reject(error) : resolve()))
114+
)
115+
})
116+
117+
async function launch(): Promise<ElectronApplication> {
118+
const launched = await electron.launch({
119+
args: [process.env.SIM_DESKTOP_E2E_MAIN ?? '.'],
120+
cwd: DESKTOP_DIR,
121+
env: { ...process.env, SIM_DESKTOP_ORIGIN: origin, SIM_DESKTOP_USER_DATA: userData },
122+
})
123+
const host = await launched.firstWindow()
124+
await expect(host.getByRole('heading')).toHaveText('Session cookie fixture')
125+
await host.evaluate(async (scope) => {
126+
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
127+
await api.browserAgent.activateScope(scope)
128+
const updateBounds = () =>
129+
api.browserAgent.setPanelBounds(
130+
{ x: 0, y: 80, width: innerWidth, height: innerHeight - 80 },
131+
null,
132+
scope
133+
)
134+
updateBounds()
135+
setInterval(updateBounds, 200)
136+
}, SCOPE)
137+
return launched
138+
}
139+
140+
async function navigate(target: ElectronApplication, url: string) {
141+
const id = `fixture-${++serial}`
142+
calls.set(id, { chatId: SCOPE, toolName: 'browser_open_url', args: { url } })
143+
const host = await target.firstWindow()
144+
const result = await host.evaluate(
145+
async ({ id, url, scope }) => {
146+
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
147+
return api.browserAgent.executeTool(id, 'browser_open_url', { url }, scope)
148+
},
149+
{ id, url, scope: SCOPE }
150+
)
151+
expect(result.ok, result.error).toBe(true)
152+
}
153+
154+
async function cookiesSentToSite(target: ElectronApplication): Promise<string[]> {
155+
lastCookieHeader = undefined
156+
await navigate(target, `${site}/account`)
157+
await expect.poll(() => lastCookieHeader).not.toBeUndefined()
158+
return (lastCookieHeader ?? '')
159+
.split(';')
160+
.map((pair) => pair.trim())
161+
.filter(Boolean)
162+
.sort()
163+
}
164+
165+
test('keeps a session-cookie login across a restart without reviving deleted cookies', async () => {
166+
app = await launch()
167+
await navigate(app, `${site}/sign-in`)
168+
expect(await cookiesSentToSite(app)).toEqual(['login=fixture', 'remember=1'])
169+
170+
await app.evaluate(({ session }) =>
171+
session.fromPartition('persist:sim-browser-agent').cookies.flushStore()
172+
)
173+
await app.close()
174+
175+
app = await launch()
176+
expect(await cookiesSentToSite(app)).toEqual(['login=fixture', 'remember=1'])
177+
})
178+
})

0 commit comments

Comments
 (0)