forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathChatToolExecutorSQLTests.swift
More file actions
389 lines (344 loc) · 13.8 KB
/
Copy pathChatToolExecutorSQLTests.swift
File metadata and controls
389 lines (344 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
import GRDB
import XCTest
@testable import Omi_Computer
private final class OwnerAuthorizationSequence: @unchecked Sendable {
private let lock = NSLock()
private var remainingAllowedChecks: Int
init(allowedChecks: Int) {
remainingAllowedChecks = allowedChecks
}
func isCurrent() -> Bool {
lock.lock()
defer { lock.unlock() }
guard remainingAllowedChecks > 0 else { return false }
remainingAllowedChecks -= 1
return true
}
}
private struct OwnerFenceDefaultsReference: @unchecked Sendable {
let value: UserDefaults
}
private final class SQLTransactionObserver: TransactionObserver, @unchecked Sendable {
private let blocksCommit: Bool
private let commitRelease = DispatchSemaphore(value: 0)
private let lock = NSLock()
private var reachedWillCommit = false
private var willCommitWaiters: [CheckedContinuation<Void, Never>] = []
private var observedDML = false
private var committed = false
private var rolledBack = false
init(blocksCommit: Bool = false) {
self.blocksCommit = blocksCommit
}
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { true }
func databaseDidChange(with event: DatabaseEvent) {
lock.withLock { observedDML = true }
}
func databaseWillCommit() throws {
let waiters = lock.withLock { () -> [CheckedContinuation<Void, Never>] in
reachedWillCommit = true
let waiters = willCommitWaiters
willCommitWaiters.removeAll()
return waiters
}
waiters.forEach { $0.resume() }
if blocksCommit { commitRelease.wait() }
}
func databaseDidCommit(_ db: Database) {
lock.withLock { committed = true }
}
func databaseDidRollback(_ db: Database) {
lock.withLock { rolledBack = true }
}
func waitUntilWillCommit() async {
if lock.withLock({ reachedWillCommit }) { return }
await withCheckedContinuation { continuation in
let resumeImmediately = lock.withLock { () -> Bool in
if reachedWillCommit { return true }
willCommitWaiters.append(continuation)
return false
}
if resumeImmediately { continuation.resume() }
}
}
func allowCommit() {
commitRelease.signal()
}
func snapshot() -> (observedDML: Bool, committed: Bool, rolledBack: Bool) {
lock.withLock { (observedDML, committed, rolledBack) }
}
}
final class ChatToolExecutorSQLTests: XCTestCase {
private var originalAuthOwner: String?
private var originalOwnerOverride: String?
private var originalOwnerBackup: String?
override func setUp() async throws {
try await super.setUp()
originalAuthOwner = UserDefaults.standard.string(forKey: .authUserId)
originalOwnerOverride = UserDefaults.standard.string(forKey: .automationOwnerOverride)
originalOwnerBackup = UserDefaults.standard.string(forKey: .automationOwnerABackup)
await restoreOriginalOwnerDefaults()
}
override func tearDown() async throws {
await restoreOriginalOwnerDefaults()
try await super.tearDown()
}
func testReadOnlySQLAllowsSelectAndReadOnlyCTE() {
XCTAssertTrue(ChatToolExecutor.isReadOnlySQLStatement("SELECT * FROM screenshots LIMIT 1"))
XCTAssertTrue(
ChatToolExecutor.isReadOnlySQLStatement(
"WITH recent AS (SELECT * FROM screenshots LIMIT 5) SELECT * FROM recent"
)
)
}
func testReadOnlySQLBlocksDataModifyingCTEs() {
XCTAssertFalse(
ChatToolExecutor.isReadOnlySQLStatement(
"WITH target AS (SELECT id FROM screenshots LIMIT 1) DELETE FROM screenshots WHERE id IN (SELECT id FROM target) RETURNING id"
)
)
XCTAssertFalse(
ChatToolExecutor.isReadOnlySQLStatement(
"WITH target AS (SELECT id FROM action_items LIMIT 1) UPDATE action_items SET completed = 1 WHERE id IN (SELECT id FROM target) RETURNING id"
)
)
XCTAssertFalse(
ChatToolExecutor.isReadOnlySQLStatement(
"WITH new_row AS (SELECT 'x' AS value) INSERT INTO action_items (description) SELECT value FROM new_row RETURNING id"
)
)
}
func testReadOnlySQLIgnoresMutatingWordsInsideLiteralsAndComments() {
XCTAssertTrue(
ChatToolExecutor.isReadOnlySQLStatement(
"SELECT * FROM screenshots WHERE ocrText LIKE '%DELETE%' -- UPDATE later"
)
)
XCTAssertTrue(
ChatToolExecutor.isReadOnlySQLStatement(
"WITH words AS (SELECT 'INSERT UPDATE DELETE' AS text) SELECT text FROM words"
)
)
}
func testSQLAuthorizationIsOutsideSwiftPhysicalPreconditions() {
XCTAssertEqual(
ChatToolExecutor.physicalExecutionPrecondition(toolName: "execute_sql"),
.satisfied
)
}
func testExecuteSQLBindsDMLParametersAndPersistsMutations() async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("execute-sql-parameters-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let pool = try DatabasePool(path: directory.appendingPathComponent("test.sqlite").path)
try await pool.write { db in
try db.execute(sql: "CREATE TABLE probe (person TEXT PRIMARY KEY, note TEXT NOT NULL)")
}
let insertResult = await ChatToolExecutor.executeSQL(
["query": "INSERT INTO probe (person, note) VALUES (?, ?)", "parameters": ["O'Connor", "created"]],
dbQueue: pool,
expectedOwnerID: nil
)
XCTAssertEqual(insertResult, "OK: 1 row(s) affected")
let noteAfterInsert = try await pool.read { db in
try String.fetchOne(db, sql: "SELECT note FROM probe WHERE person = ?", arguments: ["O'Connor"])
}
XCTAssertEqual(noteAfterInsert, "created")
let updateResult = await ChatToolExecutor.executeSQL(
["query": "UPDATE probe SET note = ? WHERE person = ?", "parameters": ["updated", "O'Connor"]],
dbQueue: pool,
expectedOwnerID: nil
)
XCTAssertEqual(updateResult, "OK: 1 row(s) affected")
let noteAfterUpdate = try await pool.read { db in
try String.fetchOne(db, sql: "SELECT note FROM probe WHERE person = ?", arguments: ["O'Connor"])
}
XCTAssertEqual(noteAfterUpdate, "updated")
let unsafeUpdateResult = await ChatToolExecutor.executeSQL(
["query": "UPDATE probe SET note = ?", "parameters": ["unsafe"]],
dbQueue: pool,
expectedOwnerID: nil
)
XCTAssertEqual(unsafeUpdateResult, "Error: UPDATE without WHERE clause is not allowed")
let noteAfterRejectedUpdate = try await pool.read { db in
try String.fetchOne(db, sql: "SELECT note FROM probe WHERE person = ?", arguments: ["O'Connor"])
}
XCTAssertEqual(noteAfterRejectedUpdate, "updated")
let deleteResult = await ChatToolExecutor.executeSQL(
["query": "DELETE FROM probe WHERE person = ?", "parameters": ["O'Connor"]],
dbQueue: pool,
expectedOwnerID: nil
)
XCTAssertEqual(deleteResult, "OK: 1 row(s) affected")
let remainingRows = try await pool.read { db in
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM probe")
}
XCTAssertEqual(remainingRows, 0)
}
func testPostDMLOwnerRevocationRollsBackPrimarySQLWrite() async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("owner-bound-sql-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let pool = try DatabasePool(path: directory.appendingPathComponent("test.sqlite").path)
try await pool.write { db in
try db.execute(sql: "CREATE TABLE probe (value TEXT NOT NULL)")
}
let observer = SQLTransactionObserver()
pool.add(transactionObserver: observer, extent: .nextTransaction)
// Initial guard, lease admission, and in-transaction preflight pass. The
// fourth check runs after INSERT and deliberately revokes authorization.
let authorization = OwnerAuthorizationSequence(allowedChecks: 3)
let result = try await ChatToolExecutor.executeWriteQuery(
"INSERT INTO probe(value) VALUES ('must-roll-back')",
dbQueue: pool,
expectedOwnerID: "owner-a",
ownerIsCurrent: { _ in authorization.isCurrent() }
)
XCTAssertEqual(result, ChatToolExecutor.authorizedOwnerChangedResult())
let count = try await pool.read { db in
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM probe") ?? -1
}
XCTAssertEqual(count, 0)
let transaction = observer.snapshot()
XCTAssertTrue(transaction.observedDML, "the production SQL DML must execute before revocation")
XCTAssertFalse(transaction.committed)
XCTAssertTrue(transaction.rolledBack, "the post-DML authorization check must roll back")
}
@MainActor
func testEffectiveOwnerTransitionWaitsThroughPhysicalSQLCommit() async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("owner-commit-fence-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let pool = try DatabasePool(path: directory.appendingPathComponent("test.sqlite").path)
try await pool.write { db in
try db.execute(sql: "CREATE TABLE probe (value TEXT NOT NULL)")
}
let observer = SQLTransactionObserver(blocksCommit: true)
pool.add(transactionObserver: observer, extent: .nextTransaction)
let suiteName = "OwnerCommitFence.\(UUID().uuidString)"
let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set("owner-a", forKey: .authUserId)
let defaultsReference = OwnerFenceDefaultsReference(value: defaults)
let writeTask = Task.detached {
try await ChatToolExecutor.executeWriteQuery(
"INSERT INTO probe(value) VALUES ('committed-for-owner-a')",
dbQueue: pool,
expectedOwnerID: "owner-a",
ownerIsCurrent: { expectedOwnerID in
RuntimeOwnerIdentity.currentOwnerId(
defaults: defaultsReference.value,
allowAutomationOverride: true
) == expectedOwnerID
}
)
}
await observer.waitUntilWillCommit()
let transitionTask = Task { @MainActor in
await RuntimeOwnerIdentity.applyAutomationOwnerOverride(
"owner-b",
defaults: defaultsReference.value
)
}
await EffectiveOwnerTransitionFence.shared.waitUntilTransitionIsPending()
XCTAssertNil(defaults.string(forKey: .automationOwnerOverride))
XCTAssertEqual(defaults.string(forKey: .authUserId), "owner-a")
observer.allowCommit()
_ = try await writeTask.value
_ = await transitionTask.value
let count = try await pool.read { db in
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM probe") ?? -1
}
XCTAssertEqual(count, 1)
XCTAssertEqual(defaults.string(forKey: .automationOwnerOverride), "owner-b")
let transaction = observer.snapshot()
XCTAssertTrue(transaction.observedDML)
XCTAssertTrue(transaction.committed)
XCTAssertFalse(transaction.rolledBack)
}
@MainActor
func testKernelStampedReadOnlySQLRejectsPhysicalMutationInput() async {
await Self.establishStandardOwner("sql-owner")
let result = await ChatToolExecutor.execute(
ToolCall(
name: "execute_sql",
arguments: [
"query": "UPDATE action_items SET completed = 1 WHERE id = 42",
"read_only": true,
],
thoughtSignature: nil
),
expectedOwnerID: "sql-owner"
)
XCTAssertEqual(
result,
"Error: this SQL surface is read-only. Use SELECT or read-only WITH queries."
)
}
private static func establishStandardOwner(_ ownerID: String?) async {
let bootstrapOwner = "chat-tool-sql-owner-bootstrap"
await Self.transitionStandardOwner(to: ownerID == bootstrapOwner ? nil : bootstrapOwner)
await Self.transitionStandardOwner(to: ownerID)
}
private static func transitionStandardOwner(to ownerID: String?) async {
do {
try await RuntimeOwnerIdentity.performEffectiveOwnerTransition(
defaults: .standard,
allowAutomationOverride: false,
plannedNextOwner: { _, _ in ownerID },
quiesceVoice: { _, _ in },
revokeKernelOwner: { _, _ in },
retargetLocalStorage: { _, _ in },
ownerDidChange: {}
) { defaults in
defaults.removeObject(forKey: .automationOwnerOverride)
defaults.removeObject(forKey: .automationOwnerABackup)
if let ownerID {
defaults.set(ownerID, forKey: .authUserId)
} else {
defaults.removeObject(forKey: .authUserId)
}
}
} catch {
XCTFail("owner transition failed: \(error)")
}
}
private func restoreOriginalOwnerDefaults() async {
let authOwner = originalAuthOwner
let ownerOverride = originalOwnerOverride
let ownerBackup = originalOwnerBackup
let effectiveOwner =
ownerOverride?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
? ownerOverride
: authOwner
await Self.transitionStandardOwner(to: "chat-tool-sql-owner-restore")
do {
try await RuntimeOwnerIdentity.performEffectiveOwnerTransition(
defaults: .standard,
allowAutomationOverride: true,
plannedNextOwner: { _, _ in effectiveOwner },
quiesceVoice: { _, _ in },
revokeKernelOwner: { _, _ in },
retargetLocalStorage: { _, _ in },
ownerDidChange: {}
) { defaults in
for (key, value) in [
(DefaultsKey.authUserId, authOwner),
(DefaultsKey.automationOwnerOverride, ownerOverride),
(DefaultsKey.automationOwnerABackup, ownerBackup),
] {
if let value {
defaults.set(value, forKey: key.rawValue)
} else {
defaults.removeObject(forKey: key.rawValue)
}
}
}
} catch {
XCTFail("owner restoration failed: \(error)")
}
}
}