Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion config/oxlint-anti-slop.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@
// mock-descendant-sweep.ts (daemon and relay) is a test-only side-effect shim: its whole body
// is one vi.mock that keeps mock PTY PIDs away from the host process table, and it exists so
// 60 suites do not each inline the same hoisted factory. It is never imported by product code.
// daemon-init-reset-modules-pin.ts re-applies those factories after resetModules so bun does
// not load the real ipc/pty barrel; also never imported by product code.
{
"files": ["**/mock-descendant-sweep.ts"],
"files": ["**/mock-descendant-sweep.ts", "**/daemon-init-reset-modules-pin.ts"],
"rules": {
"anti-slop/no-module-mocking": "off"
}
Expand Down
2 changes: 1 addition & 1 deletion config/scripts/orcad-e2e-routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ it('routes managed target-owner guards and menus to the real launch regression',
'tests/e2e/helpers/orcad-convert-flow.ts',
'tests/e2e/helpers/orcad-convert-host.ts',
'tests/e2e/helpers/orcad-upgrade-profile.ts',
'tests/e2e/helpers/docker-ssh-relay-target.ts'
'tests/e2e/helpers/docker-ssh-host-target.ts'
],
ORCAD_OPEN_IN_OWNER_E2E_SPEC
)
Expand Down
8 changes: 4 additions & 4 deletions config/scripts/pr-e2e-source-routing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const PR_E2E_SOURCE_ROUTES = [
id: 'ssh.orcad-browser-drop-owner',
specs: ['tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-relay-target|browser-split-(?:guest-probes|page-server))\.ts$/.test(
/^tests\/e2e\/helpers\/(?:orcad-managed-workspace|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target|browser-split-(?:guest-probes|page-server))\.ts$/.test(
file
) ||
(isProductSource(file) &&
Expand All @@ -156,7 +156,7 @@ export const PR_E2E_SOURCE_ROUTES = [
id: 'ssh.orcad-open-in-owner',
specs: ['tests/e2e/ssh-orcad-open-in-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-relay-target)\.ts$/.test(
/^tests\/e2e\/helpers\/(?:orcad-managed-workspace|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target)\.ts$/.test(
file
) ||
(isProductSource(file) &&
Expand All @@ -168,7 +168,7 @@ export const PR_E2E_SOURCE_ROUTES = [
id: 'ssh.orcad-terminal-root-owner',
specs: ['tests/e2e/ssh-orcad-terminal-root-owner.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:terminal-workspace-root-link|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-relay-target)\.ts$/.test(
/^tests\/e2e\/helpers\/(?:terminal-workspace-root-link|orcad-managed-workspace|orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-host-target)\.ts$/.test(
file
) ||
(isProductSource(file) &&
Expand Down Expand Up @@ -208,7 +208,7 @@ export const PR_E2E_SOURCE_ROUTES = [
specs: ['tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts'],
matches: (file) =>
file === 'tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts' ||
/^tests\/e2e\/helpers\/docker-ssh-relay-(?:image|target)\.ts$/.test(file) ||
/^tests\/e2e\/helpers\/docker-ssh-host-(?:image|target)\.ts$/.test(file) ||
(isProductSource(file) &&
/^src\/main\/(?:browser\/(?:ssh-browser-network-execution-route|browser-network-deferred-socket|browser-network-execution-route|system-ssh-socks-client-socket)|ssh\/system-ssh-dynamic-forward-process)\.ts$/.test(
file
Expand Down
6 changes: 3 additions & 3 deletions config/scripts/ssh-browser-e2e-routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ it('executes both Docker network routes in a Node job with their opt-in enabled'
'src/main/browser/browser-network-execution-route.ts',
'src/main/browser/system-ssh-socks-client-socket.ts',
'src/main/ssh/system-ssh-dynamic-forward-process.ts',
'tests/e2e/helpers/docker-ssh-relay-target.ts',
'tests/e2e/helpers/docker-ssh-relay-image.ts'
'tests/e2e/helpers/docker-ssh-host-target.ts',
'tests/e2e/helpers/docker-ssh-host-image.ts'
]) {
expect(selectPrE2eSpecs([changed])).toContain(spec)
}
expect(selectPrE2eSpecs(['src/renderer/src/components/Unrelated.tsx'])).not.toContain(spec)
expect(selectPrE2eSpecs(['tests/e2e/helpers/docker-ssh-relay-terminal-tabs.ts'])).not.toContain(
expect(selectPrE2eSpecs(['tests/e2e/helpers/docker-ssh-host-terminal-tabs.ts'])).not.toContain(
spec
)
})
8 changes: 4 additions & 4 deletions docs/assets/readme-downloads.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions docs/audits/SYNC-NOTES-2026-10-11d.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Sync notes: stablyai/orca `main` into `fork-main` (2026-10-11d)

Branch: `sync/upstream-2026-10-11d` from `origin/fork-main` (`cc1230a91d3`, PR #19).
Merge: `upstream/main` @ `b4aec8a8961` (`git merge --no-ff`).
Merge commit: `1e2fb380435`. Merge-base was `4468a5375f2`.

This is a follow-up to PR #19 (`sync/upstream-2026-10-11c`). Do not merge this
PR. Do not cut tags. Do not touch Coolify. Karl GO still stands to cut
`v1.4.224-0` after this PR merges.

## Policy

- Keep fork features listed in `config/fork-features.yml`.
- Re-apply #15/#17/#18/#19 resolutions where upstream re-touched the same files.
- Upstream wins for files the fork does not customize.
- Same relay-conflict discipline as 11c: SSH docker fixtures stay on the
host names (`docker-ssh-host`); no leftover `docker-ssh-relay` paths.

## Upstream commits (5, oldest first)

- `bdc0921718b` test(e2e): one posixShellQuote; docker-ssh-host fixture names (#27449)
- `10d51086716` feat(runtime): accept a declared spawn intent from paired clients (#27452)
- `c9a9ad33b8e` test(e2e): one managed-host setup path and skip gate for the ssh-orcad specs (#27465)
- `3b49ee40db4` Update README downloads badge
- `b4aec8a8961` fix(sidebar): stop a removed folder workspace from leaving a blank row (#26376)

## Conflict resolutions

None. `git merge --no-ff upstream/main` auto-merged five overlapping files.
Verified each auto-merge kept fork deltas and added only the upstream
`spawnIntent` / `terminal.spawn-intent.v1` pieces:

- `src/shared/protocol-version.ts` — kept
`SSH_TARGET_MANAGEMENT_RUNTIME_CAPABILITY` and
`...FORK_TERMINAL_AND_AGENT_LAUNCH_RUNTIME_CAPABILITIES`; added
`TERMINAL_SPAWN_INTENT_RUNTIME_CAPABILITY` after paired-parking.
- `src/shared/rpc-contract/agent-session-params.ts` — optional
`spawnIntent: SpawnIntentParam`.
- `src/shared/agent-session-host-authority.ts` — optional `spawnIntent` on
`RuntimeEnsureAgentSessionRequest`.
- `src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts` —
forward `params.spawnIntent` into ensure.
- `src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts` —
forward `request.spawnIntent` into create.

## Auto-merged fork semantics kept

- `PET_SPEAK_METHODS` in `src/main/runtime/rpc/methods/index.ts`
- `pet.speak.subscribe` as a `'none'` relayed stream carrier
- Fork `orcad-host-adapters.ts` extraction
- Automation `agentFlags` / `activeView` (untouched by these five commits)

## CI follow-up

- Shard 3/5 bun: `daemon-init-provider-installation` first test
(`re-binds listeners after the first daemon provider is installed`)
finished in ~2.3s with `setLocalPtyProviderMock` at 0 calls. bun's first
import after `vi.resetModules()` missed the hoisted `../ipc/pty` mock, so
the real PTY barrel loaded. Re-pin that mock (plus `node:fs` / `node:net`)
after reset and load `ipc/pty` before `daemon-provider-init`. The `vi.doMock`
calls live in test-only `daemon-init-reset-modules-pin.ts` (anti-slop
`no-module-mocking` is file-scoped off there, like `mock-descendant-sweep.ts`).

## Light local checks (this box, via `heavy-run`, glibc Node 22)

- `pnpm install --ignore-scripts` then `pnpm --filter @orca/process-host build`.
- `tsc --noEmit -p config/tsconfig.tc.cli.json`: clean.
- vitest `--config config/vitest.config.ts --maxWorkers=1`:
- spawn-intent + host-spawn-admission + rpc errors: 73 passed.
- worktree / folder-workspace metadata: 25 passed.
- e2e routing + global-setup-cli-artifact: 15 passed.
- pet-speak replay + mobile method routing: 4 passed.
- Full `pnpm tc` (node+web) and electron-builder not run locally.
4 changes: 4 additions & 0 deletions src/main/daemon/daemon-init-dependency-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ export function createDaemonInitModuleFactories(state: DaemonInitMockState) {
// Why: bun aliases `fs` to `node:fs`; doc-preview-file-access reads constants at import.
constants: { O_NOFOLLOW: 0, O_NONBLOCK: 0 },
mkdirSync: vi.fn<(...args: unknown[]) => void>(),
chmodSync: vi.fn<(...args: unknown[]) => void>(),
lstatSync: vi.fn(() => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}),
existsSync: (p: string) => probeSocketExistsMock(p) || p.includes('.pid'),
unlinkSync: unlinkSyncMock,
readFileSync: readFileSyncMock,
Expand Down
24 changes: 17 additions & 7 deletions src/main/daemon/daemon-init-fresh-import.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { vi } from 'vitest'
import { setAppEnvironment, type AppEnvironment } from '../../shared/app-environment'
import type { DaemonInitMockState } from './daemon-init-test-harness'
import {
pinDaemonInitMocksAfterReset,
type DaemonInitPinFactories
} from './daemon-init-reset-modules-pin'

export type { DaemonInitPinFactories }

/** Resets every mock plus the module registry, then re-imports daemon-init so its module-level spawner/adapter/restartInFlight start fresh. */
export async function importFreshDaemonInit(state: DaemonInitMockState) {
export async function importFreshDaemonInit(
state: DaemonInitMockState,
pinFactories?: DaemonInitPinFactories
) {
const {
getPathMock,
getAppPathMock,
Expand Down Expand Up @@ -124,12 +133,13 @@ export async function importFreshDaemonInit(state: DaemonInitMockState) {
exit: () => {},
getAppMetrics: () => []
} as unknown as AppEnvironment)
if (pinFactories) {
await pinDaemonInitMocksAfterReset(pinFactories)
}
// Why: import after resetModules so module-level spawner/adapter/restartInFlight start fresh — needed to test first-init and the coalescer.
const [init, restart, providerState, cleanup] = await Promise.all([
import('./daemon-provider-init'),
import('./daemon-provider-restart'),
import('./daemon-provider-state'),
import('./daemon-protocol-cleanup')
])
const init = await import('./daemon-provider-init')
const restart = await import('./daemon-provider-restart')
const providerState = await import('./daemon-provider-state')
const cleanup = await import('./daemon-protocol-cleanup')
return { ...init, ...restart, ...providerState, ...cleanup }
}
2 changes: 2 additions & 0 deletions src/main/daemon/daemon-init-provider-installation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ const {
)

vi.mock('fs', () => moduleFactories.fs())
vi.mock('node:fs', () => moduleFactories.fs())
vi.mock('child_process', async (importOriginal) =>
moduleFactories.childProcess(await importOriginal<Record<string, unknown>>())
)
vi.mock('net', () => moduleFactories.net())
vi.mock('node:net', () => moduleFactories.net())
vi.mock('./daemon-health', () => moduleFactories.daemonHealth())
vi.mock('./daemon-pid-identity', () => moduleFactories.daemonPidIdentity())
vi.mock('./daemon-tcc-attribution', () => moduleFactories.daemonTccAttribution())
Expand Down
27 changes: 27 additions & 0 deletions src/main/daemon/daemon-init-reset-modules-pin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { vi } from 'vitest'

/** Factories the test file's hoisted `vi.mock` calls already registered; re-applied after resetModules. */
export type DaemonInitPinFactories = {
fs: () => object
net: () => object
ipcPty: () => object
daemonSpawner: () => object
daemonPtyAdapter: () => object
}

/**
* bun's first import after resetModules can miss hoisted vi.mock factories (real ipc/pty
* barrel loads, setLocalPtyProviderMock stays at 0). Re-pin, then load ipc/pty first.
*/
export async function pinDaemonInitMocksAfterReset(
factories: DaemonInitPinFactories
): Promise<void> {
vi.doMock('fs', factories.fs)
vi.doMock('node:fs', factories.fs)
vi.doMock('net', factories.net)
vi.doMock('node:net', factories.net)
vi.doMock('../ipc/pty', factories.ipcPty)
vi.doMock('./daemon-spawner', factories.daemonSpawner)
vi.doMock('./daemon-pty-adapter', factories.daemonPtyAdapter)
await import('../ipc/pty')
}
12 changes: 10 additions & 2 deletions src/main/daemon/daemon-init-test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,19 @@ export function createDaemonInitMocks(): DaemonInitMocks {
})
}

const moduleFactories = createDaemonInitModuleFactories(state)
return {
...state,
...createNetConnectStubs(state),
mockConnectedAdoptionClientOnce,
importFresh: () => importFreshDaemonInit(state),
moduleFactories: createDaemonInitModuleFactories(state)
importFresh: () =>
importFreshDaemonInit(state, {
fs: moduleFactories.fs,
net: moduleFactories.net,
ipcPty: moduleFactories.ipcPty,
daemonSpawner: moduleFactories.daemonSpawner,
daemonPtyAdapter: moduleFactories.daemonPtyAdapter
}),
moduleFactories
}
}
79 changes: 79 additions & 0 deletions src/main/daemon/daemon-pty-adapter-spawn-intent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ function createPayloads(requestSpy: { mock: { calls: unknown[][] } }): unknown[]
return requestSpy.mock.calls.filter(([type]) => type === 'createOrAttach').map(([, p]) => p)
}

const CLAIM = {
claim: {
digestVersion: 1 as const,
keyId: 'key',
identityDigest: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
worktreeScopeDigest: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
agent: 'codex' as const
},
surface: {
worktreeId: 'worktree',
tabId: 'tab',
leafId: '11111111-1111-4111-8111-111111111111',
terminalHandle: 'term_intent_claim'
}
}

describe('daemon spawn intent wire', () => {
it('forwards a declared intent on create and omits it on attach-only', async () => {
const ensureConnectedSpy = vi
Expand Down Expand Up @@ -40,6 +56,69 @@ describe('daemon spawn intent wire', () => {
}
})

it('never forwards interactive for a claimed resume, which an older daemon would not gate', async () => {
const ensureConnectedSpy = vi
.spyOn(DaemonClient.prototype, 'ensureConnected')
.mockResolvedValue()
const requestSpy = vi.spyOn(DaemonClient.prototype, 'request').mockResolvedValue({
isNew: true,
pid: null,
shellState: 'unsupported',
snapshot: null
})
const adapter = new DaemonPtyAdapter({
socketPath: '/tmp/orca-intent-claim.sock',
tokenPath: '/tmp/orca-intent-claim.token'
})
try {
await adapter
.spawn({
sessionId: 'claimed',
cols: 80,
rows: 24,
spawnIntent: 'interactive',
agentSessionEnsure: CLAIM
})
.catch(() => {})
await adapter.spawn({ sessionId: 'shell', cols: 80, rows: 24, spawnIntent: 'interactive' })
const [claimed, shell] = createPayloads(requestSpy)
expect(claimed).toHaveProperty('spawnIntent', 'automatic-restore')
expect(shell).toHaveProperty('spawnIntent', 'interactive')
} finally {
adapter.dispose()
requestSpy.mockRestore()
ensureConnectedSpy.mockRestore()
}
})

it('passes a deferral back as-is instead of treating the daemon as gone', async () => {
const ensureConnectedSpy = vi
.spyOn(DaemonClient.prototype, 'ensureConnected')
.mockResolvedValue()
const requestSpy = vi
.spyOn(DaemonClient.prototype, 'request')
.mockRejectedValue(new Error('terminal_spawn_deferred:paused'))
const adapter = new DaemonPtyAdapter({
socketPath: '/tmp/orca-intent-deferred.sock',
tokenPath: '/tmp/orca-intent-deferred.token'
})
try {
await expect(
adapter.spawn({
sessionId: 'deferred',
cols: 80,
rows: 24,
spawnIntent: 'automatic-restore'
})
).rejects.toThrow('terminal_spawn_deferred:paused')
expect(createPayloads(requestSpy)).toHaveLength(1)
} finally {
adapter.dispose()
requestSpy.mockRestore()
ensureConnectedSpy.mockRestore()
}
})

it('reads the launcher flag and observes for anything else', () => {
expect(readTerminalSpawnAdmissionArg(['node', 'entry', '--spawn-admission', 'enforce'])).toBe(
'enforce'
Expand Down
Loading
Loading