mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
test: prune shared guards and manual Swift policy checks (#41554)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -11,38 +11,6 @@ import Foundation
|
||||
// ios/MobileWebShellNavigationPolicy.swift \
|
||||
// tests/MobileWebShellChecks.swift && /tmp/mobile-web-shell-checks
|
||||
@main struct MobileWebShellChecks {
|
||||
static let session = "sess-01JN_aZ9"
|
||||
|
||||
static func parts(
|
||||
path: String,
|
||||
method: String = "GET",
|
||||
hasRangeHeader: Bool = false,
|
||||
scheme: String? = MobileWebShellOrigin.scheme,
|
||||
host: String? = session,
|
||||
port: Int? = nil,
|
||||
user: String? = nil,
|
||||
query: String? = nil,
|
||||
fragment: String? = nil,
|
||||
urlByteCount: Int = 64
|
||||
) -> MobileWebShellRequestParts {
|
||||
MobileWebShellRequestParts(
|
||||
method: method,
|
||||
hasRangeHeader: hasRangeHeader,
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port,
|
||||
user: user,
|
||||
query: query,
|
||||
fragment: fragment,
|
||||
percentEncodedPath: path,
|
||||
urlByteCount: urlByteCount
|
||||
)
|
||||
}
|
||||
|
||||
static func resolve(_ request: MobileWebShellRequestParts) -> String? {
|
||||
MobileWebShellOrigin.resolveRequestPath(request, sessionId: session)
|
||||
}
|
||||
|
||||
static func manifest(
|
||||
schemaVersion: Int = 1,
|
||||
entrypoint: String = "index.html",
|
||||
@@ -68,42 +36,6 @@ import Foundation
|
||||
}
|
||||
|
||||
|
||||
static func checkRequestResolution() {
|
||||
precondition(resolve(parts(path: "/")) == "/")
|
||||
precondition(resolve(parts(path: "")) == "/")
|
||||
precondition(resolve(parts(path: "/assets/aa.js")) == "/assets/aa.js")
|
||||
// A host a parser canonicalised must still bind to this session.
|
||||
precondition(resolve(parts(path: "/", host: session.uppercased())) == "/")
|
||||
|
||||
precondition(resolve(parts(path: "/", method: "POST")) == nil)
|
||||
precondition(resolve(parts(path: "/", method: "HEAD")) == nil)
|
||||
precondition(resolve(parts(path: "/", hasRangeHeader: true)) == nil)
|
||||
precondition(resolve(parts(path: "/", scheme: "https")) == nil)
|
||||
precondition(resolve(parts(path: "/", scheme: nil)) == nil)
|
||||
// Unicode folding would serve assets to unminted Kelvin-sign hosts.
|
||||
precondition(MobileWebShellOrigin.resolveRequestPath(
|
||||
parts(path: "/", host: "\u{212A}ey"),
|
||||
sessionId: "key"
|
||||
) == nil)
|
||||
precondition(MobileWebShellOrigin.resolveRequestPath(
|
||||
parts(path: "/", host: "KEY"),
|
||||
sessionId: "key"
|
||||
) == "/")
|
||||
precondition(resolve(parts(path: "/", host: "other-session")) == nil)
|
||||
precondition(resolve(parts(path: "/", host: nil)) == nil)
|
||||
precondition(resolve(parts(path: "/", port: 443)) == nil)
|
||||
precondition(resolve(parts(path: "/", user: "someone")) == nil)
|
||||
precondition(resolve(parts(path: "/", query: "v=1")) == nil)
|
||||
precondition(resolve(parts(path: "/", fragment: "frag")) == nil)
|
||||
precondition(resolve(parts(path: "/assets/%2e%2e/etc")) == nil)
|
||||
precondition(resolve(parts(path: "assets/aa.js")) == nil)
|
||||
precondition(resolve(parts(path: "/", urlByteCount: 8 * 1024)) == "/")
|
||||
precondition(resolve(parts(path: "/", urlByteCount: 8 * 1024 + 1)) == nil)
|
||||
precondition(MobileWebShellOrigin.resolveRequestPath(parts(path: "/"), sessionId: "") == nil)
|
||||
}
|
||||
|
||||
|
||||
|
||||
static func checkGenerationMap() {
|
||||
guard let built = generation(manifest()) else { preconditionFailure("manifest rejected") }
|
||||
// Only / may serve document bytes with CSP; no entrypoint alias.
|
||||
@@ -214,156 +146,9 @@ import Foundation
|
||||
|
||||
|
||||
|
||||
static func bridgeSource(
|
||||
isOurWebView: Bool = true,
|
||||
isMainFrame: Bool = true,
|
||||
hasCommittedDocument: Bool = true,
|
||||
originProtocol: String = MobileWebShellOrigin.scheme,
|
||||
originHost: String = session
|
||||
) -> MobileWebShellBridgeSource {
|
||||
MobileWebShellBridgeSource(
|
||||
isOurWebView: isOurWebView,
|
||||
isMainFrame: isMainFrame,
|
||||
hasCommittedDocument: hasCommittedDocument,
|
||||
originProtocol: originProtocol,
|
||||
originHost: originHost
|
||||
)
|
||||
}
|
||||
|
||||
static func acceptsBridge(_ source: MobileWebShellBridgeSource) -> Bool {
|
||||
MobileWebShellBridge.accepts(source, sessionId: session)
|
||||
}
|
||||
|
||||
|
||||
static func checkBridgeAcceptance() {
|
||||
precondition(acceptsBridge(bridgeSource()))
|
||||
// Simulator: WebKit ASCII-lowercases custom-scheme hosts; exact session comparison refuses every message.
|
||||
precondition(acceptsBridge(bridgeSource(originHost: "sess-01jn_az9")))
|
||||
precondition(acceptsBridge(bridgeSource(originHost: "SESS-01JN_AZ9")))
|
||||
|
||||
// A frame we did not serve.
|
||||
precondition(!acceptsBridge(bridgeSource(originHost: "sess-01JN_aZ8")))
|
||||
precondition(!acceptsBridge(bridgeSource(originHost: "")))
|
||||
precondition(!acceptsBridge(bridgeSource(originHost: "sess-01JN_aZ9.evil")))
|
||||
// Unicode folding would accept unminted Kelvin-sign hosts.
|
||||
precondition(!MobileWebShellBridge.accepts(
|
||||
bridgeSource(originHost: "\u{212A}ey"),
|
||||
sessionId: "key"
|
||||
))
|
||||
|
||||
// Another scheme reaching the same handler.
|
||||
precondition(!acceptsBridge(bridgeSource(originProtocol: "https")))
|
||||
precondition(!acceptsBridge(bridgeSource(originProtocol: "")))
|
||||
precondition(!acceptsBridge(bridgeSource(originProtocol: "orca-mobile-web ")))
|
||||
|
||||
// A subframe, and a message routed to a WebView that is not ours.
|
||||
precondition(!acceptsBridge(bridgeSource(isMainFrame: false)))
|
||||
precondition(!acceptsBridge(bridgeSource(isOurWebView: false)))
|
||||
|
||||
// The replaced same-origin document remains alive until the new load commits.
|
||||
precondition(!acceptsBridge(bridgeSource(hasCommittedDocument: false)))
|
||||
|
||||
// No applied session is not an empty one: nothing may be accepted before a load.
|
||||
precondition(!MobileWebShellBridge.accepts(bridgeSource(originHost: ""), sessionId: ""))
|
||||
precondition(!MobileWebShellBridge.accepts(bridgeSource(originHost: "a b"), sessionId: "a b"))
|
||||
}
|
||||
|
||||
static func checkBridgePostTarget() {
|
||||
func canPost(
|
||||
_ host: String?,
|
||||
_ sessionId: String = session,
|
||||
committed: Bool = true
|
||||
) -> Bool {
|
||||
MobileWebShellBridge.canPost(
|
||||
toFrameOriginHost: host,
|
||||
sessionId: sessionId,
|
||||
hasCommittedDocument: committed
|
||||
)
|
||||
}
|
||||
|
||||
precondition(canPost(session))
|
||||
// The same ASCII fold as acceptance: WebKit reports the host lowercased.
|
||||
precondition(canPost("sess-01jn_az9"))
|
||||
|
||||
// Never post without an accepted frame: before first message, after failure/death, or with bridge disabled.
|
||||
precondition(!canPost(nil))
|
||||
|
||||
// A frame from another document, and a frame under no session at all.
|
||||
precondition(!canPost("sess-01JN_aZ8"))
|
||||
precondition(!canPost("\u{212A}ey", "key"))
|
||||
precondition(!canPost(session, ""))
|
||||
|
||||
// Provisional navigation invalidates the held frame until commit.
|
||||
precondition(!canPost(session, committed: false))
|
||||
}
|
||||
|
||||
|
||||
// One allow/opener decision; TypeScript’s readBridgeExternalLinkUrl owns scheme filtering.
|
||||
static func checkNavigationVerdict() {
|
||||
let foreign = "https://example.com/artifact-link"
|
||||
let document = "orca-mobile-web://\(session)/"
|
||||
func verdict(
|
||||
_ url: String? = "https://example.com/artifact-link",
|
||||
isMainFrame: Bool = true,
|
||||
isDocumentUrl: Bool = false,
|
||||
isShellLoad: Bool = false,
|
||||
hasGesture: Bool = true,
|
||||
isDownload: Bool = false
|
||||
) -> MobileWebShellNavigationVerdict {
|
||||
MobileWebShellNavigationPolicy.verdict(
|
||||
url: url,
|
||||
isMainFrame: isMainFrame,
|
||||
isDocumentUrl: isDocumentUrl,
|
||||
isShellLoad: isShellLoad,
|
||||
hasGesture: hasGesture,
|
||||
isDownload: isDownload
|
||||
)
|
||||
}
|
||||
precondition(verdict() == .cancelAndOffer(foreign))
|
||||
// WebKit reports our own load with main-frame source and target.
|
||||
precondition(verdict(document, isDocumentUrl: true, isShellLoad: true, hasGesture: false) == .allow)
|
||||
// Never offer/reload other document-URL navigations; sandboxed top navigation can report no gesture.
|
||||
precondition(verdict(document, isDocumentUrl: true, hasGesture: false) == .cancel)
|
||||
precondition(verdict(document, isDocumentUrl: true, hasGesture: true) == .cancel)
|
||||
// A top-page meta refresh or a redirect to somewhere else: refused, and never opened.
|
||||
precondition(verdict(hasGesture: false) == .cancel)
|
||||
// Gestured downloads reach the opener, never the document-load allowance.
|
||||
precondition(
|
||||
verdict(document, isDocumentUrl: true, isShellLoad: true, hasGesture: false, isDownload: true)
|
||||
== .cancel
|
||||
)
|
||||
precondition(verdict(isDownload: true) == .cancelAndOffer(foreign))
|
||||
// Document-URL downloads are never offered, regardless of gesture.
|
||||
precondition(verdict(document, isDocumentUrl: true, isDownload: true) == .cancel)
|
||||
// A foreign-URL subframe is never offered: the user is not leaving the app.
|
||||
precondition(verdict(isMainFrame: false) == .cancel)
|
||||
// Sealed srcdoc frames (HTML previews, chat visuals) load; no other subframe or top-level srcdoc does.
|
||||
let srcdoc = MobileWebShellNavigationPolicy.inlineDocumentUrl
|
||||
precondition(verdict(srcdoc, isMainFrame: false, hasGesture: false) == .allow)
|
||||
precondition(verdict(srcdoc, isMainFrame: false, isDownload: true) == .cancel)
|
||||
precondition(verdict(srcdoc, hasGesture: false) == .cancel)
|
||||
precondition(verdict("about:blank", isMainFrame: false, hasGesture: false) == .cancel)
|
||||
precondition(verdict(document, isMainFrame: false, isDocumentUrl: true, isShellLoad: true) == .cancel)
|
||||
precondition(verdict(nil) == .cancel)
|
||||
precondition(verdict("") == .cancel)
|
||||
// The crossing cap, at it and one past it.
|
||||
let cap = MobileWebShellNavigationPolicy.maxCancelledNavigationUrlCharacters
|
||||
let prefix = "https://example.com/"
|
||||
let atCap = prefix + String(repeating: "a", count: cap - prefix.count)
|
||||
precondition(MobileWebShellNavigationPolicy.offerableUrl(atCap) == atCap)
|
||||
precondition(MobileWebShellNavigationPolicy.offerableUrl(atCap + "a") == nil)
|
||||
precondition(verdict(atCap + "a") == .cancel)
|
||||
// A scheme the opener will refuse still crosses: one filter, in the half that updates.
|
||||
precondition(verdict("javascript:alert(1)") == .cancelAndOffer("javascript:alert(1)"))
|
||||
}
|
||||
|
||||
static func main() {
|
||||
checkRequestResolution()
|
||||
checkGenerationMap()
|
||||
checkLoadStateMachine()
|
||||
checkNavigationVerdict()
|
||||
checkBridgeAcceptance()
|
||||
checkBridgePostTarget()
|
||||
print("mobile web shell checks OK")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
@main
|
||||
struct RecordingInterruptionIntentTests {
|
||||
static func main() {
|
||||
var intent = RecordingInterruptionIntent()
|
||||
// An idle audio interruption never starts a recording.
|
||||
intent.began(wasRecording: false)
|
||||
precondition(intent.ended(shouldResume: true, isRecording: false) == .blocked)
|
||||
// A still-owned recording resumes once, not again on a duplicate end event.
|
||||
intent.began(wasRecording: true)
|
||||
precondition(intent.ended(shouldResume: true, isRecording: false) == .resume)
|
||||
precondition(intent.ended(shouldResume: true, isRecording: false) == .blocked)
|
||||
// A blocked or missing shouldResume bit consumes the old resume request.
|
||||
intent.began(wasRecording: true)
|
||||
precondition(intent.ended(shouldResume: false, isRecording: false) == .blocked)
|
||||
precondition(intent.ended(shouldResume: true, isRecording: false) == .blocked)
|
||||
// JavaScript cancels capture after began; ended must not turn its microphone back on.
|
||||
intent.began(wasRecording: true)
|
||||
intent.cancel()
|
||||
precondition(intent.ended(shouldResume: true, isRecording: false) == .blocked)
|
||||
// A newer explicit recording must not be cancelled by the old end notification.
|
||||
intent.began(wasRecording: true)
|
||||
intent.cancel()
|
||||
precondition(intent.ended(shouldResume: true, isRecording: true) == .none)
|
||||
// The most recent interruption, even one while idle, replaces old resume intent.
|
||||
intent.began(wasRecording: true)
|
||||
intent.began(wasRecording: false)
|
||||
precondition(intent.ended(shouldResume: true, isRecording: false) == .blocked)
|
||||
print("RecordingInterruptionIntent: 8 assertions passed")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BoundedMap } from './bounded-map'
|
||||
|
||||
describe('BoundedMap', () => {
|
||||
@@ -29,41 +29,6 @@ describe('BoundedMap', () => {
|
||||
expect(m.has('c')).toBe(true)
|
||||
})
|
||||
|
||||
it('never reports success for an entry it did not retain', () => {
|
||||
const onEvict = vi.fn()
|
||||
// An entry ceiling above total once admitted then evicted the same key while set returned true.
|
||||
const m = new BoundedMap<string, string>({
|
||||
maxEntries: 10,
|
||||
maxBytes: 3,
|
||||
maxEntryBytes: 5,
|
||||
sizeOf: (v) => v.length,
|
||||
onEvict
|
||||
})
|
||||
m.set('s1', 'x')
|
||||
m.set('s2', 'y')
|
||||
expect(m.set('big', 'xxxx')).toBe(false)
|
||||
expect(m.has('big')).toBe(false)
|
||||
expect(m.has('s1')).toBe(true)
|
||||
expect(m.has('s2')).toBe(true)
|
||||
expect(onEvict).not.toHaveBeenCalled()
|
||||
expect(m.retainedBytes).toBe(2)
|
||||
})
|
||||
|
||||
it('fails closed on an unmeasurable weight instead of poisoning the ledger', () => {
|
||||
const m = new BoundedMap<string, string>({
|
||||
maxEntries: 10,
|
||||
maxBytes: 10,
|
||||
sizeOf: (v) => (v === 'bad' ? Number.NaN : v.length)
|
||||
})
|
||||
expect(m.set('ok', 'abc')).toBe(true)
|
||||
expect(m.set('nan', 'bad')).toBe(false)
|
||||
expect(m.set('neg', 'x')).toBe(true)
|
||||
expect(m.retainedBytes).toBe(4)
|
||||
expect(Number.isFinite(m.retainedBytes)).toBe(true)
|
||||
// ceiling still armed after the rejected weight
|
||||
expect(m.set('huge', 'zzzzzzzzzzzz')).toBe(false)
|
||||
})
|
||||
|
||||
it('a rejected oversized overwrite leaves the existing value untouched', () => {
|
||||
const m = new BoundedMap<string, string>({
|
||||
maxEntries: 10,
|
||||
@@ -92,67 +57,4 @@ describe('BoundedMap', () => {
|
||||
['b', 2]
|
||||
])
|
||||
})
|
||||
|
||||
it('an overflow-rejected overwrite keeps the prior value and leaves no orphaned weight', () => {
|
||||
const weights = new Map<string, number>([
|
||||
['a', Number.MAX_SAFE_INTEGER - 1],
|
||||
['b', 1]
|
||||
])
|
||||
const m = new BoundedMap<string, string>({
|
||||
maxEntries: 10,
|
||||
maxBytes: Number.MAX_SAFE_INTEGER,
|
||||
sizeOf: (_v, k) => weights.get(k) ?? 0
|
||||
})
|
||||
m.set('a', 'A')
|
||||
m.set('b', 'B')
|
||||
weights.set('b', 3) // next weight for 'b' exceeds the remaining headroom
|
||||
expect(m.set('b', 'B2')).toBe(false)
|
||||
expect(m.has('b')).toBe(true) // rejection must not displace the prior value
|
||||
expect(m.peek('b')).toBe('B')
|
||||
expect(m.retainedBytes).toBe(Number.MAX_SAFE_INTEGER)
|
||||
// the rejected key must not strand a weight: deleting everything returns the ledger to zero
|
||||
weights.set('b', 1)
|
||||
m.delete('a')
|
||||
m.delete('b')
|
||||
expect(m.retainedBytes).toBe(0)
|
||||
expect(m.size).toBe(0)
|
||||
})
|
||||
|
||||
it('rejects the entry when sizeOf throws instead of propagating', () => {
|
||||
const m = new BoundedMap<string, string>({
|
||||
maxEntries: 4,
|
||||
maxBytes: 50,
|
||||
sizeOf: (v) => {
|
||||
if (v === 'boom') {
|
||||
throw new Error('measure failed')
|
||||
}
|
||||
return v.length
|
||||
}
|
||||
})
|
||||
expect(m.set('ok', 'abc')).toBe(true)
|
||||
expect(() => m.set('bad', 'boom')).not.toThrow()
|
||||
expect(m.set('bad', 'boom')).toBe(false)
|
||||
expect(m.has('bad')).toBe(false)
|
||||
expect(m.retainedBytes).toBe(3)
|
||||
})
|
||||
|
||||
it('runs every disposal even when one onEvict throws', () => {
|
||||
const disposed: string[] = []
|
||||
const m = new BoundedMap<string, string>({
|
||||
maxEntries: 10,
|
||||
maxBytes: 2,
|
||||
sizeOf: (v) => v.length,
|
||||
onEvict: (_v, k) => {
|
||||
disposed.push(k)
|
||||
if (k === 'a') {
|
||||
throw new Error('dispose failed')
|
||||
}
|
||||
}
|
||||
})
|
||||
m.set('a', 'x')
|
||||
m.set('b', 'y')
|
||||
expect(() => m.set('c', 'zz')).toThrow('dispose failed')
|
||||
expect(disposed).toEqual(['a', 'b']) // 'b' still disposed despite the earlier throw
|
||||
expect(m.retainedBytes).toBeLessThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { withClaudeAgentViewPlugin } from './claude-agent-view-launch'
|
||||
import {
|
||||
tokenizeStartupCommand,
|
||||
type AgentStartupShell
|
||||
} from './agent-launch/startup/tui-agent-startup-shell'
|
||||
|
||||
it.each(['posix', 'powershell', 'cmd'] as const)(
|
||||
'preserves existing plugin arguments and prompts in %s',
|
||||
(shell: AgentStartupShell) => {
|
||||
const command = 'claude --plugin-dir user-plugin --resume session -- "my prompt"'
|
||||
const result = withClaudeAgentViewPlugin(command, 'orca plugin', shell)
|
||||
const parsed = tokenizeStartupCommand(result, shell)
|
||||
expect(parsed.ok && parsed.tokens).toEqual([
|
||||
'claude',
|
||||
'--plugin-dir',
|
||||
'orca plugin',
|
||||
'--plugin-dir',
|
||||
'user-plugin',
|
||||
'--resume',
|
||||
'session',
|
||||
'--',
|
||||
'my prompt'
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['ssh host claude', 'bash -c claude', 'echo /bin/claude', 'claude && echo done'])(
|
||||
'leaves shell syntax and ambiguous executables unchanged: %s',
|
||||
(command) => {
|
||||
expect(withClaudeAgentViewPlugin(command, '/orca/plugin', 'posix')).toBe(command)
|
||||
}
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { cleanCloudServiceOrigin } from './cloud-service-url'
|
||||
it.each(['ftp://localhost', 'file://localhost', 'ws://localhost', 'http://example.com'])(
|
||||
it.each(['ftp://localhost', 'http://example.com'])(
|
||||
'rejects %s even with the development loopback exception',
|
||||
(url) => {
|
||||
expect(cleanCloudServiceOrigin(url, true)).toBeNull()
|
||||
|
||||
Reference in New Issue
Block a user