mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(macos): accept current native state schema (#111032)
This commit is contained in:
@@ -2923,6 +2923,17 @@ jobs:
|
||||
xcodebuild -version
|
||||
swift --version
|
||||
|
||||
- name: Native state schema version contract
|
||||
run: |
|
||||
if [[ -f scripts/check-native-state-schema-version.mjs ]]; then
|
||||
node scripts/check-native-state-schema-version.mjs
|
||||
elif [[ "$HISTORICAL_TARGET" == "true" ]]; then
|
||||
echo "[skip] native state schema version guard is not present in this historical target"
|
||||
else
|
||||
echo "Current CI targets must provide scripts/check-native-state-schema-version.mjs." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Swift lint
|
||||
run: |
|
||||
if [[ -x ./scripts/lint-swift.sh && -x ./scripts/format-swift.sh ]]; then
|
||||
|
||||
@@ -528,7 +528,33 @@ struct PortGuardianRecordStoreTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func `foreign newer and incompatible databases fail closed`() throws {
|
||||
func `supported and newer Node schema versions open or fail closed`() throws {
|
||||
let fixture = try Self.fixture()
|
||||
defer { fixture.cleanup() }
|
||||
|
||||
for version in [4, 5] {
|
||||
let databaseURL = fixture.root.appendingPathComponent("supported-v\(version).sqlite")
|
||||
try Self.seedVersionedPortGuardianDatabase(databaseURL, schemaVersion: version)
|
||||
let store = try PortGuardianRecordStore(databaseURL: databaseURL)
|
||||
let record = Self.record(
|
||||
pid: Int32(4200 + version),
|
||||
port: 18780 + version,
|
||||
timestamp: Double(version))
|
||||
try store.upsert(record)
|
||||
#expect(try store.records() == [record])
|
||||
}
|
||||
|
||||
for version in [6, 99] {
|
||||
let databaseURL = fixture.root.appendingPathComponent("newer-v\(version).sqlite")
|
||||
try Self.seedVersionedPortGuardianDatabase(databaseURL, schemaVersion: version)
|
||||
#expect(throws: PortGuardianStoreError.self) {
|
||||
try PortGuardianRecordStore(databaseURL: databaseURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `foreign and incompatible databases fail closed`() throws {
|
||||
let fixture = try Self.fixture()
|
||||
defer { fixture.cleanup() }
|
||||
|
||||
@@ -538,12 +564,6 @@ struct PortGuardianRecordStoreTests {
|
||||
try PortGuardianRecordStore(databaseURL: foreignURL)
|
||||
}
|
||||
|
||||
let newerURL = fixture.root.appendingPathComponent("newer.sqlite")
|
||||
try Self.execute(newerURL, "PRAGMA user_version = 5")
|
||||
#expect(throws: PortGuardianStoreError.self) {
|
||||
try PortGuardianRecordStore(databaseURL: newerURL)
|
||||
}
|
||||
|
||||
let uniqueIndexURL = fixture.root.appendingPathComponent("unique-index.sqlite")
|
||||
try Self.execute(uniqueIndexURL, """
|
||||
CREATE TABLE macos_port_guardian_records (
|
||||
@@ -572,7 +592,7 @@ struct PortGuardianRecordStoreTests {
|
||||
try store.upsert(existing)
|
||||
try JSONEncoder().encode([existing]).write(to: fixture.legacyURL, options: [.atomic])
|
||||
|
||||
try Self.execute(fixture.databaseURL, "PRAGMA user_version = 5")
|
||||
try Self.execute(fixture.databaseURL, "PRAGMA user_version = 6")
|
||||
|
||||
#expect(throws: PortGuardianStoreError.self) {
|
||||
try store.upsert(Self.record(pid: 4243, port: 18790, timestamp: 43))
|
||||
@@ -649,6 +669,36 @@ struct PortGuardianRecordStoreTests {
|
||||
}
|
||||
}
|
||||
|
||||
private static func seedVersionedPortGuardianDatabase(
|
||||
_ databaseURL: URL,
|
||||
schemaVersion: Int) throws
|
||||
{
|
||||
try self.execute(databaseURL, """
|
||||
CREATE TABLE schema_meta (
|
||||
meta_key TEXT NOT NULL PRIMARY KEY,
|
||||
role TEXT NOT NULL,
|
||||
schema_version INTEGER NOT NULL,
|
||||
agent_id TEXT,
|
||||
app_version TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
) STRICT;
|
||||
INSERT INTO schema_meta (
|
||||
meta_key, role, schema_version, agent_id, app_version, created_at, updated_at
|
||||
) VALUES ('primary', 'global', \(schemaVersion), NULL, NULL, 1, 1);
|
||||
CREATE TABLE macos_port_guardian_records (
|
||||
pid INTEGER NOT NULL PRIMARY KEY,
|
||||
port INTEGER NOT NULL,
|
||||
command TEXT NOT NULL,
|
||||
mode TEXT NOT NULL,
|
||||
timestamp REAL NOT NULL
|
||||
) STRICT;
|
||||
CREATE INDEX idx_macos_port_guardian_records_port
|
||||
ON macos_port_guardian_records(port, timestamp DESC);
|
||||
PRAGMA user_version = \(schemaVersion);
|
||||
""")
|
||||
}
|
||||
|
||||
private static func scalarInt(_ databaseURL: URL, _ sql: String) throws -> Int64 {
|
||||
var database: OpaquePointer?
|
||||
guard sqlite3_open(databaseURL.path, &database) == SQLITE_OK, let database else {
|
||||
|
||||
@@ -35,7 +35,7 @@ public enum OpenClawNativeStateSQLiteValueType: Equatable, Sendable {
|
||||
/// One recursive connection lock serializes transactions and statement access.
|
||||
public final class OpenClawNativeStateSQLite: @unchecked Sendable {
|
||||
// Keep aligned with OPENCLAW_STATE_SCHEMA_VERSION. Native clients never upgrade this database.
|
||||
private static let maximumSupportedSchemaVersion: Int64 = 4
|
||||
private static let maximumSupportedSchemaVersion: Int64 = 5
|
||||
private static let defaultBusyTimeoutMilliseconds: Int32 = 5000
|
||||
|
||||
private struct SchemaObject: Hashable {
|
||||
|
||||
@@ -634,6 +634,14 @@ export function createChangedCheckPlan(result, options = {}) {
|
||||
if (hasMacosAppCiPath(result.paths)) {
|
||||
add("macOS app CI tests", ["test:macos:ci"], baseEnv);
|
||||
}
|
||||
if (lanes.apps || lanes.core) {
|
||||
addCommand(
|
||||
"native state schema version guard",
|
||||
"node",
|
||||
["scripts/check-native-state-schema-version.mjs"],
|
||||
baseEnv,
|
||||
);
|
||||
}
|
||||
|
||||
if (lanes.core || lanes.extensions) {
|
||||
add("database-first legacy-store guard", ["check:database-first-legacy-stores"]);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface NativeStateSchemaSources {
|
||||
swiftSource: string;
|
||||
typescriptSource: string;
|
||||
}
|
||||
|
||||
export function compareNativeStateSchemaVersions(sources: NativeStateSchemaSources): number;
|
||||
export function checkNativeStateSchemaVersion(
|
||||
readFileSync?: typeof import("node:fs").readFileSync,
|
||||
): number;
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
|
||||
const SWIFT_CONTRACT_PATH =
|
||||
"apps/shared/OpenClawKit/Sources/OpenClawNativeState/OpenClawNativeStateSQLite.swift";
|
||||
const TYPESCRIPT_CONTRACT_PATH = "src/state/openclaw-state-db-contract.ts";
|
||||
|
||||
function extractSingleVersion(source, pattern, label) {
|
||||
const matches = [...source.matchAll(pattern)];
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(`Expected exactly one ${label} declaration; found ${matches.length}`);
|
||||
}
|
||||
return Number(matches[0][1]);
|
||||
}
|
||||
|
||||
export function compareNativeStateSchemaVersions({ swiftSource, typescriptSource }) {
|
||||
const swiftVersion = extractSingleVersion(
|
||||
swiftSource,
|
||||
/^\s*private static let maximumSupportedSchemaVersion: Int64 = (\d+)\s*$/gmu,
|
||||
"Swift maximumSupportedSchemaVersion",
|
||||
);
|
||||
const typescriptVersion = extractSingleVersion(
|
||||
typescriptSource,
|
||||
/^export const OPENCLAW_STATE_SCHEMA_VERSION = (\d+);\s*$/gmu,
|
||||
"TypeScript OPENCLAW_STATE_SCHEMA_VERSION",
|
||||
);
|
||||
if (swiftVersion !== typescriptVersion) {
|
||||
throw new Error(
|
||||
`Native state schema version drift: Swift supports ${swiftVersion}, TypeScript owns ${typescriptVersion}`,
|
||||
);
|
||||
}
|
||||
return swiftVersion;
|
||||
}
|
||||
|
||||
export function checkNativeStateSchemaVersion(readFileSync = fs.readFileSync) {
|
||||
return compareNativeStateSchemaVersions({
|
||||
swiftSource: readFileSync(SWIFT_CONTRACT_PATH, "utf8"),
|
||||
typescriptSource: readFileSync(TYPESCRIPT_CONTRACT_PATH, "utf8"),
|
||||
});
|
||||
}
|
||||
|
||||
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
||||
try {
|
||||
const version = checkNativeStateSchemaVersion();
|
||||
console.log(`native state schema version guard passed (v${version})`);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ export const BOUNDARY_CHECKS = [
|
||||
["run", "lint:extensions:telegram-grammy-types"],
|
||||
],
|
||||
["lint:ui:no-raw-window-open", "pnpm", ["lint:ui:no-raw-window-open"]],
|
||||
["native-state-schema-version", "node", ["scripts/check-native-state-schema-version.mjs"]],
|
||||
].map(([label, command, args]) => ({ label, command, args }));
|
||||
|
||||
/**
|
||||
|
||||
@@ -770,6 +770,10 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
["scripts/check.mjs", ["test/scripts/check.test.ts"]],
|
||||
["scripts/check-changed.mjs", ["test/scripts/changed-lanes.test.ts"]],
|
||||
["scripts/check-max-lines-ratchet.mjs", ["test/scripts/check-max-lines-ratchet.test.ts"]],
|
||||
[
|
||||
"scripts/check-native-state-schema-version.mjs",
|
||||
["test/scripts/check-native-state-schema-version.test.ts"],
|
||||
],
|
||||
["config/max-lines-baseline.txt", ["test/scripts/check-max-lines-ratchet.test.ts"]],
|
||||
[".oxlintrc.json", ["test/scripts/oxlint-config.test.ts"]],
|
||||
[
|
||||
@@ -2175,6 +2179,10 @@ for (const sourcePath of CROSS_OS_RELEASE_CHECK_SOURCE_PATHS) {
|
||||
const TOOLING_DECLARATION_SOURCE_MIRRORS = [
|
||||
["scripts/build-stamp.d.mts", "scripts/build-stamp.mjs"],
|
||||
["scripts/ci-changed-scope.d.mts", "scripts/ci-changed-scope.mjs"],
|
||||
[
|
||||
"scripts/check-native-state-schema-version.d.mts",
|
||||
"scripts/check-native-state-schema-version.mjs",
|
||||
],
|
||||
["scripts/copy-bundled-plugin-metadata.d.mts", "scripts/copy-bundled-plugin-metadata.mjs"],
|
||||
["scripts/docs-link-audit.d.mts", "scripts/docs-link-audit.mjs"],
|
||||
["scripts/openclaw-npm-resume-run.d.mts", "scripts/openclaw-npm-resume-run.mjs"],
|
||||
|
||||
@@ -2092,6 +2092,27 @@ describe("scripts/changed-lanes", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("runs the native state schema guard for either contract owner", () => {
|
||||
for (const changedPath of [
|
||||
"apps/shared/OpenClawKit/Sources/OpenClawNativeState/OpenClawNativeStateSQLite.swift",
|
||||
"src/state/openclaw-state-db-contract.ts",
|
||||
]) {
|
||||
const plan = createChangedCheckPlan(detectChangedLanes([changedPath]), {
|
||||
env: { PATH: "/usr/bin" },
|
||||
platform: "linux",
|
||||
swiftlintAvailable: false,
|
||||
});
|
||||
|
||||
expect(plan.commands).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "native state schema version guard",
|
||||
bin: "node",
|
||||
args: ["scripts/check-native-state-schema-version.mjs"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("runs macOS app CI tests for macOS packaging scripts and owner tests", () => {
|
||||
for (const changedPath of [
|
||||
"scripts/codesign-mac-app.sh",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
checkNativeStateSchemaVersion,
|
||||
compareNativeStateSchemaVersions,
|
||||
} from "../../scripts/check-native-state-schema-version.mjs";
|
||||
|
||||
describe("native state schema version guard", () => {
|
||||
it("keeps the checked-in Swift and TypeScript contracts aligned", () => {
|
||||
expect(checkNativeStateSchemaVersion()).toBe(5);
|
||||
});
|
||||
|
||||
it("fails when a deliberate Swift fixture drifts behind TypeScript", () => {
|
||||
expect(() =>
|
||||
compareNativeStateSchemaVersions({
|
||||
swiftSource: "private static let maximumSupportedSchemaVersion: Int64 = 4\n",
|
||||
typescriptSource: "export const OPENCLAW_STATE_SCHEMA_VERSION = 5;\n",
|
||||
}),
|
||||
).toThrow("Native state schema version drift: Swift supports 4, TypeScript owns 5");
|
||||
});
|
||||
});
|
||||
@@ -3170,6 +3170,16 @@ describe("ci workflow guards", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("checks native and Node state schema versions in the macOS lane", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const schemaVersionStep = workflow.jobs["macos-swift"].steps.find(
|
||||
(step: WorkflowStep) => step.name === "Native state schema version contract",
|
||||
);
|
||||
|
||||
expect(schemaVersionStep.run).toContain("node scripts/check-native-state-schema-version.mjs");
|
||||
expect(schemaVersionStep.run).toContain('elif [[ "$HISTORICAL_TARGET" == "true" ]]');
|
||||
});
|
||||
|
||||
it("resets SwiftPM state between macOS release build retries", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const macosInstallStep = workflow.jobs["macos-swift"].steps.find(
|
||||
|
||||
@@ -246,6 +246,14 @@ describe("run-additional-boundary-checks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps native and Node state schema versions aligned in CI", () => {
|
||||
expect(BOUNDARY_CHECKS).toContainEqual({
|
||||
label: "native-state-schema-version",
|
||||
command: "node",
|
||||
args: ["scripts/check-native-state-schema-version.mjs"],
|
||||
});
|
||||
});
|
||||
|
||||
it("buffers grouped output and reports aggregate failures", async () => {
|
||||
const buffer = createOutputBuffer();
|
||||
const failures = await runChecks(
|
||||
|
||||
Reference in New Issue
Block a user