fix(macos): accept current native state schema (#111032)

This commit is contained in:
Peter Steinberger
2026-07-18 19:58:37 -07:00
committed by GitHub
parent 208036afdc
commit 28ce6b8116
12 changed files with 207 additions and 9 deletions
+11
View File
@@ -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 {
+8
View File
@@ -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 }));
/**
+8
View File
@@ -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"],
+21
View File
@@ -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");
});
});
+10
View File
@@ -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(