@@ -3,9 +3,11 @@
|
3 | 3 | * Verifies secrets/state persistence, runtime overlays, and legacy JSON |
4 | 4 | * migration boundaries in temporary agent directories. |
5 | 5 | */ |
| 6 | +import { spawn } from "node:child_process"; |
6 | 7 | import fs from "node:fs"; |
7 | 8 | import os from "node:os"; |
8 | 9 | import path from "node:path"; |
| 10 | +import { DatabaseSync } from "node:sqlite"; |
9 | 11 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
10 | 12 | import { |
11 | 13 | closeOpenClawAgentDatabasesForTest, |
@@ -147,6 +149,68 @@ describe("auth profile sqlite store", () => {
|
147 | 149 | }); |
148 | 150 | }); |
149 | 151 | |
| 152 | +it("waits for brief rollback-journal contention before reading persisted auth", async () => { |
| 153 | +await withAgentDirEnv("openclaw-auth-sqlite-contention-", async (agentDir) => { |
| 154 | +saveAuthProfileStore(apiKeyStore("sk-test"), agentDir); |
| 155 | +closeOpenClawAgentDatabasesForTest(); |
| 156 | + |
| 157 | +const databasePath = resolveAuthProfileDatabasePath(agentDir); |
| 158 | +const setup = new DatabaseSync(databasePath); |
| 159 | +setup.exec("PRAGMA journal_mode = DELETE;"); |
| 160 | +setup.close(); |
| 161 | + |
| 162 | +const child = spawn( |
| 163 | +process.execPath, |
| 164 | +[ |
| 165 | +"-e", |
| 166 | +` |
| 167 | + const { DatabaseSync } = require("node:sqlite"); |
| 168 | + const db = new DatabaseSync(process.argv[1]); |
| 169 | + db.exec("PRAGMA journal_mode = DELETE; BEGIN EXCLUSIVE;"); |
| 170 | + db.prepare( |
| 171 | + "UPDATE auth_profile_store SET updated_at = updated_at + 1 WHERE store_key = ?", |
| 172 | + ).run("primary"); |
| 173 | + process.stdout.write("locked\\n"); |
| 174 | + setTimeout(() => { |
| 175 | + db.exec("ROLLBACK;"); |
| 176 | + db.close(); |
| 177 | + }, 250); |
| 178 | + `, |
| 179 | +databasePath, |
| 180 | +], |
| 181 | +{ stdio: ["ignore", "pipe", "pipe"] }, |
| 182 | +); |
| 183 | +const childExit = new Promise<void>((resolve, reject) => { |
| 184 | +child.once("error", reject); |
| 185 | +child.once("exit", (code) => { |
| 186 | +if (code === 0) { |
| 187 | +resolve(); |
| 188 | +} else { |
| 189 | +reject(new Error(`contention child exited with code ${code}`)); |
| 190 | +} |
| 191 | +}); |
| 192 | +}); |
| 193 | +await new Promise<void>((resolve, reject) => { |
| 194 | +let locked = false; |
| 195 | +child.stdout.once("data", () => { |
| 196 | +locked = true; |
| 197 | +resolve(); |
| 198 | +}); |
| 199 | +child.once("error", reject); |
| 200 | +child.once("exit", (code) => { |
| 201 | +if (!locked) { |
| 202 | +reject(new Error(`contention child exited before locking with code ${code}`)); |
| 203 | +} |
| 204 | +}); |
| 205 | +}); |
| 206 | + |
| 207 | +const loaded = loadPersistedAuthProfileStore(agentDir); |
| 208 | + |
| 209 | +await childExit; |
| 210 | +expect(loaded?.profiles["openai:default"]).toMatchObject({ key: "sk-test" }); |
| 211 | +}); |
| 212 | +}); |
| 213 | + |
150 | 214 | it("uses the configured agent id for custom agentDir databases", async () => { |
151 | 215 | await withAgentDirEnv("openclaw-auth-sqlite-custom-agent-", (envAgentDir) => { |
152 | 216 | const customAgentDir = path.join(path.dirname(path.dirname(envAgentDir)), "custom-coder"); |
|